文章著作权归纪卓志(https://github.com/jizhuozhi)所有,转载需注明引用地址(https://blog.csdn.net/ji_1218060852/article/details/128605716),侵权必究
跳跃表[1,2,3]是一种用于在大多数应用程序中取代平衡树的概率数据结构。跳跃表拥有与平衡树相同的期望时间上界,并且更简单、更快、是用更少的空间。在查找与列表的线性操作上,比平衡树更快,并且更简单。
概率平衡也可以被用在基于树的数据结构[4]上,例如树堆(Treap)。与平衡二叉树相同,跳跃表也实现了以下两种操作
- 通过搜索引用[5],可以保证从任意元素开始,搜索到在列表中间隔为 k k k的元素的任意期望时间是 O ( l o g k ) O(log k) O(logk)
- 实现线性表的常规操作(例如将元素插入到列表第k个元素后面)
这几种操作在平衡树中也可以实现,但是在跳跃表中实现起来更简单而且非常的快,并且通常情况下很难在平衡树中直接实现(树的线索化可以实现与链表相同的效果,但是这使得实现变得更加复杂[6])
预览
最简单的支持查找的数据结构可能就是链表。Figure.1是一个简单的链表。在链表中执行一次查找的时间正比于必须考查的节点个数,这个个数最多是 N N N。
Figure.2表示一个链表,在该链表中,每个一个节点就有一个附加的指针指向它在表中的前两个位置上的节点。正因为这个前向指针,在最坏情况下最多考查 ⌈ N / 2 ⌉ + 1 \lceil N/2\rceil+1 ⌈N/2⌉+1个节点。
Figure.3将这种想法扩展,每个序数是4的倍数的节点都有一个指针指向下一个序数为4的倍数的节点。只有 ⌈ N / 4 ⌉ + 2 \lceil N/4\rceil+2 ⌈N/4⌉+2个节点被考查。
这种跳跃幅度的一般情况如Figure.4所示。每个 2 i 2^i 2i节点就有一个指针指向下一个 2 i 2^i 2i节点,前向指针的间隔最大为 N / 2 N/2 N/2。可以证明总的指针最大不会超过 2 N 2N 2N(见空间复杂度分析),但现在在一次查找中最多考查 ⌈ l o g N ⌉ \lceil logN\rceil ⌈logN⌉个节点。这意味着一次查找中总的时间消耗为 O ( l o g N ) O(logN) O(logN),也就是说在这种数据结构中的查找基本等同于二分查找(binary search)。
在这种数据结构中,每个元素都由一个节点表示。每个节点都有一个高度(height)或级别(level),表示节点所拥有的前向指针数量。每个节点的第 i i i个前向指针指向下一个级别为 i i i或更高的节点。
在前面描述的数据结构中,每个节点的级别都是与元素数量有关的,当插入或删除时需要对数据结构进行调整来满足这样的约束,这是很呆板且低效的。为此,可以将每个 2 i 2^i 2i节点就有一个指针指向下一个 2 i 2^i 2i节点的限制去掉,当新元素插入时为每个新节点分配一个随机的级别而不用考虑数据结构的元素数量。
虽然无法通过元素数量来确定每个节点的级别,但是通过考查Figure.1到Figure.4中的节点分布规律不难发现,随着级别的增加,当前级别的节点数量成比例减少。在Figure.1到Figure.4中,这个比例是 1 2 \frac{1}{2} 21,也就是说只有 1 2 i \frac{1}{2^i} 2i1个节点的级别是 i i i,随机选择节点的级别的概率分布遵循 P ( X ) = ( 1 2 ) X P(X)=(\frac{1}{2})^X P(X)=(21)X。所以Figure.5也可以认为是这种数据结构的一个实例。
数据结构
到此为止,已经得到了所有让链表支持快速查找的充要条件,而这种形式的数据结构就是跳跃表。接下来将会使用更正规的方式来定义跳跃表
- 所有元素在跳跃表中都是由一个节点表示。
- 每个节点都有一个高度或级别,有时候也可以称之为阶(step),节点的级别是一个与元素总数无关的随机数。规定
NULL
的级别是 ∞ \infty ∞。 - 每个级别为 k k k的节点都有 k k k个前向指针,且第 i i i个前向指针指向下一个级别为 i i i或更高的节点。
- 每个节点的级别都不会超过一个明确的常量
MaxLevel
。整个跳跃表的级别是所有节点的级别的最高值。如果跳跃表是空的,那么跳跃表的级别就是 1 1 1。 - 存在一个头节点
head
,它的级别是MaxLevel
,所有高于跳跃表的级别的前向指针都指向NULL
。
稍后将会提到,节点的查找过程是在头节点从最高级别的指针开始,沿着这个级别一直走,直到找到大于正在寻找的节点的下一个节点(或者是NULL
),在此过程中除了头节点外并没有使用到每个节点的级别,因此每个节点无需存储节点的级别。
在跳跃表中,级别为 1 1 1的前向指针与原始的链表结构中next
指针的作用完全相同,因此跳跃表支持所有链表支持的算法。
对应到高级语言中的结构定义如下所示(后续所有代码示例都将使用C语言描述)
#define SKIP_LIST_KEY_TYPE int
#define SKIP_LIST_VALUE_TYPE int
#define SKIP_LIST_MAX_LEVEL 32
#define SKIP_LIST_P 0.5
struct Node {
SKIP_LIST_KEY_TYPE key;
SKIP_LIST_VALUE_TYPE value;
struct Node *forwards[]; // flexible array member
};
struct SkipList {
struct Node *head;
int level;
};
struct Node *CreateNode(int level) {
struct Node *node;
assert(level > 0);
node = malloc(sizeof(struct Node) + sizeof(struct Node *) * level);
return node;
}
struct SkipList *CreateSkipList() {
struct SkipList *list;
struct Node *head;
int i;
list = malloc(sizeof(struct SkipList));
head = CreateNode(SKIP_LIST_MAX_LEVEL);
for (i = 0; i < SKIP_LIST_MAX_LEVEL; i++) {
head->forwards[i] = NULL;
}
list->head = head;
list->level = 1;
return list;
}
从前面的预览章节中,不难看出MaxLevel
的选值影响着跳跃表的查询性能,关于MaxLevel
的选值将会在后续章节中进行介绍。在此先将MaxLevel
定义为 32 32 32,这对于 2 32 2^{32} 232个元素的跳跃表是足够的。延续预览章节中的描述,跳跃表的概率被定义为 0.5 0.5 0.5,关于这个值的选取问题将会在后续章节中进行详细介绍。
算法
搜索
在跳跃表中进行搜索的过程,是通过Z字形遍历所有没有超过要寻找的目标元素的前向指针来完成的。在当前级别没有可以移动的前向指针时,将会移动到下一级别进行搜索。直到在级别为 1 1 1的时候且没有可以移动的前向指针时停止搜索,此时直接指向的节点(级别为 1 1 1的前向指针)就是包含目标元素的节点(如果目标元素在列表中的话)。在Figure.6中展示了在跳跃表中搜索元素 17 17 17的过程。
整个过程的示例代码如下所示,因为高级语言中的数组下标从0
开始,因此forwards[0]
表示节点的级别为 1 1 1的前向指针,依此类推
struct Node *SkipListSearch(struct SkipList *list, SKIP_LIST_KEY_TYPE target) {
struct Node *current;
int i;
current = list->head;
for (i = list->level - 1; i >= 0; i--) {
while (current->forwards[i] && current->forwards[i]->key < target) {
current = current->forwards[i];
}
}
current = current->forwards[0];
if (current->key == target) {
return current;
} else {
return NULL;
}
}
插入和删除
在插入和删除节点的过程中,需要执行和搜索相同的逻辑。在搜索的基础上,需要维护一个名为update
的向量,它维护的是搜索过程中跳跃表每个级别上遍历到的最右侧的值,表示插入或删除的节点的左侧直接直接指向它的节点,用于在插入或删除后调整节点所在所有级别的前向指针(与朴素的链表节点插入或删除的过程相同)。
当新插入节点的级别超过当前跳跃表的级别时,需要增加跳跃表的级别并将update
向量中对应级别的节点修改为head
节点。
Figure.7和Figure.8展示了在跳跃表中插入元素 16 16 16的过程。首先,在Figure.7中执行与搜索相同的查询过程,在每个级别遍历到的最后一个元素在对应层级的前向指针被标记为灰色,表示稍后将会对齐进行调整。接下来在Figure.8中,在元素为 13 13 13的节点后插入元素 16 16 16,元素 16 16 16对应的节点的级别是 5 5 5,这比跳跃表当前级别要高,因此需要增加跳跃表的级别到 5 5 5,并将head
节点对应级别的前向指针标记为灰色。Figure.8中所有虚线部分都表示调整后的效果。
struct Node *SkipListInsert(struct SkipList *list, SKIP_LIST_KEY_TYPE key, SKIP_LIST_VALUE_TYPE value) {
struct Node *update[SKIP_LIST_MAX_LEVEL];
struct Node *current;
int i;
int level;
current = list->head;
for (i = list->level - 1; i >= 0; i--) {
while (current->forwards[i] && current->forwards[i]->key < target) {
current = current->forwards[i];
}
update[i] = current;
}
current = current->forwards[0];
if (current->key == target) {
current->value = value;
return current;
}
level = SkipListRandomLevel();
if (level > list->level) {
for (i = list->level; i < level; i++) {
update[i] = list->header;
}
}
current = CreateNode(level);
current->key = key;
current->value = value;
for (i = 0; i < level; i++) {
current->forwards[i] = update[i]->forwards[i];
update[i]->forwards[i] = current;
}
return current;
}
在删除节点后,如果删除的节点是跳跃表中级别最大的节点,那么需要降低跳跃表的级别。
Figure.9和Figure.10展示了在跳跃表中删除元素 19 19 19的过程。首先,在Figure.9中执行与搜索相同的查询过程,在每个级别遍历到的最后一个元素在对应层级的前向指针被标记为灰色,表示稍后将会对齐进行调整。接下来在Figure.10中,首先通过调整前向指针将元素 19 19 19对应的节点从跳跃表中卸载,因为元素 19 19 19对应的节点是级别最高的节点,因此将其从跳跃表中移除后需要调整跳跃表的级别。Figure.10中所有虚线部分都表示调整后的效果。
struct Node *SkipListDelete(struct SkipList *list, SKIP_LIST_KEY_TYPE key) {
struct Node *update[SKIP_LIST_MAX_LEVEL];
struct Node *current;
int i;
current = list->head;
for (i = list->level - 1; i >= 0; i--) {
while (current->forwards[i] && current->forwards[i]->key < key) {
current = current->forwards[i];
}
update[i] = current;
}
current = current->forwards[0];
if (current && current->key == key) {
for (i = 0; i < list->level; i++) {
if (update[i]->forwards[i] == current) {
update[i]->forwards[i] = current->forwards[i];
} else {
break;
}
}
while (list->level > 1 && list->head->forwards[list->level - 1] == NULL) {
list->level--;
}
}