跳表 SkipList
跳表,又叫做跳跃表,它在有序链表的基础上实现了“跳跃”。这种数据结构 由William Pugh于1990年发布,设计的初衷是为了取代平衡树(例如红黑树)。Redis中的SortedSet、LevelDB中的 MemTable都用到了跳表,对比平衡树,跳表的实现和维护会更加简单,跳表的搜索、删除、添加的平均时间复杂度都是O(logN),甚至可以替代红黑树。
跳表的原理
图片来源网络
对于一个普通有序链表的查找只能从头到尾遍历,时间复杂度为O(N)。
对链表稍作修改,新增一层“索引层”,从原先的逐个查找,到跳跃查找。
当索引层越高,每次跳跃的距离也越远,实现了在链表上的二分查找。
调表的查找
- 从顶层链表的首元素开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部
- 如果该元素等于目标元素,则表明该元素已被找到
- 如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索
例: 在下图中查找19
- 19小于21,往下一层查找
- 19大于9,移动到9所处节点
- 19小于21,下一层
- 19大于17,移动到17所处节点
- 19小于21,下一层
- 找到
跳表的插入
如果跳表内已经存在被插入节点,只需要更新其value即可。如果被插入节点为新节点,就需要找到所有的前驱节点,使新节点的next根更新为前序节点的next,前序节点next更新为新节点。但如果为双向链表就不用那么麻烦了。
新节点的索引层数是随机的。
例: 插入17
跳表的删除
跳表的删除和插入差不多,也要找到其前驱节点,并更新。
删除节点后需要更新索引层的高度。
C++ 实现
#include <iostream>
#include <cmath>
#include <ctime>
#include <cassert>
#include <algorithm>
template<class K, class V>
class SkipList
{
struct ListNode
{
ListNode(const K& key, const V& value, int level) :
_key(key), _value(value), _next(nullptr), _level(level), _nexts(new ListNode* [_level])
{
for (int i = 0; i < level; i++)
_nexts[i] = nullptr;
}
~ListNode()
{
delete[] _nexts;
}
K _key;
V _value;
ListNode* _next;
int _level;
ListNode** _nexts;
};
typedef ListNode Node;
public:
SkipList()
{
_head = new Node(K(), V(), MAX_LEVEL);
}
~SkipList()
{
while (_head)
{
Node* next = _head->_next;
delete _head;
_head = next;
}
}
V get(const K& key)
{
assert(!empty());
int index = _level - 1;
Node* cur = _head;
while (index >= 0)
{
if (cur->_nexts[index] == nullptr)
index--;
else if (cur->_nexts[index]->_key < key)
cur = cur->_nexts[index];
else if (cur->_nexts[index]->_key >= key)
index--;
else
return cur->_nexts[index]->_value;
}
return V();
}
V put(const K& key, const V& value)
{
int index = _level - 1;
Node* cur = _head;
bool find = false;
Node** prevs = new Node * [_level];
while (index >= 0)
{
if (cur->_nexts[index] == nullptr)
prevs[index--] = cur;
else if (cur->_nexts[index]->_key < key)
cur = cur->_nexts[index];
else if (cur->_nexts[index]->_key > key)
prevs[index--] = cur;
else
{
cur = cur->_nexts[index];
find = true;
break;
}
}//end of while
if (find)
{
V tmp = cur->_value;
cur->_value = value;
delete[] prevs;
return tmp;
}
int newLevel = randomLevel();
Node* newNode = new Node(key, value, newLevel);
//std::cout << newLevel << std::endl;
for (int i = 0; i < newLevel; i++)
{
if (i >= _level)
{
_head->_nexts[i] = newNode;
}
else
{
newNode->_nexts[i] = prevs[i]->_nexts[i];
prevs[i]->_nexts[i] = newNode;
}
}
_level = std::max(newLevel, _level);
_size++;
delete[] prevs;
return value;
}
bool remove(const K& key)
{
int index = _level - 1;
Node* cur = _head;
bool find = false;
Node** prevs = new Node * [_level];
while (index >= 0)
{
if (cur->_nexts[index] == nullptr)
prevs[index--] = cur;
else if (cur->_nexts[index]->_key < key)
cur = cur->_nexts[index];
else if (cur->_nexts[index]->_key > key)
prevs[index--] = cur;
else {
prevs[index--] = cur;
find = true;
}
}
if (!find)
{
delete[] prevs;
return false;
}
Node* delNode = prevs[0]->_nexts[0];
for (int i = 0; i < delNode->_level; i++)
prevs[i]->_nexts[i] = delNode->_nexts[i];
//更新层数
while (_level > 0 && _head->_nexts[_level - 1] == nullptr)
_level--;
_size--;
delete delNode;
delete[] prevs;
return true;
}
int randomLevel()
{
int level = 1;
while ((rand() / (RAND_MAX + 1.0) < P) && level < MAX_LEVEL)
level++;
return level;
}
bool empty()
{
return _size == 0;
}
public:
const double P = 0.25; //概率 一般取 0.5 或0.25
const int MAX_LEVEL = 32;
size_t _size = 0;
Node* _head;
int _level = 0;
};
时间 / 空间复杂度
时间复杂度:
跳表的增删改都是基于查找,时间复杂度为O (logN)
空间复杂度:
第1层索引层的节点数为 N/2,第2层为 N/4 , 第3层 N/8 …
等比数列,最终索引层的节点数也不会超过N,空间复杂度O(N)。
在实际开发中,实际存储的节点数据基本要比辅助使用的内存要大得多。
总结
跳表相比于红黑树,实现起来更加简单,代码易于维护,接口的时间复杂度都一样。
跳表在进行区间查找时非常高效,由于底层是一个链表,只需要定位其左区间,就可以向后遍历获取整个区间。
跳表更加灵活,通过更改索引层的创建策略,可以实现效率和内存使用的人为调节。