实现跳表

本文最后更新于:2021年7月20日 下午

跳表(Skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,可比拟二叉查找树,平均期望的查找、插入、删除时间复杂度都是O(logn),许多知名的开源软件(库)中的数据结构均采用了跳表这种数据结构。

  • Redis中的有序集合zset
  • LevelDB、RocksDB、HBase中Memtable
  • ApacheLucene中的TermDictionary、Posting List

跳跃链表的最大优势在于无论是查找、插入和删除都是O(logn),不过由于跳跃链表的操作是基于概率形成的,那么它操作复杂度大于O(logn)的概率为,可以看出当n越大的时候失败的概率越小。

跳跃链表的空间复杂度的期望为O(n),链表的层数期望为O(logn).

跳表的结构

跳跃表由多条链构成(L0,L1,L2 ……,Lh),且满足如下三个条件:

  • 每条链必须包含两个特殊元素:+∞ 和 -∞(其实不需要)
  • L0包含所有的元素,并且所有链中的元素按照升序排列。
  • 每条链中的元素集合必须包含于序数较小的链的元素集合。
img

性质:

一种插入、查询、删除时间效率为O(logn),空间效率为O(n) 的数据结构。

数据结构:

1
2
3
4
5
6
struct node
{
int key;
struct node *forward[MAXlevel];
};
// MAXlevel可以是log(n)/log(2)

高度控制策略

跳表高度控制的策略,有如下两种:

  1. 限制最大高度:插入算法中限制跳表的最大高度,h=max{10, 3* RoundUp(logn)},当超过最大高度的时候,即使随机函数值为1也不能继续在 Tower上Append Entry了。边界情况,当RoundUP(logn) <RoundUp(log(n+1)),仍然可以append一次。
  2. 不限制最大高度:插入算法中不限制跳表的最大高度,以随机函数的值来确定是否继续在Tower上Append Entry。(后面会给出高度>log(n)的概率推导,概率是非常低的,所以这种方法也能Work)

查找:

在跳跃表中查找一个元素x,按照如下几个步骤进行:

  1. 从最上层的链(Lh)的开头开始

  2. 假设当前位置为p,它向右指向的节点为q(p与q不一定相邻),且q的值为y。将y与x作比较

    (1) x=y 输出查询成功及相关信息

    (2) x>y 从p向右移动到q的位置

    (3) x<y 从p向下移动一格

  3. 如果当前位置在最底层的链中(L0),且还要往下移动的话,则输出查询失败

img

插入:

向跳跃表中插入一个元素,相当于在表中插入一列从S0中某一位置出发向上的连续一段元素。有两个参数需要确定,即插入列的位置以及它的“高度”。

  • 关于插入的位置,我们先利用跳跃表的查找功能,找到比x小的最大的数y。根据跳跃表中所有链均是递增序列的原则,x必然就插在y的后面。

  • 插入列的“高度”较前者来说显得更加重要,也更加难以确定。由于它的不确定性,使得不同的决策可能会导致截然不同的算法效率。为了使插入数据之后,保持该数据结构进行各种操作均为O(logn)复杂度的性质,我们引入随机化算法(Randomized Algorithms)。

img

删除:

删除操作分为以下三个步骤:

  1. 在跳跃表中查找到这个元素的位置,如果未找到,则退出
  2. 将该元素所在整列从表中删除
  3. 将多余的“空链”删除

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/* skiplist */
#include <random>
#include <iostream>
using namespace std;
#define MAXLEVEL 4

// 随机生成level算法
int randomLevel() {
int k = 1;
while (rand() % 2) {
k++;
}
k = (k < MAXLEVEL) ? k : MAXLEVEL;
return k;
}

// node
class node {
public:
node(int val, int level) {
this->val = val;
this->level = level;
// 初始化结点指针
for (int i = 0; i < MAXLEVEL; ++i) {
forward[i] = nullptr;
}
}
~node() {}

int val;
int level;
node* forward[MAXLEVEL]; // 指向node*的数组
};

// skiplist
class skiplist {
public:
skiplist() {
max_length = 0;
// 初始化head为一个MAXLEVEL的结点,虚拟头结点
node* p = new node(0, MAXLEVEL);
head = p;
}

// insert 所有方法都是跳过第一个结点
void insert(int val) {
int level = randomLevel();
if (head == nullptr) {
// 第一个结点高度为maxlevel
node* p = new node(val, MAXLEVEL);
head = p;
max_length++;
}
else {
node* cur = head;
node* p = new node(val, level);
// 保存每一层插入的最近前结点
node* last[MAXLEVEL];
for (int i = MAXLEVEL-1; i >= 0; --i) {
// 在第i层找到最后一个比val小的结点
while (cur->forward[i] != nullptr && cur->forward[i]->val < val) {
cur = cur->forward[i];
}
last[i] = cur;
}
// 找到插入位置
if (cur->forward[0] == nullptr) {
max_length++;
// 插入到最后
for (int i = p->level - 1; i >= 0; --i) {
last[i]->forward[i] = p;
}
} else if (cur->forward[0]->val > val) {
max_length++;
// 插入到中间
for (int i = p->level - 1; i >= 0; --i) {
p->forward[i] = last[i]->forward[i];
last[i]->forward[i] = p;
}
}
}
}

// search,从最高level开始
void search(int val) {
node* cur = head;
for (int i = MAXLEVEL-1; i >= 0; --i) {
while (cur->forward[i] != nullptr && cur->forward[i]->val < val) {
cur = cur->forward[i];
}
if (cur->forward[i] != nullptr && cur->forward[i]->val == val) {
cout << "search val = " << val << endl;
}
}
cout << "not find val = " << val << endl;
}

// delete,方法同insert相同
void del(int val) {
node* cur = head;
node* target = nullptr;
node* last[MAXLEVEL];
// 找到结点,并保存前结点
for (int i = MAXLEVEL-1; i >= 0; --i) {
while (cur->forward[i] != nullptr && cur->forward[i]->val < val) {
cur = cur->forward[i];
}
last[i] = cur;
if (cur->forward[i] != nullptr && cur->forward[i]->val == val) {
target = cur->forward[i];
}
}
if (target != nullptr) {
// 执行删除
for (int i = target->level - 1; i >= 0; --i) {
last[i]->forward[i] = target->forward[i];
}
delete(target);
--max_length;
cout << "delete val = " << val << endl;
}
}

int get_max_length() {
return max_length;
}

// print
void print() {
for (int i = MAXLEVEL-1; i >= 0; --i) {
node* cur = head;
while (cur->forward[i]) {
cur = cur->forward[i];
cout << cur->val << " --> ";
}
cout << endl;
}
}

private:
node* head; // 指向跳表头结点
int max_length; // 计算跳表的最大结点个数
};


int main() {
skiplist sk1;
sk1.insert(3);
sk1.insert(7);
sk1.insert(4);
sk1.insert(6);
sk1.insert(5);
sk1.insert(-1);
sk1.del(4);
sk1.insert(6);
sk1.search(8);
sk1.del(8);
sk1.print();
return 0;
}

补充:

【问题】在Redis中,zset 是如何实现的?

当有序集合的结点个数 > 128 或者任意节点的 value 值 > 64 时,才会使用 skiplist 存储,否则使用 ziplist(字节数组) 存储。

【问题】Redis中,节点的层数如何确定?

限定最高层级。

1
2
3
4
5
6
7
#define ZSKIPLIST_MAXLEVEL 32
int zslRandomLevel(void) {
int level = 1;
while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

参考:

https://mp.weixin.qq.com/s/6rVgWdb8wG9d-xInkguv_w

https://blog.csdn.net/u013011841/article/details/39158585

https://blog.csdn.net/weixin_44866921/article/details/108956004

https://blog.csdn.net/weixin_45827856/article/details/103254965


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!