

摊还分析是算法分析中的一种重要技术,用于分析一个操作序列的平均代价。与最坏情况分析不同,摊还分析关注的是一系列操作的整体性能,而不是单个操作的最坏情况。这种分析方法对于评估那些偶尔会执行昂贵操作,但大多数时候操作都很高效的数据结构特别有用。
本文将详细讲解《算法导论》第 17 章介绍的三种摊还分析方法:聚合分析、核算法和势能法,并通过动态表的例子展示这些方法的应用。


聚合分析(Aggregate Analysis)是最简单的摊还分析方法。它的基本思想是:对所有操作序列,计算其总代价的上界,然后将这个总代价除以操作数,得到每个操作的平均代价,这个平均代价就是每个操作的摊还代价。
聚合分析的步骤:
考虑栈的三种操作:
push(S, x):将元素 x 压入栈 Spop(S):弹出栈顶元素multipop(S, k):弹出栈顶 k 个元素(或弹出栈中所有元素,如果栈中元素少于 k 个)最坏情况分析:
push和pop操作的代价为 O (1)multipop操作的代价为 O (n),其中 n 是栈的大小聚合分析: 考虑一个包含 n 个操作的序列,每个元素最多被压入栈一次,弹出一次。因此,n 个操作的总代价为 O (n),每个操作的摊还代价为 O (1)。
下面是栈操作的 C++ 实现,包含了基本操作和代价统计:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 实现一个支持push、pop和multipop操作的栈
template <typename T>
class Stack {
private:
vector<T> elements; // 存储栈元素
int total_cost; // 记录总操作代价
public:
// 构造函数
Stack() : total_cost(0) {}
// 入栈操作
void push(const T& element) {
elements.push_back(element);
total_cost += 1; // push操作代价为1
}
// 出栈操作
T pop() {
if (elements.empty()) {
throw runtime_error("Stack is empty");
}
T top = elements.back();
elements.pop_back();
total_cost += 1; // pop操作代价为1
return top;
}
// 批量出栈操作
void multipop(int k) {
int pop_count = min(k, (int)elements.size());
for (int i = 0; i < pop_count; ++i) {
elements.pop_back();
}
total_cost += pop_count; // multipop代价为弹出的元素数量
}
// 判断栈是否为空
bool isEmpty() const {
return elements.empty();
}
// 获取栈大小
int size() const {
return elements.size();
}
// 获取总操作代价
int getTotalCost() const {
return total_cost;
}
// 重置代价计数器
void resetCost() {
total_cost = 0;
}
};
int main() {
Stack<int> s;
int num_operations = 0;
// 执行一系列栈操作
try {
cout << "执行栈操作序列:" << endl;
cout << "push(1)";
s.push(1);
num_operations++;
cout << "\npush(2)";
s.push(2);
num_operations++;
cout << "\npush(3)";
s.push(3);
num_operations++;
cout << "\npop() -> 返回值: " << s.pop();
num_operations++;
cout << "\npush(4)";
s.push(4);
num_operations++;
cout << "\nmultipop(2)";
s.multipop(2);
num_operations++;
cout << "\n当前栈大小: " << s.size() << endl;
cout << "总操作次数: " << num_operations << endl;
cout << "总操作代价: " << s.getTotalCost() << endl;
cout << "每个操作的摊还代价: " << (double)s.getTotalCost() / num_operations << endl;
} catch (const exception& e) {
cout << "\n错误: " << e.what() << endl;
}
return 0;
}
上述代码实现了一个支持push、pop和multipop操作的栈,并统计了操作的总代价。通过聚合分析,我们可以看到尽管multipop操作在最坏情况下代价很高,但一系列操作的平均代价仍然是 O (1)。
这表明在这个操作序列中,每个操作的平均代价为 1,验证了我们的聚合分析结论。
核算法(Accounting Method)为不同的操作分配不同的摊还代价,某些操作的摊还代价可能高于其实际代价,而其他操作的摊还代价可能低于其实际代价。"核" 表示存储的信用,可以用来支付未来操作的代价。
核算法的关键思想:

