介绍
- 跳表全称叫做跳跃表,简称跳表。
- 跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。
- 跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
- 跳表可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis中有用到。
- 跳表采用随机技术决定链表中哪些节点应增加向前指针以及在该节点中应增加多少个指针。跳表结构的头节点需有足够的指针域,以满足可能构造最大级数的需要,而尾节点不需要指针域。
- 跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候已经十分接近要查找的元素的位置。
- 跳表中的搜索、插入、删除操作的时间均为O(logn),然而,最坏情况下时间复杂性却变成O(n)。
实现
跳表层数
由于跳表是多层链表结构,层数需要指定,即跳表的最大高度,取经验值level = 8
;
1
| static const int level = 8;
|
跳表节点
在跳表的每一层都由头节点引出一条链表,所以一个跳表节点需要:
- 节点值;
- next数组,存储该节点在每一层的next指针,没有就是空指针;
1 2 3 4 5 6 7 8 9
| struct Node { int val; vector<Node*> next;
Node(int _val) : val(_val) { next.resize(level, nullptr); } } *head;
|
跳表结构
- 一个跳表类中成员函数一般就是查找、插入、删除操作;
- 层索引是从最下层到最上层,0~level-1;
- 我们统一定义一个辅助的查询函数
find()
,用来找到每一层中小于目标值的最大的节点;
- 每一层的查找、插入、删除操作同于操作普通链表;
- 插入操作时从下往上遍历每一层,每一层有50%的概率插入新节点,第0层一定是含有所有节点;
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
| class Skiplist { public: Skiplist() { head = new Node(-1); } ~Skiplist() { delete head; }
void find(int target, vector<Node*>& pre) { auto p = head; for (int i = level - 1; i >= 0; --i) { while (p->next[i] && p->next[i]->val < target) { p = p->next[i]; } pre[i] = p; } }
bool search(int target) { vector<Node*> pre(level); find(target, pre);
auto p = pre[0]->next[0]; return p && p->val == target; }
void add(int num) { vector<Node*> pre(level); find(num, pre);
auto p = new Node(num); for (int i = 0; i < level; i++) { p->next[i] = pre[i]->next[i]; pre[i]->next[i] = p; if (rand() % 2) break; } }
bool erase(int num) { vector<Node*> pre(level); find(num, pre);
auto p = pre[0]->next[0]; if (!p || p->val != num) return false;
for (int i = 0; i < level && pre[i]->next[i] == p; ++i) { pre[i]->next[i] = p->next[i]; }
delete p;
return true; } };
|
【参考资料】力扣题解-设计跳表