跳表
跳表 (Skip List) 是由 William Pugh 发明的一种查找数据结构,支持对数据的快速查找,插入和删除。
跳表的期望空间复杂度为
基本思想¶
顾名思义,跳表是一种类似于链表的数据结构。更加准确地说,跳表是对有序链表的改进。
为方便讨论,后续所有有序链表默认为 升序 排序。
一个有序链表的查找操作,就是从头部开始逐个比较,直到当前节点的值大于或者等于目标节点的值。很明显,这个操作的复杂度是
跳表在有序链表的基础上,引入了 分层 的概念。首先,跳表的每一层都是一个有序链表,特别地,最底层是初始的有序链表。每个位于第
记在 n 个节点的跳表中,期望包含
在跳表中查找,就是从第
复杂度证明¶
空间复杂度¶
对于一个节点而言,节点的最高层数为
在最坏的情况下,每一层有序链表等于初始有序链表,即跳表的 最差空间复杂度 为
时间复杂度¶
从后向前分析查找路径,这个过程可以分为从最底层爬到第
假设当前我们处于一个第
令
解得
由此可以得出:在长度为
现在只需要分析爬到第
所以,跳表查询的期望查找步数为
在最坏的情况下,每一层有序链表等于初始有序链表,查找过程相当于对最高层的有序链表进行查询,即跳表查询操作的 最差时间复杂度 为
插入操作和删除操作就是进行一遍查询的过程,途中记录需要修改的节点,最后完成修改。易得每一层至多只需要修改一个节点,又因为跳表期望层数为
具体实现¶
获取节点的最大层数¶
模拟以
1 2 3 4 5 6 | int randomLevel() {
int lv = 1;
// MAXL = 32, S = 0xFFFF, PS = S * P, P = 1 / 4
while ((rand() & S) < PS) ++lv;
return min(MAXL, lv);
}
|
查询¶
查询跳表中是否存在键值为 key
的节点。具体实现时,可以设置两个哨兵节点以减少边界条件的讨论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | V& find(const K& key) {
SkipListNode<K, V>* p = head;
// 找到该层最后一个键值小于 key 的节点,然后走向下一层
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
}
// 现在是小于,所以还需要再往后走一步
p = p->forward[0];
// 成功找到节点
if (p->key == key) return p->value;
// 节点不存在,返回 INVALID
return tail->value;
}
|
插入¶
插入节点 (key, value)
。插入节点的过程就是先执行一遍查询的过程,中途记录新节点是要插入哪一些节点的后面,最后再执行插入。每一层最后一个键值小于 key
的节点,就是需要进行修改的节点。
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 | void insert(const K &key, const V &value) {
// 用于记录需要修改的节点
SkipListNode<K, V> *update[MAXL + 1];
SkipListNode<K, V> *p = head;
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
// 第 i 层需要修改的节点为 p
update[i] = p;
}
p = p->forward[0];
// 若已存在则修改
if (p->key == key) {
p->value = value;
return;
}
// 获取新节点的最大层数
int lv = randomLevel();
if (lv > level) {
lv = ++level;
update[lv] = head;
}
// 新建节点
SkipListNode<K, V> *newNode = new SkipListNode<K, V>(key, value, lv);
// 在第 0~lv 层插入新节点
for (int i = lv; i >= 0; --i) {
p = update[i];
newNode->forward[i] = p->forward[i];
p->forward[i] = newNode;
}
++length;
}
|
删除¶
删除键值为 key
的节点。删除节点的过程就是先执行一遍查询的过程,中途记录要删的节点是在哪一些节点的后面,最后再执行删除。每一层最后一个键值小于 key
的节点,就是需要进行修改的节点。
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 | bool erase(const K &key) {
// 用于记录需要修改的节点
SkipListNode<K, V> *update[MAXL + 1];
SkipListNode<K, V> *p = head;
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
// 第 i 层需要修改的节点为 p
update[i] = p;
}
p = p->forward[0];
// 节点不存在
if (p->key != key) return false;
// 从最底层开始删除
for (int i = 0; i <= level; ++i) {
// 如果这层没有 p 删除就完成了
if (update[i]->forward[i] != p) {
break;
}
// 断开 p 的连接
update[i]->forward[i] = p->forward[i];
}
// 回收空间
delete p;
// 删除节点可能导致最大层数减少
while (level > 0 && head->forward[level] == tail) --level;
// 跳表长度
--length;
return true;
}
|
完整代码¶
下列代码是用跳表实现的 map。未经正经测试,仅供参考。
参考代码
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 161 162 163 164 165 166 167 168 | #include <bits/stdc++.h>
using namespace std;
template <typename K, typename V>
struct SkipListNode {
int level;
K key;
V value;
SkipListNode **forward;
SkipListNode() {}
SkipListNode(K k, V v, int l, SkipListNode *nxt = NULL) {
key = k;
value = v;
level = l;
forward = new SkipListNode *[l + 1];
for (int i = 0; i <= l; ++i) forward[i] = nxt;
}
~SkipListNode() {
if (forward != NULL) delete[] forward;
}
};
template <typename K, typename V>
struct SkipList {
static const int MAXL = 32;
static const int P = 4;
static const int S = 0xFFFF;
static const int PS = S / P;
static const int INVALID = INT_MAX;
SkipListNode<K, V> *head, *tail;
int length;
int level;
SkipList() {
srand(time(0));
level = length = 0;
tail = new SkipListNode<K, V>(INVALID, 0, 0);
head = new SkipListNode<K, V>(INVALID, 0, MAXL, tail);
}
~SkipList() {
delete head;
delete tail;
}
int randomLevel() {
int lv = 1;
while ((rand() & S) < PS) ++lv;
return min(MAXL, lv);
}
void insert(const K &key, const V &value) {
SkipListNode<K, V> *update[MAXL + 1];
SkipListNode<K, V> *p = head;
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
update[i] = p;
}
p = p->forward[0];
if (p->key == key) {
p->value = value;
return;
}
int lv = randomLevel();
if (lv > level) {
lv = ++level;
update[lv] = head;
}
SkipListNode<K, V> *newNode = new SkipListNode<K, V>(key, value, lv);
for (int i = lv; i >= 0; --i) {
p = update[i];
newNode->forward[i] = p->forward[i];
p->forward[i] = newNode;
}
++length;
}
bool erase(const K &key) {
SkipListNode<K, V> *update[MAXL + 1];
SkipListNode<K, V> *p = head;
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
update[i] = p;
}
p = p->forward[0];
if (p->key != key) return false;
for (int i = 0; i <= level; ++i) {
if (update[i]->forward[i] != p) {
break;
}
update[i]->forward[i] = p->forward[i];
}
delete p;
while (level > 0 && head->forward[level] == tail) --level;
--length;
return true;
}
V &operator[](const K &key) {
V v = find(key);
if (v == tail->value) insert(key, 0);
return find(key);
}
V &find(const K &key) {
SkipListNode<K, V> *p = head;
for (int i = level; i >= 0; --i) {
while (p->forward[i]->key < key) {
p = p->forward[i];
}
}
p = p->forward[0];
if (p->key == key) return p->value;
return tail->value;
}
bool count(const K &key) { return find(key) != tail->value; }
};
int main() {
SkipList<int, int> L;
map<int, int> M;
clock_t s = clock();
for (int i = 0; i < 1e5; ++i) {
int key = rand(), value = rand();
L[key] = value;
M[key] = value;
}
for (int i = 0; i < 1e5; ++i) {
int key = rand();
if (i & 1) {
L.erase(key);
M.erase(key);
} else {
int r1 = L.count(key) ? L[key] : 0;
int r2 = M.count(key) ? M[key] : 0;
assert(r1 == r2);
}
}
clock_t e = clock();
cout << "Time elapsed: " << (double)(e - s) / CLOCKS_PER_SEC << endl;
// about 0.2s
return 0;
}
|
跳表的随机访问优化¶
访问跳表中第
跳表的随机访问优化就是对每一个前向指针,再多维护这个前向指针的长度。假设
现在访问跳表中的第
这样,就可以快速地访问到跳表的第
参考资料¶
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用