当前位置: 首页 > web >正文

《算法导论》第 17 章 - 摊还分析

目录

 

引言

思维导图

17.1 聚合分析

方法概述

案例分析:栈操作

代码实现

代码说明

 

17.2 核算法

方法概述

案例分析:二进制计数器

代码实现

代码说明

 

17.3 势能法

方法概述

案例分析:栈操作的势能法分析

代码实现

代码说明

 

17.4 动态表

17.4.1 表扩张

方法概述

代码实现

代码说明

17.4.2 表扩张和收缩

方法概述

代码实现

代码说明

 

思考题

 

本章注记

 

总结


 

 

引言

        摊还分析是算法分析中的一种重要技术,用于分析一个操作序列的平均代价。与最坏情况分析不同,摊还分析关注的是一系列操作的整体性能,而不是单个操作的最坏情况。这种分析方法对于评估那些偶尔会执行昂贵操作,但大多数时候操作都很高效的数据结构特别有用。

        本文将详细讲解《算法导论》第 17 章介绍的三种摊还分析方法:聚合分析、核算法和势能法,并通过动态表的例子展示这些方法的应用。

思维导图

 

 

17.1 聚合分析

 

方法概述

        聚合分析(Aggregate Analysis)是最简单的摊还分析方法。它的基本思想是:对所有操作序列,计算其总代价的上界,然后将这个总代价除以操作数,得到每个操作的平均代价,这个平均代价就是每个操作的摊还代价。

聚合分析的步骤:

  1. 确定所有可能的操作
  2. 找到一个操作序列的总代价的上界
  3. 每个操作的摊还代价为总代价除以操作数

案例分析:栈操作

考虑栈的三种操作:

  • push(S, x):将元素 x 压入栈 S
  • pop(S):弹出栈顶元素
  • multipop(S, k):弹出栈顶 k 个元素(或弹出栈中所有元素,如果栈中元素少于 k 个)

 

最坏情况分析

  • pushpop操作的代价为 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操作代价为1return 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;
}

 

代码说明

        上述代码实现了一个支持pushpopmultipop操作的栈,并统计了操作的总代价。通过聚合分析,我们可以看到尽管multipop操作在最坏情况下代价很高,但一系列操作的平均代价仍然是 O (1)。

 

运行程序后,你会看到类似以下的输出:

执行栈操作序列:
push(1)
push(2)
push(3)
pop() -> 返回值: 3
push(4)
multipop(2)
当前栈大小: 1
总操作次数: 6
总操作代价: 6
每个操作的摊还代价: 1

 

        这表明在这个操作序列中,每个操作的平均代价为 1,验证了我们的聚合分析结论。

 

17.2 核算法

 

方法概述

        核算法(Accounting Method)为不同的操作分配不同的摊还代价,某些操作的摊还代价可能高于其实际代价,而其他操作的摊还代价可能低于其实际代价。"核" 表示存储的信用,可以用来支付未来操作的代价。

 

核算法的关键思想:

  1. 为每种操作分配一个摊还代价
  2. 当摊还代价高于实际代价时,将差额存储为信用
  3. 当摊还代价低于实际代价时,使用之前存储的信用来弥补差额
  4. 确保总信用始终非负

 

案例分析:二进制计数器

        考虑一个二进制计数器递增的操作increment。我们可以使用核算法分析这个操作的摊还代价。

        实际代价:在increment操作中,翻转的位数就是实际代价。例如,将 1011 变为 1100 需要翻转 3 位(最后三位),实际代价为 3。

 

摊还代价分配

  • increment操作分配 2 的摊还代价
  • 当某个位从 0 翻转为 1 时,存储 1 个信用(用于未来将其翻转为 0)
  • 当某个位从 1 翻转为 0 时,使用之前存储的 1 个信用支付翻转代价

 

        通过这种分配方式,总信用始终非负,且每个increment操作的摊还代价为 O (1)。

代码实现

下面是二进制计数器的实现,使用核算法进行代价分析:

