跳表(Skip List)是一种可以进行对数级别查找的数据结构,它通过在数据中构建多级索引来提高查询效率。跳表是一种基于链表的随机化数据结构,其本质是由多个链表组成,每个链表中的元素都是原始链表中的元素。
我们知道链表的查找时间复杂度是O(N),如果这个链表数据是有序的,还是O(N),我们如何利用有序这一点,来进行优化呢?接下来就是我们的主角跳表登场。
跳表在实际应用中有许多用途,例如作为Redis等数据库的有序数据结构实现,以及作为平衡树等数据结构的替代方案。与其他数据结构相比,跳表具有实现简单、空间复杂度低、查询效率高等优点。
注意重点,在这种处理下,我们插入的结点的层数是随机的!如此一来,插入删除操作就简化了很多。
首先要分析,这个随机层数是怎么来的。一般跳表会设置一个最大的层数限制maxLevel。其次会设计一个概率p。这个p就是指 最开始从第一层开始,每多一层的概率为p。
最大层数限制很好理解,这个p就是每次插入的时候,由它来决定这个结点有多少层。每多一层其实就是多一个指针。
在Redis中,这两个参数的取值为
- p = 1/4
- maxLevel = 32
再简单分析这个p就是:
平均计算如下:
其实skiplist只要理解了思想,实现起来还是比较简单了,我们可以参考力扣上的一道题
代码:
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
-
- using namespace std;
-
- struct SkiplistNode
- {
- int _val;
- vector
_nextV; // 层数也就是指针,用数组存起来 -
- SkiplistNode(int val, int level)
- :_val(val)
- , _nextV(level, nullptr)
- {}
- };
-
- class Skiplist
- {
- typedef SkiplistNode Node;
- public:
- Skiplist()
- {
- srand(time(0));
-
- // 头结点的层数设置为1
- _head = new SkiplistNode(-1, 1);
- }
-
- bool search(int target)
- {
- Node* cur = _head;
- int level = _head->_nextV.size() - 1;
- while (level >= 0)
- {
- // 目标值比下一个结点的值要大的话,就往右走
- // 如果下一个结点是空(尾),或者目标值比下一个结点要小,就向下走
- if (cur->_nextV[level] && cur->_nextV[level]->_val < target)
- {
- // 往右走
- cur = cur->_nextV[level];
- }
- else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > target)
- {
- // 往下走
- --level;
- }
- else
- {
- // 找到了
- return true;
- }
- }
-
- return false;
- }
-
- vector
FindPrevNode(int num) - {
- Node* cur = _head;
- int level = _head->_nextV.size() - 1;
-
- // 记录 被改动的位置的每一层的前一个结点指针
- vector
prevV(level + 1, _head) ; -
- while (level >= 0)
- {
- // 同理 比下一个结点大就往右走
- // 下一个结点是空,或者目标值比下一个结点要小,就往下走
- if (cur->_nextV[level] && cur->_nextV[level]->_val < num)
- {
- cur = cur->_nextV[level]; // 向右走
- }
- else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num)
- {
- // 注意 这里num等于也是要进来的
- // 更新level层的前一个
- prevV[level] = cur;
-
- --level; // 向下走
- }
- }
-
- return prevV;
- }
-
- void add(int num)
- {
- vector
prevV = FindPrevNode(num); -
- int n = RandomLevel();
- Node* newnode = new Node(num, n);
-
- // 注意:如果n超过了当前的最大层,那么就要相应的提高_head的层数
- if (n > _head->_nextV.size())
- {
- _head->_nextV.resize(n, nullptr);
- prevV.resize(n, _head);
- }
-
- // 链接前后结点
- for (size_t i = 0; i < n; ++i)
- {
- // 注意顺序,先设置好目标结点的指针指向
- newnode->_nextV[i] = prevV[i]->_nextV[i];
- prevV[i]->_nextV[i] = newnode;
- }
- }
-
- bool erase(int num)
- {
- vector
prevV = FindPrevNode(num); -
- // 如果第一层的下一个不是val,则val不在表中
- if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)
- {
- return false;
- }
- else
- {
- Node* del = prevV[0]->_nextV[0];
- // 先将del结点的前后结点链接起来
- for (size_t i = 0; i < del->_nextV.size(); ++i)
- {
- prevV[i]->_nextV[i] = del->_nextV[i];
- }
- delete del;
-
- // 如果删除的这个结点改变了跳表结点的当前最高层数,
- // 那么应该将头结点的层数降到第二高的层数
- int i = _head->_nextV.size() - 1;
- while (i >= 0)
- {
- if (_head->_nextV[i] == nullptr)
- --i;
- else
- break;
- }
- _head->_nextV.resize(i + 1);
-
- return true;
- }
-
- return false;
- }
-
- int RandomLevel()
- {
- size_t level = 1;
- // rand() 的取值范围在 [0,RAND_MAX] 之间
- // 这里就转换成了 如果区间在 [0,_p]就加一层
- while (rand() <= RAND_MAX * _p && level < _maxLevel)
- {
- ++level;
- }
-
- return level;
- }
- private:
- Node* _head;
- size_t _maxLevel = 32;
- double _p = 0.25;
- };
-
- void Test()
- {
- Skiplist sl;
- //int a[] = { 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6 };
- int a[] = { 1, 2, 3, 4 };
- for (auto e : a)
- {
- sl.add(e);
- }
-
- /*int x;
- cin >> x;
- sl.erase(x);*/
- sl.erase(1);
- //cout << sl.erase(0) << " " << sl.erase(1) << endl;
- }
跳表稍复杂一点的地方就是插入和删除了。因为我们还要需要找到目标结点的每一层的前一个结点,将它们放入数组中,然后才能处理好目标结点的前后结点之间的关系。
另外就是它的查找逻辑,设cur来遍历结点,那么cur的移动就有两种情况,一种是目标值大于cur下一个结点的值的话,cur就向右走;另一种就是小于,那么cur就向下走(--level)。再然后就是等于了。
二者都可以做到遍历数据有序,并且时间复杂度都差不多。
跳表跟平衡搜索树(AVL树和RB树)的优势就是:
1. 跳表实现简单,且容易控制。不像AVL树和RB树非常复杂,跳表这里我们删除操作都很容易就实现了。
2.跳表的空间消耗相对较低,不像平衡搜索树,不仅每个结点都有三叉链(指针),而且还要存平衡因子或者颜色。当跳表中的p = 1/2时,每个结点所包含的指针个数为2;p = 1/4时,每个结点所包含的指针个数为1.33。
因此,跟平衡搜索树比起来,还有是有优势的。
跳表跟哈希表比起来,各有优缺点。
跳表的优点:
1.空间消耗还是略低哈希表。哈希表存在链表指针和表空间的消耗。
2.跳表遍历数据能有序。
3.哈希表扩容时有性能损耗。跳表就没有。
4.在极端场景下,哈希表哈希冲突高,效率下降的厉害,还需要红黑树来接力,增加了算法复杂度。
哈希表的优点:
时间复杂度是O(1),比跳表要快。
所以这样看来,跳表跟哈希表比起来,有些还是有优势的,但是没有跟平衡搜索树比起来那么大。