算法导论 总结索引 | 第五部分 第二十章:van Emde Boas树

news2024/11/15 11:55:28

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、优先队列
每个元素都有一个与之相关的优先级。在优先队列中,高优先级的元素在低优先级的元素之前被处理
插入元素:将一个新元素插入到优先队列中。
删除最小元素:从优先队列中删除并返回优先级最高(通常是最小)的元素。
查找最小元素:返回优先级最高(通常是最小)的元素,但不删除它

几种实现优先队列的常见方法:

  1. 使用数组实现
    插入操作:O(1)
    删除最小元素操作:O(n)
    查找最小元素操作:O(n)
  2. 使用有序数组实现
    插入操作:O(n)
    删除最小元素操作:O(1)
    查找最小元素操作:O(1)
  3. 使用堆实现(例如二叉堆)
    插入操作:O(log n)
    删除最小元素操作:O(log n)
    查找最小元素操作:O(1)
  4. 使用平衡二叉搜索树(例如红黑树)
    插入操作: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 树上这些操作的递归调用次数的关键

  1. MINIMUM 和 MAXIMUM 操作甚至不需要递归,因为可以直接返回 min 和 max 的值
  2. SUCCESOR 操作可以避免 一个用于判断值 x 的后继是否位于 high(x) 中的递归调用。这是因为 x 的后继位于 x 簇中,当且仅当 x 严格小于 x 簇的 max。对于 PREDECESSOR 和 min 情况,同理
  3. 通过 min 和 max 的值,可以在常数时间内告知一棵 vEB 是否为空、仅含一个元素或两个以上元素。这种能力将在 INSERT 和 DELETE 操作中发挥作用。如果 min 和 max 都为 NIL,那么vEB树为空。如果 min 和 max 都不为 NIL 但彼此相等,那么 vEB 树仅含一个元素。如果 min 和 max 都不为 NIL 且不相等,那么 vEB 树包含两个或两个以上元素
  4. 如果一棵 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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1950245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络流】——初识(最大流)

网络流-最大流 基础信息引入一些概念基本性质 最大流定义 Ford–Fulkerson 增广Edmons−Karp算法Dinic 算法参考文献 基础信息 引入 假定现在有一个无限放水的自来水厂和一个无限收水的小区&#xff0c;他们之间有多条水管和一些节点构成。 每一条水管有三个属性&#xff1a…

sql_exporter通过sql收集业务数据并通过prometheus+grafana展示

下载并解压安装sql_exporter wget https://github.com/free/sql_exporter/releases/download/0.5/sql_exporter-0.5.linux-amd64.tar.gz #解压 tar xvf sql_exporter-0.5.linux-amd64.tar.gz -C /usr/local/修改主配置文件 cd /usr/local/ mv sql_exporter-0.5.linux-amd64 s…

海山数据库(He3DB)技术解析:海山Redis定时任务与持久化管控设计

文章目录 引言一、背景介绍二、具体实现1、多副本容灾功能2、主备切换后任务断点续做功能3、持久化管控编排功能 三、总结作者 引言 云Redis数据库服务是目前广泛应用的模式&#xff0c;其数据持久化方案是现在研究的热点内容&#xff0c;数据持久化操作主要由参数设置自动触发…

AI学习记录 - 激活函数的作用

试验&#xff0c;通过在线性公式加入激活函数&#xff0c;可以拟合复杂的情况&#xff08;使用js实现&#xff09; 结论:1、线性函数的叠加&#xff0c;无论叠加多少次&#xff0c;都是线性的 如下图 示例代码 线性代码&#xff0c;使用ykxb的方式&#xff0c;叠加10个函数…

AnimationCurve动画曲线 简单使用

资料 AnimationCurve AnimationCurve 表示一条曲线。可在曲线上添加关键帧&#xff0c;编辑曲线。 水平轴表示时间&#xff0c;竖直轴表示曲线的高度 获取曲线高度方法&#xff0c;AnimationCurve.Evaluate 示例 循环移动Cube,Cube沿着曲线移动 using UnityEngine; publ…

正则表达式与文本处理

目录 一、正则表达式 1、正则表达式定义 1.1正则表达式的概念及作用 1.2、正则表达式的工具 1.3、正则表达式的组成 2、基础正则表达式 3、扩展正则表达式 4、元字符操作 4.1、查找特定字符 4.2、利用中括号“[]”来查找集合字符 4.3、查找行首“^”与行尾字符“$”…

火山引擎边缘智能平台,让AI走进企业现场

如何让大模型更好地与生产进行融合&#xff0c;让AI生产力为企业降本增效&#xff0c;是每个企业都在关注的问题。但设备异构、隐私安全、传输延迟等困难&#xff0c;让大模型走进企业现场变得步履维艰。这种情况&#xff0c;就需要借助边缘智能来应对这些挑战。 什么是边缘智能…

二维数组前缀和

二维数组前缀和&#xff08;Leetcode304&#xff09; 想法(参考题解)&#xff1a; 如上图&#xff0c;在矩阵中根据给定的方框围成的范围&#xff0c;确定范围内元素之和。题目&#xff1a;二维区域和检索 - 矩阵不可变。思路就是使用前缀和&#xff0c;前缀和表示的是面积&am…