#include <iostream>
#include <vector>
#include <iomanip>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:// 构造函数,初始值为0BinaryCounter() : total_actual_cost(0), total_amortized_cost(0), credit(0) {bits.push_back(false);  // 初始有一位,值为0}// 递增操作void increment() {int i = 0;int actual_cost = 0;// 翻转所有连续的1while (i < bits.size() && bits[i]) {bits[i] = false;  // 1翻转为0i++;actual_cost++;credit--;  // 使用1个信用支付翻转代价}// 如果还有位,翻转最左边的0if (i < bits.size()) {bits[i] = true;  // 0翻转为1actual_cost++;credit++;  // 存储1个信用,用于未来翻转} else {// 如果所有位都是1,需要扩展ensureCapacity();bits[i] = true;  // 新位翻转为1actual_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;}// 获取总实际代价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();// 临时打印二进制表示cout << setw(20);for (auto it = counter.bits.rbegin(); it != counter.bits.rend(); ++it) {cout << (*it ? "1" : "0");}cout << 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,且信用始终保持非负。

 

17.3 势能法

 

方法概述

        势能法(Potential Method)是另一种常用的摊还分析方法。它通过定义一个势能函数(potential function)将数据结构的状态映射到一个非负的势能值。摊还代价由实际代价加上势能的变化组成。

 

势能法的核心概念:

  1. 定义一个势能函数 Φ,将数据结构的状态映射到非负实数
  2. 操作的摊还代价为:\(\hat{c}_i = c_i + Φ(D_i) - Φ(D_{i-1})\)
  3. 总摊还代价是总实际代价加上最终势能与初始势能之差
  4. 确保势能函数始终非负,且初始势能 Φ(D₀) ≥ 0

 

        势能法的优势在于它不需要跟踪每个对象的信用,而是通过整个数据结构的势能变化来计算摊还代价。

案例分析:栈操作的势能法分析

我们再次分析栈的pushpopmultipop操作,但这次使用势能法:

 

定义势能函数:令 Φ(S) 为栈 S 中元素的数量,即栈的大小。

