赞
踩
skiplist 本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为 key 或者 key/value 的查找模型。
skiplist 是由 William Pugh 发明的,最早出现于他在 1990 年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。
skiplist 是一个 list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是 O(N)。
William Pugh 开始的优化思路:
上面我们说到,skiplist 插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时的效率呢?
这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数 maxLevel 的限制,其次会设置一个多增加一层的概率 p。那么计算这个随机层数的伪代码如下图:
在 Redis 的 skiplist 实现中,这两个参数的取值为:
- p = 1/4
- maxLevel = 32
因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
跳表的平均时间复杂度为 O(logN)。
可以了解学习:Redis 内部数据结构详解 —— skiplist-CSDN博客
力扣对应题目链接:1206. 设计跳表 - 力扣(LeetCode)
- //力扣AC代码
- struct SkiplistNode
- {
- int _val;
- vector<SkiplistNode*> _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] && target > cur->_nextV[level]->_val)
- {
- cur = cur->_nextV[level];
- }
- // 下一个节点为空(尾)或目标值比下一个节点值要小 -> 向下走
- else if(cur->_nextV[level] == nullptr || target < cur->_nextV[level]->_val)
- {
- level--;
- }
- else
- {
- return true;
- }
- }
- return false;
- }
-
- vector<Node*> FindPrevNode(int num)
- {
- Node* cur = _head;
- int level = _head->_nextV.size() - 1;
-
- // 插入位置每一层前一个节点指针
- vector<Node*> 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)
- {
- // 更新level层前一个
- prevV[level] = cur;
- level--;
- }
- }
- return prevV;
- }
-
- void add(int num)
- {
- // 插入位置每一层的前一个节点指针
- vector<Node*> 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<Node*> 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;
- }
- }
-
- int RandomLevel()
- {
- size_t level = 1;
- // rand() -> [0, RAND_MAX]
- while (rand() <= RAND_MAX * _p && level < _maxLevel)
- {
- level++;
- }
- return level;
- }
-
- private:
- Node* _head; // 头节点
- size_t _maxLevel = 32;
- double _p = 0.25;
- };
-
- /**
- * Your Skiplist object will be instantiated and called as such:
- * Skiplist* obj = new Skiplist();
- * bool param_1 = obj->search(target);
- * obj->add(num);
- * bool param_3 = obj->erase(num);
- */
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
了解更多,可以参考:跳表 - OI Wiki (oi-wiki.org)
skiplist 相比平衡搜索树(AVL 树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。
skiplist 的优势是:
- skiplist 实现简单,容易控制。而平衡树增删查改遍历都更复杂。
- skiplist 的额外空间消耗更低。而平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。
skiplist 中当 p=1/2 时,每个节点所包含的平均指针数目为 2。
skiplist 中当 p=1/4 时,每个节点所包含的平均指针数目为 1.33。
skiplist 相比哈希表而言,就没有那么大的优势了。相比而言:
skiplist 优势如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。