考虑一个二进制计数器递增的操作increment。我们可以使用核算法分析这个操作的摊还代价。
实际代价:在increment操作中,翻转的位数就是实际代价。例如,将 1011 变为 1100 需要翻转 3 位(最后三位),实际代价为 3。
摊还代价分配:
increment操作分配 2 的摊还代价 通过这种分配方式,总信用始终非负,且每个increment操作的摊还代价为 O (1)。
下面是二进制计数器的实现,使用核算法进行代价分析:
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
using namespace std;
// 二进制计数器类,使用核算法分析increment操作
class BinaryCounter {
private:
vector<bool> bits; // 存储二进制位,bits[0]是最低位
int total_actual_cost; // 总实际代价
int total_amortized_cost; // 总摊还代价
int credit; // 信用值
// 确保有足够的位数,必要时扩展
void ensureCapacity() {
bits.push_back(false); // 扩展一位,初始为0
}
public:
// 构造函数,初始值为0
BinaryCounter() : total_actual_cost(0), total_amortized_cost(0), credit(0) {
bits.push_back(false); // 初始有一位,值为0
}
// 递增操作
void increment() {
int i = 0;
int actual_cost = 0;
// 翻转所有连续的1
while (i < bits.size() && bits[i]) {
bits[i] = false; // 1翻转为0
i++;
actual_cost++;
credit--; // 使用1个信用支付翻转代价
}
// 如果还有位,翻转最左边的0
if (i < bits.size()) {
bits[i] = true; // 0翻转为1
actual_cost++;
credit++; // 存储1个信用,用于未来翻转
} else {
// 如果所有位都是1,需要扩展
ensureCapacity();
bits[i] = true; // 新位翻转为1
actual_cost++;
credit++; // 存储1个信用,用于未来翻转
}
// 核算法中,为increment操作分配2的摊还代价
int amortized_cost = 2;
credit += (amortized_cost - actual_cost);
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
// 检查信用是否非负(核算法的要求)
if (credit < 0) {
cerr << "警告:信用变为负数,摊还代价分配不合理!" << endl;
}
}
// 获取当前计数值
int getValue() const {
int value = 0;
int power = 1;
for (bool bit : bits) {
if (bit) {
value += power;
}
power *= 2;
}
return value;
}
// 打印二进制表示
void printBinary() const {
cout << "二进制表示: ";
for (auto it = bits.rbegin(); it != bits.rend(); ++it) {
cout << (*it ? "1" : "0");
}
cout << endl;
}
// 获取二进制表示的字符串
string getBinaryString() const {
string binary;
for (auto it = bits.rbegin(); it != bits.rend(); ++it) {
binary += (*it ? "1" : "0");
}
return binary;
}
// 获取总实际代价
int getTotalActualCost() const {
return total_actual_cost;
}
// 获取总摊还代价
int getTotalAmortizedCost() const {
return total_amortized_cost;
}
// 获取当前信用值
int getCredit() const {
return credit;
}
};
int main() {
BinaryCounter counter;
// 执行10次递增操作
int num_operations = 10;
cout << "执行" << num_operations << "次increment操作:" << endl;
cout << left << setw(10) << "操作次数" << setw(15) << "计数值" << setw(20) << "二进制表示"
<< setw(15) << "当前信用" << setw(15) << "累计实际代价" << "累计摊还代价" << endl;
cout << string(80, '-') << endl;
for (int i = 0; i < num_operations; ++i) {
counter.increment();
cout << left << setw(10) << (i + 1)
<< setw(15) << counter.getValue()
<< setw(20) << counter.getBinaryString()
<< setw(15) << counter.getCredit()
<< setw(15) << counter.getTotalActualCost()
<< counter.getTotalAmortizedCost() << endl;
}
cout << string(80, '-') << endl;
cout << "平均实际代价: " << (double)counter.getTotalActualCost() / num_operations << endl;
cout << "平均摊还代价: " << (double)counter.getTotalAmortizedCost() / num_operations << endl;
return 0;
}
上述代码实现了一个二进制计数器,并使用核算法分析了increment操作的代价。我们为每个increment操作分配了 2 的摊还代价,通过信用机制平衡了不同操作的实际代价。
运行程序后,你会看到每次递增操作后的计数值、二进制表示、当前信用以及累计代价。从输出结果可以看出,尽管某些increment操作的实际代价较高(如从 3 到 4 需要翻转 3 位),但通过核算法分配的摊还代价始终为 2,且信用始终保持非负。