摊还代价计算

  • push操作:实际代价 c=1,势能变化为 Φ(S') - Φ(S) = 1,所以摊还代价\(\hat{c}\)=1+1=2
  • pop操作:实际代价 c=1,势能变化为 Φ(S') - Φ(S) = -1,所以摊还代价\(\hat{c}\)=1+(-1)=0
  • multipop(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, multipopint 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操作的摊还代价为 2
  • pop操作的摊还代价为 0
  • multipop操作的摊还代价为 0

 

        尽管某些操作的实际代价可能较高(如multipop),但通过势能法计算的摊还代价平衡了这些差异,使得一系列操作的平均代价保持在较低水平。

 

17.4 动态表

 

        动态表是摊还分析的一个重要应用场景。动态表能够根据需要自动扩张或收缩,以适应元素数量的变化。在这一节中,我们将分析动态表的扩张和收缩操作的摊还代价。

17.4.1 表扩张

方法概述

当表已满时,插入新元素需要进行表扩张:

  1. 分配一个更大的表(通常是原大小的两倍)
  2. 将原表中的所有元素复制到新表中
  3. 释放原表的空间
  4. 插入新元素

 

        如果简单地进行最坏情况分析,插入操作的代价可能是 O (n)(当需要扩张时)。但使用摊还分析,我们可以证明插入操作的摊还代价是 O (1)。

 

使用势能法分析表扩张:

  • 定义势能函数:Φ(T) = 2・num [T] - size [T],其中 num [T] 是表中元素数量,size [T] 是表的大小
  • 当表未满时插入元素,势能增加 1,摊还代价为 2
  • 当表满时插入元素(需要扩张),势能变化为 -(size [T]-1),摊还代价为 2

代码实现

#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 - sizeint 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)。运行程序后,你可以观察到:

  • 大多数插入操作的实际代价为 1
  • 当表满时插入元素,实际代价会突然增加(需要复制所有元素)
  • 但每次插入操作的摊还代价始终为 2
  • 总摊还代价约为总插入次数的 2 倍

 

        这验证了我们的分析结论:动态表插入操作的摊还代价为 O (1)。

17.4.2 表扩张和收缩

方法概述

除了扩张,动态表通常还需要支持收缩操作,当表中元素过少时减小表的容量,以节省空间。

为了避免 "震荡" 现象(频繁交替进行扩张和收缩),通常采用以下策略:

  • 当表满时(num [T] = size [T]),将表扩张到原来的 2 倍
  • 当表中元素不足四分之一时(num [T] = size [T]/4),将表收缩到原来的一半

使用势能法分析这种策略,可以证明插入和删除操作的摊还代价都是 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 - numint 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)。

 

思考题

  1. 用聚合分析法分析二进制计数器的increment操作,证明 n 次操作的总代价是 O (n)。

  2. 设计一个动态表,使得插入和删除操作的摊还代价都为 O (1),但使用不同的扩张和收缩策略(例如,当表满时扩张到原来的 3 倍,当元素数量少于容量的 1/3 时收缩到原来的 1/2)。

  3. 考虑一个栈,除了pushpop操作外,还有一个copy操作,它将当前栈复制一份并返回新栈。copy操作的实际代价是栈的大小 k。使用势能法分析这三种操作的摊还代价。

  4. 证明:如果一个数据结构的操作序列的总摊还代价是 T (n),那么存在一个操作序列使得其总实际代价至少是 T (n) - Φ(Dn) + Φ(D0)。

  5. 设计一个支持insertdeletesearch操作的动态集合数据结构,并使用摊还分析证明其每个操作的摊还代价是 O (log n)。

 

本章注记

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

  • 聚合分析法最简单,直接计算 n 个操作的总代价,然后求平均值。
  • 核算法为不同操作分配不同的摊还代价,通过信用机制平衡代价差异。
  • 势能法通过定义势能函数,将数据结构的状态映射到一个势能值,通过势能变化来计算摊还代价。

 

        动态表是摊还分析的经典应用,通过合理设计扩张和收缩策略,可以保证插入和删除操作的摊还代价为 O (1)。这种分析方法也适用于其他数据结构,如二叉搜索树、堆、哈希表等。

        在实际应用中,选择哪种摊还分析方法取决于具体问题。聚合分析法最直观,核算法适合跟踪单个对象的信用,势能法则更适合分析整个数据结构的状态变化。

        摊还分析的思想也可以应用于算法设计,通过巧妙安排操作顺序,将昂贵操作的代价分摊到多个廉价操作上,从而提高整体性能。

 

总结

        本章详细介绍了摊还分析的三种方法及其在动态表上的应用。通过学习摊还分析,我们能够更准确地评估那些具有间歇性昂贵操作的数据结构的性能。

        理解摊还分析不仅有助于我们分析现有数据结构,也能指导我们设计更高效的算法和数据结构。在实际编程中,这种思想可以帮助我们避免过早优化,同时确保整体性能的稳定性。

        希望本文能帮助你深入理解摊还分析的原理和应用,为你的算法学习和实践打下坚实基础。如果你有任何问题或建议,欢迎在评论区留言讨论。

 

http://www.xdnf.cn/news/17615.html

相关文章:

  • 【Docker进阶实战】从多容器编排到集群部署
  • 谷歌DeepMind发布Genie 3:通用型世界模型,可生成前所未有多样化的交互式虚拟环境
  • 【PyTorch】单目标检测项目部署
  • BGP知识点总结
  • MACBOOK M1安装达梦8数据库
  • 机器学习实战·第三章 分类(1)
  • 组合期权:对角价差
  • Python描述符进阶:自定义文档与属性删除的艺术
  • 2025年全国青少年信息素养大赛Scratch编程践挑战赛-小高组-初赛-模拟题
  • P3232 [HNOI2013] 游走,solution
  • redis 全局命令、数据结构和内部编码、单线程架构
  • 深入理解C语言一维数组的本质:数组名、指针常量与访问细节
  • 250810-OpenWebUI集成Dify应用
  • uboot使用指南
  • 分布微服务电商订单系统Rust编码开发[下]
  • MySQL的逻辑架构和SQL执行的流程:
  • Stream流应用
  • MPLS特性之PHP(Penultimate Hop Popping)
  • afsim2.9_使用QtCreator和VSCode编译
  • 【杂谈】-智能代理+可观察性:构建下一代复杂系统监控体系
  • 《解锁 C++ 起源与核心:命名空间用法 + 版本演进全知道》
  • AUTOSAR进阶图解==>AUTOSAR_ASWS_TransformerGeneral
  • 关于linux操作系统下的文件操作方法:
  • ThinkPHP8学习篇(二):路由
  • 20250810 | 深度学习入门笔记1
  • 从色彩心理学看嵌入式设备UI设计:原则、挑战与实践
  • C语言-动态内存分配函数、变量属性(全局、局部、静态、只读)、C语言内存结构;
  • go加速配置(下载第三方库)
  • [0CTF 2016]piapiapia
  • 【秋招笔试】2025.08.09美团秋招研发岗机考真题-第二题