1、一些支持优先队列操作的 数据结构,如第6章的二叉堆、第13章的红黑树 和 第19章的斐波那契堆。在这几种数据结构中, 不论是最好情况 还是 摊还情况, 至少有一项重要操作 只需要 O(n lgn) 时间
由于这些数据结构 都是基于关键字比较 决定的,因此, 8.1节中的下界 Ω(n lgn) 说明 至少有一个操作必须 Ω(lgn) 的时间
因为如果 Insert 和 Extract-Min 操作都需要 o(lg n) 时间, 那么 可以通过先执行 n 次 Insert 操作, 接着再执行n次 Extract-Min 操作 来实现 o(n lgn) 时间内 对n个关键字的排序
2、优先队列
每个元素都有一个与之相关的优先级。在优先队列中,高优先级的元素在低优先级的元素之前被处理
插入元素:将一个新元素插入到优先队列中。
删除最小元素:从优先队列中删除并返回优先级最高(通常是最小)的元素。
查找最小元素:返回优先级最高(通常是最小)的元素,但不删除它
几种实现优先队列的常见方法:
- 使用数组实现
插入操作:O(1)
删除最小元素操作:O(n)
查找最小元素操作:O(n) - 使用有序数组实现
插入操作:O(n)
删除最小元素操作:O(1)
查找最小元素操作:O(1) - 使用堆实现(例如二叉堆)
插入操作:O(log n)
删除最小元素操作:O(log n)
查找最小元素操作:O(1) - 使用平衡二叉搜索树(例如红黑树)
插入操作:O(log n)
删除最小元素操作:O(log n)
查找最小元素操作:O(log n)
使用Python实现的优先队列示例(使用堆)
import heapq
class PriorityQueue:
def __init__(self):
self._queue = []
self._index = 0
def insert(self, item, priority):
heapq.heappush(self._queue, (priority, self._index, item))
self._index += 1
def delete_min(self):
return heapq.heappop(self._queue)[-1]
def find_min(self):
return self._queue[0][-1]
# 示例使用
pq = PriorityQueue()
pq.insert("task1", 1)
pq.insert("task2", 3)
pq.insert("task3", 2)
print(pq.find_min()) # 输出 "task1"
print(pq.delete_min()) # 输出 "task1"
print(pq.delete_min()) # 输出 "task3"
print(pq.delete_min()) # 输出 "task2"
C++实现的优先队列示例(使用STL中的priority_queue)
#include <iostream>
#include <queue>
#include <vector>
#include <tuple>
class PriorityQueue {
private:
std::priority_queue<std::tuple<int, int, std::string>, std::vector<std::tuple<int, int, std::string>>, std::greater<std::tuple<int, int, std::string>>> pq;
int index = 0;
public:
void insert(const std::string& item, int priority) {
pq.push(std::make_tuple(priority, index, item));
index++;
}
std::string delete_min() {
if (pq.empty()) {
throw std::runtime_error("Priority queue is empty");
}
auto min_item = pq.top();
pq.pop();
return std::get<2>(min_item);
}
std::string find_min() const {
if (pq.empty()) {
throw std::runtime_error("Priority queue is empty");
}
return std::get<2>(pq.top());
// min_item 是一个 std::tuple<int, int, std::string> 类型的对象
// 通过 std::get<2>(min_item) 可以获取 min_item 中第三个位置的元素
}
};
int main() {
PriorityQueue pq;
pq.insert("task1", 1);
pq.insert("task2", 3);
pq.insert("task3", 2);
std::cout << "Min item: " << pq.find_min() << std::endl; // 输出 "task1"
std::cout << "Deleted min item: " << pq.delete_min() << std::endl; // 输出 "task1"
std::cout << "Deleted min item: " << pq.delete_min() << std::endl; // 输出 "task3"
std::cout << "Deleted min item: " << pq.delete_min() << std::endl; // 输出 "task2"
return 0;
}
std::priority_queue:
#include <queue>
#include <vector>
#include <functional> // For std::greater
// 默认是最大堆
std::priority_queue<int> maxHeap;
// 最小堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
3、对于计数排序, 每个关键字 都是介于 0 到 k 之间的整数, 这样排序 n 个关键字 能在 Θ(n+k) 时间内完成, 而当k=O(n)时, 排序时间为 Θ(n)
4、由于当关键码是有界范围内的整数时, 能够规避排序的 Ω(n lgn) 下界限制,
van Emde Boas树支持 优先队列操作 以及 其他一些操作,每个操作最坏情况运行时间为 O(lg lgn)。而这种数据结构 限制关键字 必须为 0~n-1 的整数且无重复
5、van Emde Boas树 支持在动态集合上运行时间为 O(lg lgn) 的操作: SEARCH, INSERT, DELETE, MINIMUM, MAXMUM, SUCCESSOR 和 PREDECESSOR
MEMBER(S, x) 操作 返回一个布尔值来指示x是否在动态集合S中
6、用 n 表示集合中当前元素的个数, 用 u 表示元素的可能取值范围, 每个 van Emde Boas树 操作在 O(lg lgu) 时间内运行完。要存储的关键字值的全域集合为 {0,1,2,…,u-1}, u 为全领域的大小。始终假设 u 恰好是 2 的幂, 即u = 2k, 其中整数 k ≥ 1
1、基本方法
讨论动态集合的几种存储方法。虽然这些操作 都无法达到想要的 O(lg lgn) 运行时间界
1.1 直接寻址
提供了种存储动态集合的最简单方法。将用于动态集合的直接寻址法 简化为一个位向量。维护一个 u 位的数组 A[1 … u-1], 以存储一个来自全领域 {0, 1, 2, …, u-1} 的动态集合。若值 x 属于动态集合,则元素 A[x] = 1; 否则, A[x] = 0
利用位向量方法 可以使 INSERT、DELETE和 MEMBER 操作的运行时间为 O(1),然而其余操作 (MINIMUM、MAXIMUM、SUCCESSOR 和 PREDECESSOR) 在最坏情况下 仍需 Θ(u) 的运行时间,这是因为 操作需要扫描 Θ(u) 个元素。一个集合 只包含值 0 和 u-1, 则要查询0的后继,就需要查询 1到u-2 的所有结点,直到发现 A[u-1] 中的1为止
1.2 叠加的二叉树结构
使用位向量上方叠加的一棵二叉树的方法,来缩短 对方向量的长扫描。位向量的全部元素 组成了二叉树的叶子,并且每个内部结点为 1 当且仅当其子树中 任一个叶结点包含 1
最坏情况运行时间为 Θ(u) 的操作如下:
- 查找集合中的最小值,从树根开始,箭头向下指向叶结点,总是走最左边包含1的结点
- 查找集合中的最大值,从树根开始,箭头向下指向叶结点,总是走最右边包含1的结点
- 查找x的后继,从x所在的叶结点开始,箭头向上指向树根(一路向上),直到过程中从左侧进入一个结点,其右孩子结点 z=1。然后从结点 z 出发,箭头向下,始终走最左边包含1的结点 (即查找出以z为根的子树中的最小值)
- 查找x的前驱,从x所在的叶结点开始,箭头向上指向树根,直到从右侧 进入一个结点,其左孩子结点 z=1。然后从结点z出发,箭头向下,始终走最右边包含 1 的结点 (即查找出以z为根的子树中的最大值)(与查找后继的步骤 对称)
由于树的高度为 lg u, 上面每个操作至多沿树进行 一趟向上 和 一趟向下的过程,因此 每个操作的最坏情况运行时间为 O(lg u)
这种办法 仅仅比红黑树好一点。MEMBER 操作的运行时间只有 O(1),而红黑树 却需要花费 O(lg n) 时间。另外,如果存储的元素个数 n 比 全领域大小 u 小得多,那么对于所有的其他操作,红黑树要快些
1.3 叠加一棵高度恒定的树
1、叠加一棵度更大的树,假定全域的大小为 u = 22k ,这里 k 为某个整数,那么 √u 是一个整数。叠加一棵度为 √u 的树,来代替位 向量上方叠加的二叉树。树的高度总是为2
同以前一样,每个内部结点存储的是 其子树的逻辑或,深度为1的 √u 个内部结点是每组 √u 个值的合计(即逻辑或)。可以为这些结点定义一个数组 summary[0 … √u-1],其中 summary[i] 包含 1 当且仅当 其子数组 A[i√u … (i+1)√u -1] 包含 1。称 A 的这个 √u 位子数组为第 i 个簇。位 A[x] 出现在簇号为 ⌊x/√u⌋ 中。现在,INSERT 变成一个 O(1) 运行时间的操作:要插入 x,置 A[x] 和 summary[⌊x/√u⌋] = 1
使用 summary 数组可以在 O(√u) 运行时间内实现 MINIMUM、MAXIMUM、SUCCESSOR、PREDECESSOR 和 DELETE 操作:
- 查找最小(最大)值,在 summary 数组中查找最左(最右)包含 1 的项,如 summary[i],然后在第 i 个簇中顺序查找 最左(最右)的 1
- 查找 x 的后继(前驱),先在 x 的簇中向右(左)查找。如果发现 1,则返回这个位置作为结果;否则,令 i = ⌊x/√u⌋(父结点 summary 所在的位置),然后从下标 i 开始在 summary 数组中向右(左)查找。找到第一个包含 1 的位置 就得到这个簇的下标。再在 这个簇中查找最左(最右)的 1,这个位置的元素就是后继(前驱)
所有元素 都在叶子结点,所以一定要落实到 叶子结点 - 删除值 x,设 i =⌊x/√u⌋。将 A[x] 置为 0,然后置 summary[i](父结点) 为第 i 个簇中所有位的逻辑或
最多对两个大小为 √u 的簇以及 summary 数组进行搜索(结点所在的簇 + 右侧某兄弟结点簇 + 寻找右侧某兄弟结点簇时 对 summary 进行的遍历),所以每个操作耗费 O(√u) 时间
叠加的二叉树 得到了时间 O(√u) 的操作,其渐近地快于 O(u)。然而,使用度为 √u 的树是产生 van Emde Boas 树的关键思想
2、将本节中的数据结构修改为 支持重复键
为了修改这些结构以允许多个元素,可以在每个条目中 存储一个链表的头节点,而不是仅存储一个位。这个链表表示该结构中包含多少个该值的元素,如果没有该值的元素,则用一个 NIL 值来表示
3、将本节中的数据结构修改为 支持具有关联卫星数据的键
所有操作将保持不变,只是树叶节点不再是整数数组,而是节点数组,每个节点 除了存储 x.key 之外,还存储 希望的任何附加卫星数据
4、假设我们不是叠加一个度为 u 的树,而是叠加一个度为 u1/k 的树,其中 k>1 是一个常数。这样的树的高度是多少,每个操作需要多长时间?
关于树的高度,只要确保叶子结点的总数为 u,所以树高为 k
操作最多遍历了所有 从该结点到根结点子树一层的簇(结点)(这些父结点的右子树 都没有是1的),然后回下来搜索 每层有没有1的,尽量靠左, O(k u1/k)
2、递归结构
1、对位向量上 度为 √u 的叠加树想法进行修改。上一节中,用到了大小为 √u 的 summary 数组,数组的每项都指向一个大小为 √u 的另一个结构。现在使用 结构递归,每次递归 都以平方根大小缩小全域
本节中假设u=22^k,其中k为整数,因此u,u(1/2),u(1/4),… 都为整数
T(u) = T(√u) + O(1) (20.2)
则递归式(20.2)的解为 T(u) = O(lg lgu)。令 m = lg u,那么 u = 2m,则有
S(2^m^) = S(2^m/2^) + O(1)
重命名 S(m) = T(2m),新的递归式为
S(m) = S(m/2) + O(1)
应用主方法的情况2
这个递归式的解为 S(m) = O(lg m)。将 S(m) 变回到 T(u),得到
T(u) = T(2^m^) = O(m) = O(lg m) = O(lg lgu)
设计一个递归的数据结构,该数据结构 每层递归以 √u 为因子缩小规模 (T(√u))。当一个操作 遍历这个数据结构时,在递归到 下一层次之前,其在每一层耗费常数时间 (O(1))
2、有另一种途径 来理解 lg lgu 如何最终成为递归式(20.2)的解。如果考虑 每层需要多少位来 存储全域,那么顶层需要 lg u,而后 每层需要前一层的一半的位数。一般说来,如果以 b 位开始 并且每层减少一半的位数,那么 lg b 层递归之后,只剩下一位。因为 b = lg u,那么lg lgu 层之后,全域大小为 2
一个给定的值 x 在簇编号 ⌊x/√u⌋ 中。如果把 x 看作 lg u 位的二进制整数,那么簇编号 ⌊x/√u⌋ 由 x 中最高 (lg u)/2 位决定。在 x 簇中,x 出现在位置 x mod √u 中,是由 x 中最低 (lg u)/2 位决定(因为 √u = 2m/2, 其中 m = lg u,m 为总位数)。后面需要这种方式 来处理下标
high(x) = ⌊x / √u⌋
low(x) = x mod √u
index(x, y) = x√u + y
函数 high(x) 给出了 x 的最高 (lg u)/2 位,即 x 的簇号。函数 low(x) 给了 x 的最低 (lg u)/2 位,即 x 在它自己簇中的位置。有恒等式 x = index(high(x), low(x))。这些函数中 使用的 u 值 始终为调用这些函数的数据结构的全域大小
一个给定值 x 在簇编号 ⌊x/u⌋ 中,如果将 x 看作是 lgu 位的二进制整数,簇编号由 x 中最高的 (lgu)/2 位决定
2.1 原型 van Emde Boas 结构
1、根据递归式 (20.2) 中的启示,虽然这个数据结构 对于某些操作达不到 O(lg lgu) 运行时间的目标,但它可以作为将在 20.3 节中见到的 van Emde Boas 树的基础
对于全域 {0, 1, 2, …, u - 1},定义原型 van Emde Boas 结构 (proto-vEB 结构) 。每个 proto-vEB(u) 结构都包括一个给定全域大小的属性 u
- 如果 u = 2,它是基础大小,只包含一个两个位的数组 A[0…1]
- 否则,对某个整数 k >= 1,u = 22^k,于是有 u >= 4。除了全域大小 u 之外,proto-vEB(u) 还具有以下属性
(1) 一个名为 summary 的指针,指向一个 proto-vEB(√u) 结构
(2) 一个数组 cluster[0…√u - 1],存储 √u 个指针,每个指针都指向一个 proto-vEB(√u) 结构
元素 x 递归地存储在 编号为 high(x) 的簇中,作为该簇中编号为 low(x) 的元素,这里 0 <= x < u
在 proto-vEB 结构中,使用显指针 而不是下标计算的方法。summary 数组包含了 proto-vEB 结构中递归存储的 summary 位向量,并且 cluster 数组包含了 √u 个指针
2、值 i 在由 summary 指向的 proto-vEB 结构中,第 i 个簇包含了 被表示集合中的某个值。cluster[i] 表示 i√u 到 (i + 1)√u - 1 的那些值,这些值形成了 第 i 个簇
在基础层上,实际动态集合的元素 被存储在一些 proto-vEB(2) 结构中,而其余的 proto-vEB(2) 结构则存储 summary 位。在每个非 summary 基础结构的底部,数字表示它存储的位。例如,标记为 “element 6, 7” 的 proto-vEB(2) 结构在 A[0] 中存储位 6 (0,因为元素 6 不在集合中),并在 A[1] 中存储位 7(1,因为元素 7 在集合中)
与簇一样,每个 summary 只是一个全域大小为 √u 的动态集合,而且每个 summary 表示为 一个 proto-vEB(√u) 结构。主 proto-vEB(16) 结构的 4 个 summary 位都在最左侧的 proto-vEB(4) 结构中,并且它们 最终出现在 2 个 proto-vEB(2) 结构中。例如,标记为“clusters 2, 3”的 proto-vEB(2) 结构有 A[0] = 0,意味着 proto-vEB(16) 结构的簇 2(包含元素 8、9、10、11) 都为 0;并且 A[1] = 1,说明 proto-vEB(16) 结构的簇 3 (包含元素 12、13、14、15) 至少有一个为 1(画圈的部分)
查看标为 “elements 0, 1” 左侧的那个 proto-vEB(2) 结构(方块部分)。因为 A[0] = 0, 所以 “elements 0, 1” 结构都为 0;由于 A[1] = 1,因此 “elements 2, 3” 结构至少有一个1
2.2 原型 van Emde Boas 结构上的操作
1、先看查询操作 MEMBER、MINIMUM、MAXIMUM 和 SUCCESSOR,这些操作不改变 proto-vEB 结构
都取一个参数 x 和一个 proto-vEB 结构 V 作为输入参数。这些操作均假定 0 ≤ x < V.u
2、判断一个值是否在集合中
需要 在一个适当的 proto-vEB(2) 结构中找到相应于 x 的位。借助全部的 summary 结构,这个操作 能够在 O(lg lgu) 时间内完成
PROTO-VEB-MEMBER(V,x) // x 是在对应簇中的下标
if V.u=2
return V.A[x]
else return PROTO-VEB-MEMBER(V.cluster[high(x)],low(x))
第 3 行处理递归情形,“钻入”到更小的 proto-vEB 结构。值 high(x) 表示要访问的 proto-vEB(√u) 结构,值 low(x) 表示要查询的 proto-vEB(√u) 结构中的元素
在 proto-vEB(16) 结构中调用 PROTO-vEB-MEMBER(V, 6) 会发生什么。high(6) = 1 (6 / 4 = 1),则递归到 右上方的 proto-vEB(4) 结构,并且 查询该结构的元素 low(6) = 2 (6 % 4 = 2) 。还需要继续进行递归,对于 u = 4,就有 high(2) = 1 和 low(2) = 0,查询右上方的 proto-vEB(2) 结构中的元素 0。这次 递归调用得到了基础情形,所以 通过递归调用链返回 A[0] = 0,表示 6 不在集合内
为了确定 PROTO-VEB-MEMBER 的运行时间,令 T(u) 表示 proto-vEB(u) 结构上的运行时间。每次递归调用 耗费 常数时间,其不包括 由递归调用自身所产生的时间。当 PROTO-VEB-MEMBER 做一次递归调用时,在 proto-vEB(√u) 结构上产生一次调用。因此,运行时间 可以用递归表达式 T(u) = T(√u) + O(1) 来刻画,该递归式就是前面的递归式 (20.2)。它的解为 T(u)=O(lg lgu),所以 PROTO-VEB-MEMBER 的运行为 O(lg lgu)
T(u) = T(√u) + O(1) (20.2)
3、查找最小元素
过程 PROTO-vEB-MINIMUM(V) 返回 proto-vEB 结构 V 中的最小元素,如果 V 代表的是一个空集,则返回 NIL
PROTO-vEB-MINIMUM(V)
1 if V.u == 2
2 if V.A[0] == 1
3 return 0
4 else if V.A[1] == 1
5 return 1
6 else return NIL
7 else min_cluster = PROTO-vEB-MINIMUM(V.summary)
8 if min_cluster==NIL
9 return NIL
10 else offset = PROTO-vEB-MINIMUM(V.cluster[min_cluster])
11 return index(min_cluster, offset)
第七行 查找包含集合元素的第一个簇号。做法是通过在 V.summary 上递归调用 PROTO-vEB-MINIMUM 来进行,其中 V.summary 是一个 proto-vEB(√u) 结构
第 10 行的递归调用是 查找最小元素在这个簇中的偏移量。最后,第 11 行 由簇号和偏移量来构造这个最小元素的值,并返回
虽然查询 summary 信息 允许我们快速地找到包含最小元素的簇,但是由于这个过程 需要两次调用 proto-vEB (√u) 结构,所以在最坏情况下 运行时间超过 O(lg lgu)
T(u) = 2T(√u) + O(1)
利用 变量替换法 来求解这递归式,令 m = lg u,可以得到:
T(2^m) = 2T(2^(m/2)) + O(1)
重命名 S(m) = T(2m),得到
S(m) = 2S(m/2) + O(1)
利用主方法的情况 1,得 S(m) = Θ(m) = Θ(lg u)。由于有第二个递归调用,PROTO-vEB-MINIMUM 的运行时间为 Θ(lg u),而不是 Θ(lg lgu)
4、查找后继
SUCCESSOR 的运行时间较长。在最坏情况下,它需要做 两次递归调用和一次 PROTO-VEB-MINIMUM 调用。过程 PROTO-VEB-SUCCESSOR(V, x) 返回 proto-vEB 结构 V 中大于 x 的最小元素,或者,如果 V 中不存在大于 x 的元素,则返回 NIL。它不要求 x 一定属于该集合,但假设 0 ≤ x < V.u
PROTO-vEB-SUCCESSOR(V, x)
1 if V.u == 2
2 if x == 0 and V.A[1] == 1 // 同一个簇中就有后继结点
3 return 1
4 else return NIL
5 else offset = PROTO-vEB-SUCCESSOR(V.cluster[high(x)], low(x)) // 两个在一个结点中的数后一个就是后继
6 if offset != NIL
7 return index(high(x), offset)
8 else succ-cluster = PROTO-vEB-SUCCESSOR(V.summary, high(x)) // 后继在相邻结点内
9 if succ-cluster == NIL
10 return NIL
11 else offset = PROTO-vEB-MINIMUM(V.cluster[succ-cluster])
12 return index(succ-cluster, offset)
第 2~4 行 平凡地处理当 x = 0 且 A[1] = 1 时,才能在 proto-vEB(2) 结构中找到 x 的后继。第 5~12 行处理 递归情形。第 5 行在 x 的簇内查找其后继 并将结果赋给变量 offset。第 6 行 判断这个簇是否存在 x 的后继,否则,必须在 其它簇中查找。第 8 行 将下一个非空簇号 赋给变量 succ-cluster,并利用 summary 信息来查找后继。第 9 行判断 succ-cluster 是否为 NIL,如果所有后继簇都是空的,第 10 行返回 NIL。如果 succ-cluster 不为 NIL,第 11 行将编号为 succ-cluster 的簇中的第一个元素 赋给 offset,并在第 12 行中返回这个簇中的 最小元素
在最坏情况下,PROTO-vEB-SUCCESSOR 在 proto-vEB(√u) 结构上 做两次自身递归调用 和 1 次 PROTO-vEB-MINIMUM 调用。所以,最坏情况下,PROTO-vEB-SUCCESSOR 的运行时间 用下面递归式表示:
T(u) = 2T(√u) + Θ(lg√u) = 2T(√u) + Θ(lgu)
利用 变量替换法 来求解这递归式,令 m = lg u,重命名 S(m) = T(2m),应用主方法的情况2,得到 T(u) = Θ(lg u lg lgu)
因此 PROTO-vEB-SUCCESSOR 是渐近地慢于 PROTO-vEB-MINIMUM
5、插入元素
需要将其插入相应的簇中,并还要将这个簇中的 “summary” 位设置为 1
PROTO-vEB-INSERT(V, x)
1 if V.u == 2
2 V.A[x] = 1
3 else PROTO-vEB-INSERT(V.cluster[high(x)], low(x))
4 PROTO-vEB-INSERT(V.summary, high(x))
第 3 行的递归调用 将 x 插入相应的簇中,并且第 4 行将该簇的 “summary” 位置为 1
因为 PROTO-vEB-INSERT 在最坏情况下 做 2 次递归调用,其运行时间 可由递归式 (20.3) 来表示。所以,PROTO-vEB-INSERT 的运行时间为 Θ(lg u)
T(u) = 2T(√u) + O(1)
6、删除元素
当插入新元素时,插入时 总是将一个 “summary” 位置为 1 ,然而删除时 却总不是将同样的 “summary” 位置为 0 。我们需要判断相应的簇中是否存在为 1 的位
PROTO-vEB-DELETE(V, x)
if V.u == 2
V.A[x] = 0
else
PROTO-vEB-DELETE(V.cluster[high(x)], low(x))
inCluster = false
for i = 0 to sqrt(u) - 1
if PROTO-vEB-MEMBER(V.cluster[high(x)], low(i))
inCluster = true
break
if inCluster == false
PROTO-vEB-DELETE(V.summary, high(x))
这里有 u 个键,每个成员关系检查需要 O(lglgu) 时间。通过递归调用,运行时间的递推关系式为:
T(u) = T(√u)+O(u lglgu)
我们进行代换 m = lgu 和 S(m) = T(2m)。然后我们 应用主定理的第3种情况来解决递推关系。代换回来后,我们发现运行时间为 T(u) = O(u lglgu)
7、写一个创建 proto-vEB(u) 结构的伪代码
MAKE-PROTO-vEB(u)
allocate a new vEB tree V
V.u = u
if u == 2
let A be an array of size 2
V.A[1] = V.A[0] = 0
else
V.summary = MAKE-PROTO-vEB(sqrt(u))
for i = 0 to sqrt(u) - 1
V.cluster[i] = MAKE-PROTO-vEB(sqrt(u))
3、van Emde Boas 树及其操作
1、proto-vEB 结构已经接近 运行时间为 O(lg u) 的目标。其缺陷是 大多数操作 要进行多次递归。要设计 一个类似于 proto-vEB 结构的数据结构,但要存储稍多一些的信息,由此可以去掉一些递归的需求
2、将允许全域大小 u 为任何一个 2 的幂,而且当 √u 不为整数(即 u 为 2 的奇数次幂 u=22k+1 ,其中某个整数 k ≥ 0)时,把一个数的 lgu 位分割成 最高 ⌈(lgu)/2⌉ 位和最低 ⌊(lgu)/2⌋ 位
把 2⌈(lgu)/2⌉ 记为 ⬆√u (u 的上平方根),2⌊(lgu)/2⌋ 记为 ⬇√u (u 的下平方根),于是有 u = ⬆√u · ⬇√u。当 u 为 2 的偶数次幂 (u=22k ,其中 k 为某个整数)时,有 u = ⬆√u = ⬇√u 。由于现在允许 u 是一个 2 的奇数幂
high(x) = ⌊x/⬇√u⌋
low(x) = x mod ⬇√u
index(x, y) = x⬇√u + y
3.1 van Emde Boas树
1、将全域大小为 u 的 vEB 树记为 vEB(u)。如果 u 不为 2 的基本情况,那么属性 summary 指向一棵 vEB(⬆√u) 树,而且数组 cluster[0…⬆√u-1] 指向 ⬆√u 个 vEB(⬇√u) 树。一棵 vEB 树含有 proto-vEB 结构中 没有的两个属性:
- min 存储 vEB 树中的最小元素
- max 存储 vEB 树中的最大元素
存储在 min 中的元素 并不出现在 任何递归的 vEB(⬇√u) 树中,这些树是由 cluster 数组指向他们的。因此在 vEB(u) 树 V 中存储的元素为 V.min 再加上由 V.cluster[0…⬆√u-1] 指向的递归存储在 vEB(⬇√u) 树中的元素
注意到,当一棵 vEB 树中 包含两个或两个以上元素时,我们以 不同方式处理 min 和 max:存储在 min 中的元素不出现在任何簇中,而存储在 max 中的元素却不是这样
在一棵不包含任何元素的 vEB 树中,不管全域的大小 u 如何,min 和 max 均为 NIL
2、一棵 vEB(16) 树 V,包含集合 {2, 3, 4, 5, 7, 14, 15},因为最小的元素是 2,所以 v.min 等于2,high(2) = 0,V.cluster[0].min 等于 3,因为 V.cluster[0] 中只包含元素 2 和 3,所以 V.cluster[0] 内的 vEB(2) 簇为空
min 和 max 属性是减少 vEB 树上这些操作的递归调用次数的关键
- MINIMUM 和 MAXIMUM 操作甚至不需要递归,因为可以直接返回 min 和 max 的值
- SUCCESOR 操作可以避免 一个用于判断值 x 的后继是否位于 high(x) 中的递归调用。这是因为 x 的后继位于 x 簇中,当且仅当 x 严格小于 x 簇的 max。对于 PREDECESSOR 和 min 情况,同理
- 通过 min 和 max 的值,可以在常数时间内告知一棵 vEB 是否为空、仅含一个元素或两个以上元素。这种能力将在 INSERT 和 DELETE 操作中发挥作用。如果 min 和 max 都为 NIL,那么vEB树为空。如果 min 和 max 都不为 NIL 但彼此相等,那么 vEB 树仅含一个元素。如果 min 和 max 都不为 NIL 且不相等,那么 vEB 树包含两个或两个以上元素
- 如果一棵 vEB 树为空,那么可以 仅更新它的 min 和 max 值来实现插入一个元素。因此,可以在常数时间内 向一棵空 vEB 树中插入元素。类似地,如果一棵 vEB 树仅含一个元素,也可以 仅更新 min 和 max 值在常数时间内删除这个元素。这些性质可以缩减递归调用链
实现 vEB树 操作的递归过程的运行时间 可由下面递归式来刻画:
T(u) <= T(⬆√u) + O(1)
这个递归式与 式(20.2) 相似,我们 用类似的方法来求解它。令 m = lg u,重写为:
T(2^m) <= T(2^⌈m/2⌉) + O(1)
对所有m ≥ 2,⌈m/2⌉ ≤ 2m/3,可以得到:
T(2^m) ≤ T(2^(2m/3)) + O(1)
令 S(m) = T(2m)
S(m) ≤ S(2m/3) + O(1)
根据主方法的情况2,有解 S(m) = O(lgm)。(对于渐近解,分数 2/3 与 1/2 没有任何差别,因为应用主方法时,得到 log3/21 = log21 = 0。) 于是我们有
T(u) = T(2^m) = S(m) = O(lg m) = O(lg lgu)
一棵 van Emde Boas 树的总空间需求是 O(u),直接地创建一棵空 vEB 树需要 O(u) 时间。相反,红黑树的建立只需常数时间。因此,不应使用一棵 van Emde Boas 树用于 仅仅执行少量操作的情况,因为建立数据结构的时间 要超过单个操作节省的时间
3.2 van Emde Boas树的操作
1、正如原型 van Emde Boas 结构上的操作,这里操作 取输入参数 V 和 x, 其中V是一棵van Emde Boas树, x是一个元素,假定0 ≤ x < V.u
2、查找最小元素和最大元素
vEB-TREE-MINIMUM(V)
return V.min
vEB-TREE-MAXIMUM(V)
return V.max
3、判断一个值是否在集合中
由于vEB树并不像 proto vEB 结构那样存储 所有 位信息,所以设计 vEB-TREE-MEMBER 返回 TRUE 或 FALSE 而不是 0或1
vEB-TREE-MEMBER(V, x)
1 if x==V.min or x==V.max
2 return TRUE
3 else if V.u==2
4 return FALSE
5 else return vEB-TREE-MEMBER(V.cluster[high[x]], low(x))
第3行 检查执行基础情形。因为一棵 vEB(2) 树中除了min 和 max 中的元素外,不包含其他元素
递归式(20.2)表明了 过程 vEB-TREE-MEMBER 的运行时间,这个过程的运行时间为 O(lg lgu)
4、查找后继和前驱
过程 PROTO-vEB-SUCCESSOR(V, x) 要进行 两个递归用:一个是 判断 x 的后继是否 和 x 一样被包含在 x 的簇中;如果不包含,另一个递归调用 就是要找出包含 x 后继的簇。由于能在 vEB 树很快地存访最大值,这样可以避免进行两次递归调用,并且使一次递归调用 或是簇上的或是 summary 上的,并非两者同时进行
vEB-TREE-SUCCESSOR(V, x)
1 if V.u == 2
2 if = 0 and V.max == 1 // 同一个结点中
3 return 1
4 else return NIL
5 else if V.min != NIL and x < V.min
6 return V.min
7 else max-low = vEB-TREE-MAXIMUM(V.cluster[high(x)])
8 if max-low != NIL and low(x) < max-low // 在同一个二层簇中有后继
9 offset = vEB-TREE-SUCCESSOR(V.cluster[high(x)], low(x))
10 return index(high(x), offset)
11 else succ-cluster = vEB-TREE-SUCCESSOR(V.summary, high(x)) // 不在同一个二层簇中有后继
12 if succ-cluster == NIL
13 return NIL
14 else offset = vEB-TREE-MINIMUM(V.cluster[succ-cluster]) // V.summary找后继最多找到vEB(2) 树
15 return index(succ-cluster, offset)
如果查找到的是 0 的后继 并且 1 在元素 2 的集合中,那么第 3 行返回 1;否则第 4 行返回 NIL
如果 不是基本情况,下面第 5 行判断 x 是否严格小于最小元素。如果是,那么第 6 行返回这个最小元素
x 大于或等于 vEB 树 V 中的最小元素值。第 7 行把 x 簇中的最大元素赋值给 max-low。如果 x 簇 存在大于 x 的元素,那么可确定 x 的后继就在 x 簇中。第 8 行测试这种情况。如果 x 的后继在 x 簇内,第 9 行确定 x 的后继在簇中的位置
如果 x 大于等于 x 簇中的最大元素,则进入第 11 行。在这种情况下,第 11~15 行 采用与 PROTO-vEB-SUCCSSOR 中第 8~12 行相同的方式来查找 x 的后继
递归式T(u) = 2T(√u) + O(1)
为 vEB-TREE-SUCCESSOR 的运行时间
根据第 7 行测试的结果,过程在第 9 行(全域大小为 ⬇√u 的 vEB 树上)或者第 11 行(全域大小为 ⬆√u 的 vEB 树上)对自身进行递归调用。所以 vEB-TREE-SUCCESSOR 的最坏情况运行时间为 O(lg lgu)
vEB-TREE-PREDECESSOR 过程与 vEB-TREE-SUCCESSOR 是对称的,但是多了一种附加情况:
vEB-TREE-PREDECESSOR(V, x)
1 if V.u == 2 // 对最后情况的判断
2 if x == 1 and V.min == 0
3 return 0
4 else return NIL
5 else if V.max != NIL and x > V.max
6 return V.max
7 else min-low = vEB-TREE-MINIMUM(V.cluster[high(x)]) // 是否在同一个二层簇中
8 if min-low != NIL and low(x) > min-low
9 offset = vEB-TREE-PREDECESSOR(V.cluster[high(x)], low(x))
10 return index(high(x), offset)
11 else pred-cluster = vEB-TREE-PREDECESSOR(V.summary, high(x)) // 不在同一个二层簇中
12 if pred-cluster == NIL
13 if V.min != NIL and x > V.min
14 return V.min
15 else return NIL
16 else offset = vEB-TREE-MAXIMUM(V.cluster[pred-cluster])
17 return index(pred-cluster, offset)
第13~14行 就是处理这个附加情况。这个附加情况 出现在 x 的前驱存在,而不x簇中。在 vEB-TREE-SUCCESSOR 中,如果x的后继 不在x簇中,那么断定 它一定在一个更高编号的簇中。但是如果x的前驱是 vEB树 V 中的最小元素,那么它的后继 不存在于任何一个簇中(在 V.min 中)。第13行就是检查这个条件,而第14行返回最小元素
与 vEB-TREE-SUCCESSOR 相比,这个附加情况 并不影响 vEB-TREE-PREDECESSOR 的渐近运行时间,所以它的最坏情况运行时间为 O(lg lgu)
5、插入一个元素
vEB-TREE-INSERT 只进行一次递归调用。如果簇 已包含另一个元素,那么簇编号 已存在于 summary 中,因此 我们不需要进行递归调用。如果簇 不包含任何元素,那么即将插入的元素 成为簇中唯一的元素,所以 不需要进行一次递归来将元素插人一棵空vEB树:
vEB-EMPTY-TREE-INSERT(V,x)
V.min=x
V.max=x
vEB-TREE-INSERT(V,x)
1 if V.min==NIL
2 vEB-EMPTY-TREE-INSERT(V,x)
3 else if x < V.min
4 exchange x with V.min
5 if V.u > 2
6 if vEB-TREE-MINIMUM(V.cluster[high(x)]) == NIL
7 vEB-TREE-INSERT(V.summary,high(x)) // 需要换一个二层簇
8 else vEB-TREE-INSERT(V.cluster[high(x)],low(x))
9 if x > V.max
// 对于一棵 vEB(2)(即 V.u > 2)而言,要不插在插在 V.min(1,2行),要不插在 V.max(3,4,9,10行)
10 V.max = x
第一行判断 V 是否是一棵空 vEB 树,第3~11行 假定 V 非空,因此某个元素 会被插人V的一个簇中,第4行 对 x 和 min 交换,这样就将旧的 min 元素 插入 V 的某个簇中,然后借助下面的代码插入
第9行将 x 插入它的簇中。在这种情况下,无需更新 summary,因为 x 的簇号已经存在于 summary 中
vEB-TREE-INSERT的运行时间 可以由递归式 T(u) = 2T(√u) + O(1)
表示。根据第 6 行的判断结果,或者执行第 7 行(在全域大小为 ⬆√u 的 vEB 树上)的递归调用,或者执行 第9行(在全域大小为 ⬇√u 的vEB树上)的递归调用。整个运行时间为 O(lglg u)
6、删除一个元素
vEB-TREE-DELETE(V, x)
1 if V.min == V.max
2 V.min = NIL
3 V.max = NIL
4 else if V.u == 2 // 到vEB(2)
5 if x == 0
6 V.min = 1
7 else V.min = 0
8 V.max = V.min
9 else if x == V.min // 找后继
10 first-cluster = vEB-TREE-MINIMUM(V.summary)
11 x = index(first-cluster, vEB-TREE-MINIMUM(V.cluster[first-cluster]))
12 V.min = x
13 vEB-TREE-DELETE(V.cluster[high(x)], low(x))
14 if vEB-TREE-MINIMUM(V.cluster[high(x)]) == NIL
15 vEB-TREE-DELETE(V.summary, high(x))
16 if x == V.max
17 summary-max = vEB-TREE-MAXIMUM(V.summary)
18 if summary-max == NIL
19 V.max = V.min
20 else V.max = index(summary-max, vEB-TREE-MAXIMUM(V.cluster[summary-max]))
21 else if x == V.max
22 V.max = index(high(x), vEB-TREE-MAXIMUM(V.cluster[high(x)])
第 9~22 行 假设 V 包含两个或两个以上的元素,并且 u ≥ 4。在这种情况下,必须从一个簇中删除元素。然而从一个簇中删除的元素 可能不一定是 x,这是因为如果 x 等于 min,当 x 被删除后,簇中的某个元素会成为新的 min,并且必须从簇中删除这个元素。如果第9行得到正是这种情况,那么第10行将变量 first-cluster 置为除了 min 外的最小元素所在的簇号,并且第11行置 x 为这个簇中最小元素的值
当执行到第 13 行时,需要从簇中删除 x,不论 x 是从参数传递来的,还是 x 是新的 min 元素。第 13 行从簇中删除 x。第 14 行判断删除后的簇 是否变为空,如果是,则第 15 行将这个簇号从 summary 中移除。在更新 summary 之后,可能还要更新 max。第 16 行判断是否正在删除 V 中的最大元素,如果是,则 第 17 行将编号为最大的非空簇编号 赋值给变量 summary-max。(调用 vEB-TREE-MAXIMUM (V.summary) 执行是因为已经在V.summary 上调用了 vEB-TREE-DELETE,因此有必要的话,V.summary.max 已被更新。)
最后来处理由于 x 被删除后,x 簇不为空的情况。虽然在这种情况下不需要更新 summary,但是要更新 max
因为 vEB-TREE-DELETE 会进行两次递归调用:一次在第 13 行,又一次在第 15 行。虽然过程 可能两次递归调用都执行,但是要看 实际发生了什么。为了第 15 行的递归调用,第 14 行必须确定 x 簇为空。当在第 13 行进行递归调用时,如果 x 是其簇中的唯一元素,此为 x 簇为空的唯一方式。然而 如果 x 是其簇中的唯一元素,则递归调用耗费的时间为 O(1),因为只执行第 1~3 行。于是,有了两个互斥的可能:
- 在第 15 行发生的情况下,第 13 行的递归调用占常数时间
- 第15行的递归调用不会发生
无论哪种情况,vEB-TREE-DELETE的运行时间 仍可用递归式 T(u) = 2T(√u) + O(1)
表示,因此最坏情况运行时间为 O(lg lg u)