势能法(Potential Method)是另一种常用的摊还分析方法。它通过定义一个势能函数(potential function)将数据结构的状态映射到一个非负的势能值。摊还代价由实际代价加上势能的变化组成。
势能法的核心概念:
势能法的优势在于它不需要跟踪每个对象的信用,而是通过整个数据结构的势能变化来计算摊还代价。
我们再次分析栈的push、pop和multipop操作,但这次使用势能法:
定义势能函数:令 Φ(S) 为栈 S 中元素的数量,即栈的大小。
摊还代价计算:
push操作:实际代价 c=1,势能变化为 Φ(S') - Φ(S) = 1,所以摊还代价\(\hat{c}\)=1+1=2pop操作:实际代价 c=1,势能变化为 Φ(S') - Φ(S) = -1,所以摊还代价\(\hat{c}\)=1+(-1)=0multipop(k)操作:假设弹出 k' 个元素,实际代价 c=k',势能变化为 Φ(S') - Φ(S) = -k',所以摊还代价\(\hat{c}\)=k' + (-k')=0通过这种分析,每个操作的摊还代价都是非负的,且 n 个操作的总摊还代价是 O (n)。
下面是使用势能法分析栈操作的实现:
#include <iostream>
#include <vector>
#include <string>
#include <iomanip>
using namespace std;
// 使用势能法分析的栈类
template <typename T>
class PotentialStack {
private:
vector<T> elements; // 存储栈元素
int total_actual_cost; // 总实际代价
int total_amortized_cost; // 总摊还代价
// 势能函数:栈中元素的数量
int potential() const {
return elements.size();
}
public:
// 构造函数
PotentialStack() : total_actual_cost(0), total_amortized_cost(0) {}
// 入栈操作
void push(const T& element) {
int old_potential = potential(); // 操作前的势能
elements.push_back(element);
int actual_cost = 1; // push操作的实际代价
int new_potential = potential(); // 操作后的势能
int amortized_cost = actual_cost + (new_potential - old_potential); // 计算摊还代价
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
}
// 出栈操作
T pop() {
if (elements.empty()) {
throw runtime_error("Stack is empty");
}
int old_potential = potential(); // 操作前的势能
T top = elements.back();
elements.pop_back();
int actual_cost = 1; // pop操作的实际代价
int new_potential = potential(); // 操作后的势能
int amortized_cost = actual_cost + (new_potential - old_potential); // 计算摊还代价
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
return top;
}
// 批量出栈操作
void multipop(int k) {
if (elements.empty()) {
return;
}
int old_potential = potential(); // 操作前的势能
int pop_count = min(k, (int)elements.size());
for (int i = 0; i < pop_count; ++i) {
elements.pop_back();
}
int actual_cost = pop_count; // multipop操作的实际代价
int new_potential = potential(); // 操作后的势能
int amortized_cost = actual_cost + (new_potential - old_potential); // 计算摊还代价
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
}
// 判断栈是否为空
bool isEmpty() const {
return elements.empty();
}
// 获取栈大小
int size() const {
return elements.size();
}
// 获取总实际代价
int getTotalActualCost() const {
return total_actual_cost;
}
// 获取总摊还代价
int getTotalAmortizedCost() const {
return total_amortized_cost;
}
// 重置代价计数器
void resetCost() {
total_actual_cost = 0;
total_amortized_cost = 0;
}
};
// 记录操作的结构体
struct Operation {
string type; // 操作类型:push, pop, multipop
int parameter; // 参数:push的值,multipop的数量
int actual_cost; // 实际代价
int amortized_cost; // 摊还代价
int potential; // 操作后的势能
};
int main() {
PotentialStack<int> s;
vector<Operation> operations;
int prev_total_actual = 0;
int prev_total_amortized = 0;
// 执行一系列栈操作
try {
// push(1)
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.push(1);
operations.push_back({
"push", 1,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// push(2)
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.push(2);
operations.push_back({
"push", 2,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// push(3)
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.push(3);
operations.push_back({
"push", 3,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// pop()
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.pop();
operations.push_back({
"pop", 0,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// push(4)
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.push(4);
operations.push_back({
"push", 4,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// multipop(2)
prev_total_actual = s.getTotalActualCost();
prev_total_amortized = s.getTotalAmortizedCost();
s.multipop(2);
operations.push_back({
"multipop", 2,
s.getTotalActualCost() - prev_total_actual,
s.getTotalAmortizedCost() - prev_total_amortized,
s.size()
});
// 输出操作结果
cout << "栈操作序列及势能法分析:" << endl;
cout << left << setw(15) << "操作" << setw(15) << "实际代价"
<< setw(15) << "摊还代价" << "操作后势能" << endl;
cout << string(60, '-') << endl;
for (const auto& op : operations) {
string op_desc = op.type;
if (op.type == "push") op_desc += "(" + to_string(op.parameter) + ")";
else if (op.type == "multipop") op_desc += "(" + to_string(op.parameter) + ")";
cout << left << setw(15) << op_desc
<< setw(15) << op.actual_cost
<< setw(15) << op.amortized_cost
<< op.potential << endl;
}
cout << string(60, '-') << endl;
cout << "总操作次数: " << operations.size() << endl;
cout << "总实际代价: " << s.getTotalActualCost() << endl;
cout << "总摊还代价: " << s.getTotalAmortizedCost() << endl;
cout << "平均实际代价: " << (double)s.getTotalActualCost() / operations.size() << endl;
cout << "平均摊还代价: " << (double)s.getTotalAmortizedCost() / operations.size() << endl;
} catch (const exception& e) {
cout << "错误: " << e.what() << endl;
}
return 0;
}
上述代码实现了一个栈,并使用势能法分析了各个操作的摊还代价。我们选择栈的大小作为势能函数,通过计算每次操作前后的势能变化来得到摊还代价。
运行程序后,你可以清楚地看到每个操作的实际代价、摊还代价以及操作后的势能值。从结果中可以验证:
push操作的摊还代价为 2pop操作的摊还代价为 0multipop操作的摊还代价为 0 尽管某些操作的实际代价可能较高(如multipop),但通过势能法计算的摊还代价平衡了这些差异,使得一系列操作的平均代价保持在较低水平。
动态表是摊还分析的一个重要应用场景。动态表能够根据需要自动扩张或收缩,以适应元素数量的变化。在这一节中,我们将分析动态表的扩张和收缩操作的摊还代价。
方法概述
当表已满时,插入新元素需要进行表扩张:
如果简单地进行最坏情况分析,插入操作的代价可能是 O (n)(当需要扩张时)。但使用摊还分析,我们可以证明插入操作的摊还代价是 O (1)。
使用势能法分析表扩张:
代码实现
#include <iostream>
#include <cstdlib>
#include <iomanip>
using namespace std;
// 支持自动扩张的动态表
template <typename T>
class DynamicTable {
private:
T* table; // 存储元素的数组
int num; // 当前元素数量
int size; // 表的当前容量
int total_actual_cost; // 总实际代价
int total_amortized_cost; // 总摊还代价
// 计算势能函数:Φ(T) = 2·num - size
int potential() const {
return 2 * num - size;
}
// 扩张表:将容量翻倍
void expand() {
int old_size = size;
size = (size == 0) ? 1 : size * 2; // 如果是空表,初始容量为1
// 分配新表
T* new_table = new T[size];
// 复制元素
for (int i = 0; i < num; ++i) {
new_table[i] = table[i];
}
// 释放旧表
if (old_size > 0) {
delete[] table;
}
table = new_table;
}
public:
// 构造函数
DynamicTable() : table(nullptr), num(0), size(0),
total_actual_cost(0), total_amortized_cost(0) {}
// 析构函数
~DynamicTable() {
if (table != nullptr) {
delete[] table;
}
}
// 插入元素
void insert(const T& element) {
int old_potential = potential(); // 操作前的势能
int actual_cost = 1; // 插入操作的基本代价
// 如果表已满,需要扩张
if (num == size) {
actual_cost += num; // 加上复制所有元素的代价
expand();
}
// 插入新元素
table[num++] = element;
// 计算摊还代价
int new_potential = potential();
int amortized_cost = actual_cost + (new_potential - old_potential);
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
}
// 获取元素数量
int getNum() const {
return num;
}
// 获取表容量
int getSize() const {
return size;
}
// 获取总实际代价
int getTotalActualCost() const {
return total_actual_cost;
}
// 获取总摊还代价
int getTotalAmortizedCost() const {
return total_amortized_cost;
}
// 获取指定索引的元素
T getElement(int index) const {
if (index < 0 || index >= num) {
throw out_of_range("Index out of range");
}
return table[index];
}
};
int main() {
DynamicTable<int> dt;
// 执行20次插入操作
int num_insertions = 20;
cout << "执行" << num_insertions << "次插入操作:" << endl;
cout << left << setw(10) << "插入次数" << setw(10) << "元素数"
<< setw(10) << "表容量" << setw(15) << "实际代价"
<< setw(15) << "摊还代价" << "势能" << endl;
cout << string(70, '-') << endl;
int prev_actual = 0;
int prev_amortized = 0;
for (int i = 0; i < num_insertions; ++i) {
prev_actual = dt.getTotalActualCost();
prev_amortized = dt.getTotalAmortizedCost();
dt.insert(i + 1); // 插入元素
int actual_cost = dt.getTotalActualCost() - prev_actual;
int amortized_cost = dt.getTotalAmortizedCost() - prev_amortized;
cout << left << setw(10) << (i + 1)
<< setw(10) << dt.getNum()
<< setw(10) << dt.getSize()
<< setw(15) << actual_cost
<< setw(15) << amortized_cost
<< (2 * dt.getNum() - dt.getSize()) << endl;
}
cout << string(70, '-') << endl;
cout << "总实际代价: " << dt.getTotalActualCost() << endl;
cout << "总摊还代价: " << dt.getTotalAmortizedCost() << endl;
cout << "平均实际代价: " << (double)dt.getTotalActualCost() / num_insertions << endl;
cout << "平均摊还代价: " << (double)dt.getTotalAmortizedCost() / num_insertions << endl;
return 0;
}
代码说明
上述代码实现了一个支持自动扩张的动态表。当表已满时,插入新元素会触发表扩张,将表的容量翻倍,并将所有元素复制到新表中。
通过势能法分析,我们证明了每次插入操作的摊还代价为 O (1)。运行程序后,你可以观察到:
这验证了我们的分析结论:动态表插入操作的摊还代价为 O (1)。
方法概述
除了扩张,动态表通常还需要支持收缩操作,当表中元素过少时减小表的容量,以节省空间。
为了避免 "震荡" 现象(频繁交替进行扩张和收缩),通常采用以下策略:
使用势能法分析这种策略,可以证明插入和删除操作的摊还代价都是 O (1)。
代码实现
下面是同时支持扩张和收缩的动态表的实现:
#include <iostream>
#include <cstdlib>
#include <iomanip>
#include <stdexcept>
using namespace std;
// 同时支持扩张和收缩的动态表
template <typename T>
class DynamicTable {
private:
T* table; // 存储元素的数组
int num; // 当前元素数量
int size; // 表的当前容量
int total_actual_cost; // 总实际代价
int total_amortized_cost; // 总摊还代价
// 计算势能函数
// 当num > size/2时:Φ(T) = 2·num - size
// 当num ≤ size/2时:Φ(T) = size/2 - num
int potential() const {
if (size == 0) return 0;
if (num > size / 2) {
return 2 * num - size;
} else {
return size / 2 - num;
}
}
// 改变表的大小
void resize(int new_size) {
if (new_size < 1) new_size = 1; // 确保最小容量为1
// 分配新表
T* new_table = new T[new_size];
// 复制元素
int copy_count = min(num, new_size);
for (int i = 0; i < copy_count; ++i) {
new_table[i] = table[i];
}
// 释放旧表
if (size > 0) {
delete[] table;
}
table = new_table;
size = new_size;
}
// 扩张表:将容量翻倍
void expand() {
resize(size * 2);
}
// 收缩表:将容量减半
void shrink() {
resize(size / 2);
}
public:
// 构造函数
DynamicTable() : table(nullptr), num(0), size(0),
total_actual_cost(0), total_amortized_cost(0) {}
// 析构函数
~DynamicTable() {
if (table != nullptr) {
delete[] table;
}
}
// 插入元素
void insert(const T& element) {
int old_potential = potential(); // 操作前的势能
int actual_cost = 1; // 插入操作的基本代价
// 如果表已满,需要扩张
if (num == size) {
actual_cost += num; // 加上复制所有元素的代价
expand();
}
// 插入新元素
table[num++] = element;
// 计算摊还代价
int new_potential = potential();
int amortized_cost = actual_cost + (new_potential - old_potential);
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
}
// 删除最后一个元素
T remove() {
if (num == 0) {
throw runtime_error("Table is empty");
}
int old_potential = potential(); // 操作前的势能
int actual_cost = 1; // 删除操作的基本代价
// 获取并删除最后一个元素
T removed = table[--num];
// 如果元素数量少于容量的1/4,并且容量大于1,则收缩
if (num > 0 && num <= size / 4) {
actual_cost += num; // 加上复制所有元素的代价
shrink();
}
// 计算摊还代价
int new_potential = potential();
int amortized_cost = actual_cost + (new_potential - old_potential);
// 更新总代价
total_actual_cost += actual_cost;
total_amortized_cost += amortized_cost;
return removed;
}
// 获取元素数量
int getNum() const {
return num;
}
// 获取表容量
int getSize() const {
return size;
}
// 获取总实际代价
int getTotalActualCost() const {
return total_actual_cost;
}
// 获取总摊还代价
int getTotalAmortizedCost() const {
return total_amortized_cost;
}
// 获取指定索引的元素
T getElement(int index) const {
if (index < 0 || index >= num) {
throw out_of_range("Index out of range");
}
return table[index];
}
};
int main() {
DynamicTable<int> dt;
cout << "执行一系列插入和删除操作:" << endl;
cout << left << setw(10) << "操作" << setw(10) << "元素数"
<< setw(10) << "表容量" << setw(15) << "实际代价"
<< setw(15) << "摊还代价" << "势能" << endl;
cout << string(70, '-') << endl;
int prev_actual = 0;
int prev_amortized = 0;
// 插入10个元素
for (int i = 0; i < 10; ++i) {
prev_actual = dt.getTotalActualCost();
prev_amortized = dt.getTotalAmortizedCost();
dt.insert(i + 1);
int actual_cost = dt.getTotalActualCost() - prev_actual;
int amortized_cost = dt.getTotalAmortizedCost() - prev_amortized;
cout << left << setw(10) << "insert(" + to_string(i+1) + ")"
<< setw(10) << dt.getNum()
<< setw(10) << dt.getSize()
<< setw(15) << actual_cost
<< setw(15) << amortized_cost
<< dt.potential() << endl;
}
// 删除6个元素
for (int i = 0; i < 6; ++i) {
prev_actual = dt.getTotalActualCost();
prev_amortized = dt.getTotalAmortizedCost();
int removed = dt.remove();
int actual_cost = dt.getTotalActualCost() - prev_actual;
int amortized_cost = dt.getTotalAmortizedCost() - prev_amortized;
cout << left << setw(10) << "remove()"
<< setw(10) << dt.getNum()
<< setw(10) << dt.getSize()
<< setw(15) << actual_cost
<< setw(15) << amortized_cost
<< dt.potential() << endl;
}
// 再插入4个元素
for (int i = 0; i < 4; ++i) {
prev_actual = dt.getTotalActualCost();
prev_amortized = dt.getTotalAmortizedCost();
dt.insert(11 + i);
int actual_cost = dt.getTotalActualCost() - prev_actual;
int amortized_cost = dt.getTotalAmortizedCost() - prev_amortized;
cout << left << setw(10) << "insert(" + to_string(11+i) + ")"
<< setw(10) << dt.getNum()
<< setw(10) << dt.getSize()
<< setw(15) << actual_cost
<< setw(15) << amortized_cost
<< dt.potential() << endl;
}
cout << string(70, '-') << endl;
cout << "总操作次数: 20" << endl;
cout << "总实际代价: " << dt.getTotalActualCost() << endl;
cout << "总摊还代价: " << dt.getTotalAmortizedCost() << endl;
cout << "平均实际代价: " << (double)dt.getTotalActualCost() / 20 << endl;
cout << "平均摊还代价: " << (double)dt.getTotalAmortizedCost() / 20 << endl;
return 0;
}
代码说明
上述代码实现了一个同时支持自动扩张和收缩的动态表。当表满时,插入操作会触发表扩张;当表中元素数量少于容量的四分之一时,删除操作会触发表收缩。
通过精心设计的势能函数和操作策略,我们避免了 "震荡" 现象,并保证了插入和删除操作的摊还代价都是 O (1)。运行程序后,你可以观察到:
这验证了我们的分析结论:支持扩张和收缩的动态表,其插入和删除操作的摊还代价都是 O (1)。

increment操作,证明 n 次操作的总代价是 O (n)。
push和pop操作外,还有一个copy操作,它将当前栈复制一份并返回新栈。copy操作的实际代价是栈的大小 k。使用势能法分析这三种操作的摊还代价。
insert、delete和search操作的动态集合数据结构,并使用摊还分析证明其每个操作的摊还代价是 O (log n)。

摊还分析是算法分析中的一种重要技术,特别适用于分析那些偶尔执行昂贵操作但通常操作高效的数据结构。本章介绍的三种摊还分析方法各有特点:

动态表是摊还分析的经典应用,通过合理设计扩张和收缩策略,可以保证插入和删除操作的摊还代价为 O (1)。这种分析方法也适用于其他数据结构,如二叉搜索树、堆、哈希表等。
在实际应用中,选择哪种摊还分析方法取决于具体问题。聚合分析法最直观,核算法适合跟踪单个对象的信用,势能法则更适合分析整个数据结构的状态变化。
摊还分析的思想也可以应用于算法设计,通过巧妙安排操作顺序,将昂贵操作的代价分摊到多个廉价操作上,从而提高整体性能。

本章详细介绍了摊还分析的三种方法及其在动态表上的应用。通过学习摊还分析,我们能够更准确地评估那些具有间歇性昂贵操作的数据结构的性能。
理解摊还分析不仅有助于我们分析现有数据结构,也能指导我们设计更高效的算法和数据结构。在实际编程中,这种思想可以帮助我们避免过早优化,同时确保整体性能的稳定性。
希望本文能帮助你深入理解摊还分析的原理和应用,为你的算法学习和实践打下坚实基础。如果你有任何问题或建议,欢迎在评论区留言讨论。