Linux 安装 GDB (无Root 权限)

引入 在Linux系统中&#xff0c;如果你需要在集群或者远程操作没有root权限的机子&#xff0c;安装GDB&#xff08;GNU调试器&#xff09;可能会有些限制&#xff0c;因为通常安装新软件或更新系统文件需要管理员权限。下面我们介绍可以在没有root权限的情况下安装GDB&#xf…

微信小程序获取蓝牙并实现内容打印

通过微信小程序如何实现获取蓝牙打印机并实现打印能力&#xff0c;之前做过一个测试Dome&#xff0c;能够获取附近的蓝牙打印机设备并实现打印,今天开放出来供大家参考。 wxml <!--右下角搜索--> <view class"ly-cass-box"><view class"ly-cas…

【Python第三方库】PyQt5安装与应用

文章目录 引言安装PYQT5基于Pyqt5的简单桌面应用常用的方法与属性QtDesigner工具使用与集成窗口类型QWidget和QMainWindow区别 UI文件加载方式直接加载UI文件的方式显示窗口转化py文件进行显示窗口 PyQt5中常用的操作信号与槽的设置绑定页面跳转 引言 PyQt5是一个流行的Python…

Java——多线程(2/9):线程创建方式三Callable,Thread的常用方法(如何创建、代码实例、API及优缺点)

目录 方式三&#xff1a;实现Callable接口 前言 如何创建 代码实例 API及优缺点 Thread的常用方法 代码演示 方式三&#xff1a;实现Callable接口 前言 前两种线程创建方式都存在的一个问题 假如线程执行完毕后有一些数据需要返回&#xff0c;他们重写的run方法均不能…

算法与算法分析

目录 一.前言 二.算法的特性和要求 三.分析算法--时间效率 四. 分析算法--空间效率 一.前言 算法就是对特定问题求解方法和步骤的一种描述&#xff0c;它是指令的有限序列。其中&#xff0c;每个指令表示一个或多个操作。总而言之&#xff0c;我们数据结构就是通过算法实现操…

如何根据员工的反馈来确定六西格玛培训需求?

在六西格玛的推广与实施过程中&#xff0c;最大的挑战往往不在于技术本身&#xff0c;而在于如何让每一位员工都能理解、接受并积极参与其中。员工是企业最直接的执行者&#xff0c;他们的声音直接反映了项目落地的难易程度及潜在障碍。因此&#xff0c;倾听并有效整合员工反馈…

Python Django功能强大的扩展库之channels使用详解

概要 随着实时 web 应用程序的兴起,传统的同步 web 框架已经无法满足高并发和实时通信的需求。Django Channels 是 Django 的一个扩展,旨在将 Django 从一个同步 HTTP 框架转变为一个支持 WebSockets、HTTP2 和其他协议的异步框架。它不仅能够处理传统的 HTTP 请求,还可以处…

STM32串口(串口基础)

串口整个东西可以说但凡你要碰单片机&#xff0c;想做点上点档次的东西的话那你就包用它的。32的串口配置并不难&#xff0c;哪怕是比起51其实也难不到哪去。 目录 一.通信基础 1.通信方式 2.通信速率 二.串口基础 1.串口的数据帧结构&#xff08;协议&#xff09; 2.ST…

【轨物方案】开关柜在线监测物联网解决方案

随着物联网技术的发展&#xff0c;电力设备状态监测技术也得到了迅速发展。传统的电力成套开关柜设备状态监测方法主要采用人工巡检和定期维护的方式&#xff0c;这种方法不仅效率低下&#xff0c;而且难以保证设备的实时性和安全性。因此&#xff0c;基于物联网技术的成套开关…

JDBC标准实现

JDBC是什么 Java Database Connectivity&#xff1a;Java访问数据库的解 决方案 JDBC定义了一套标准接口&#xff0c;即访问数据库的通用API&#xff0c; 不同的数据库厂商根据各自数据库的特点去实现这些接口。 JDBC希望用相同的方式访问不同的数据库&#xff0c;让具体 的…

Nacos 2.x 新增 grpc 端口,Nginx 需要配置TCP端口转发的注意事项

Nacos 2.x 开始&#xff0c;最大的变化就是端口。在默认主端口 8848 之外又新增了三个端口&#xff0c;新增端口是在配置的主端口 server.port 的基础上&#xff0c;进行一定偏移量自动生成。 8848&#xff08;主端口&#xff0c;默认8848&#xff09;web页面端口及相关http接口…

医院等保解决方案有哪些?用哪些软件可以加强等级保护?

在医疗领域&#xff0c;信息系统的安全直接关系到患者的隐私保护、医院的正常运营乃至社会的稳定。医院信息系统面临着越来越多的安全挑战。 为了确保患者信息的安全与隐私保护&#xff0c;医院需要采取有效的等保&#xff08;等级保护&#xff09;措施。那么&#xff0c;医院…