跳表
虽然在n个元素的有序数组中使用二分查找的时间复杂度为log(n),但是在n个有序元素的列表上时间复杂度却是O(n)。为了提高有序列表的查找性能,可以在全部或部分节点上增加额外的指针,在查找时,通过这些指针,可以跳过链表的若干节点,不必从左到右连续查看所有节点。增加了额外的前向指针的链表叫做跳表。它采用随机技术,决定链表的那些节点应该增加向前指针,以及增加多少个指针。基于这种随机技术,跳表的查找、插入、删除的平均时间复杂度为O(logn)。然而,最坏的情况仍旧是O(n)。
跳表特点与优势
我们知道,平衡树的查找效率为log(n),但是在每次插入、删除的时候由于平衡可能被破坏,需要重建平衡,导致额外的旋转操作。而跳表则使用概率平衡技术而不是强制性的平衡,因此对于插入和删除节点比传统上的平衡树算法更加高效。
跳表由原始的一个单链表以及一些索引链表组成。这些索引链表是在将新的元素插入单链表的时候随机产生的,该插入节点的索引链表节点的个数称为索引链表的层数。由于随机技术,不同单链表的索引可能有0层或者多层。处于同一层的索引节点通过指针从前往后组成一个有序链表,层数越高这个有序链表的节点数越少。
下面是一张典型的跳表:
跳表的实现
跳表的节点
和普通链表一样,跳表也需要定义一个节点。由于跳表是用来替代平衡树的,因此我们也需要定义键值对,另外由于跳表中每个节点都需要指向后续的一个或者多个节点,因此这里的next指针需要定义成一个数组,而该数组的长度由我们随机产生的level所确定。节点定义如下:
template <typename K, typename V>
struct Node
{
K key;
V value;
int level;
Node **nexts;
Node() = default;
Node(const K &k, const V &v, int lv) : key(k), value(v), level(lv)
{
nexts = new Node<K, V> *[level + 1];
memset(nexts, 0, sizeof(sizeof(Node<K, V> *) * (level + 1)));
}
~Node()
{
delete[] nexts;
}
};
这里重点说一下level和nexts指针。level是通过随机算法产生的层数,如果为0则和普通的单链表一样只有一个节点用来指向后续的节点,否则就会有多个。至于这些节点要指向哪些节点,需要在插入的时候根据插入节点的level以及找到插入位置时所走过的索引节点来确定,这部分在插入部分另外介绍。
随机算法的实现
由于每次随机的概率都是1/2,因此我们只需要一个能够生成0、1的随机算法即可。这里可以借助c++11引入的随机引擎,实现如下:
std::uniform_int_distribution<int> _int_distribute(0, 1);
std::default_random_engine _default_random_engine;
_default_random_engine(_int_distribute); // _default_random_engine 每次调用的时候会相同概率的返回0或者1
跳表的定义
假设已经存在一个长度为N的链表,该如何建立索引呢?由于我们希望跳表具有二分查找算法的效率,所以当我们要确定在某个节点处是否建立索引的时候,其概率为1/2,要在该处建立第二个索引的概率为1/4,以此类推。由于链表的长度为N,因此最大的层高为log(N)。我们先创建一个长度为k = log(N)+1的数组用来指向链表中的k个节点,我们称这个数组为header数组,在开始的时候,header数组所有的元素都为null。然后我们开始遍历单链表,在插入第一个元素的时候,我们要为该元素生成一个level,由于是随机算法该level可能大于k,因此我们需要对level的最大值做一个限定。由于插入第一个元素的时候该节点后面是空的,因此实际上level=0即可,也就是nexts数组的长度为1,我们使用一个标记_level记录当前整个跳表中所有元素的level的最大值(除了header)。当我们再次插入新元素的时候,新元素的level的最大值如果大于_level,则只能取_level+1,这样做是为了让层次有序递增,避免不必要的空间浪费。
从上面的过程可以看出,我们的跳表需要一个header数组来指向后续的索引,一个level整数来表示当前所有节点中最大的层数,一个max_level来限制最高的level并初始化header的个数。另外由于c++的随机引擎是有状态的,因此我们也需要将这部分字段放在跳表的定义中。为了以O(1)的时间复杂度获取跳表元素的个数,我们用_size字段来表示。为了以O(1)的时间复杂度判断某个元素是否在跳表的区间,我们还需要用一个tail节点来指向跳表的最后一个元素。跳表还需要有最基本的插入、删除、查找操作,因此我们的跳表结构体暂时如下:
template <typename K, typename V>
class SkipList
{
public:
SkipList(int max_level);
// 插入一个键值对,如果已经存在该key更新value然后返回false,插入成功返回true
bool insert(const K &k, const V &v);
// 查找key是否存在,存在返回该节点指针
Node<K, V> *find(const K &k) const;
bool erase(const K &k);
int size() const;
private:
int generate_level() const;
private:
int _max_level{0}; // 跳表允许的最大level
int _cur_level{0}; // 跳表的当前最大level
int _size{0};
std::uniform_int_distribution<int> _int_distribute(0, 1);
mutable std::default_random_engine _default_random_engine;
Node<K, V> *_header{nullptr}; // 链表的头节点
Node<K, V> *_tail{nullptr}; // 链表的尾节点
};
跳表的初始化
在初始化跳表的时候,我们给定一格max_level来指定该跳表允许的最大层数。一般我们允许的跳表的最大层数k应该和跳表的元素个数n成对数关系,也就是k = log(n) + 1,只有这样才能达到log(n)的查找效率。因此我们在创建跳表的时候,需要预估该跳表大概需要存放多少元素。
template <typename K, typename V>
SkipList<K, V>::SkipList(int max_level) : _max_level(max_level)
{
// 初始化header,header不存放具体的数据,保证每次插入的时候都插入在header的右侧
_header = new Node<K, V> *[_max_level + 1];
memset(_header, 0, sizeof(sizeof(Node<K, V> *) * (_max_level + 1)));
}
跳表的插入
跳表的插入遵循以下原则:
- 如果key值已经存在,则只修改value并返回false
- 如果key不存在,则找到插入点,并新建一个跳表节点,然后插入该节点,返回true
插入步骤如下:
- 从node = header 开始查找
- 从i = _cur_level开始从前往后查找,因为_cur_level是当前的最高层,再往上都是空指针没有意义
- 如果nexts[i]->key等于要插入的key则更新value后返回false
- 如果nexts[i]->key小于要插入的key则node = node->nexts[i],也就是将node指向当前层的下一节点
- 如果nexts[i]->key大于要插入的key则将i--后重复第1、2步,也就是查找下一层,直到i=0的时候找到一个比要插入的key大的,要么找到链表末尾退出循环
- 将要插入的元素插入到node节点后面,因为根据以上步骤node要么就是header节点,要么node->key是小于要插入的key,而node->nexts[i]->key是大于要插入的key的
由于插入了一个新的节点,而新节点也有自己的level,假设其值为k,也就是有一个长度为k的nexts数组,我们需要像单链表插入一样将前面的元素指向新插入的元素,然后将新元素指向其前驱节点原来的next节点。那么这些nexts指针要指向谁,而谁又应该指向新的节点呢?根据跳表的定义,我们需要将相同level的前面k个指针指向新节点,然后将新节点的k个nexts指针指向以前节点的nexts节点。这里我们只需要找到nexts指针的前驱节点就可以了,因为前驱节点中存放了nexts信息。实际上,前驱节点就是每次我们将i--之前的那个node节点,我们只需要在遍历的时候将这些节点存放起来,然后再新建节点的时候将他们连接起来即可。那要存放几个这样的前驱节点呢?答案是_cur_level+1,因为我们需要从_cur_level层开始到第0层结束,而_cur_level的最大取值为_max_level,因此为了避免每次都要创建临时的数组来存放这个信息我们在跳表中维护一个长度为_max_level+1的_lasts数组,用来存放第i层中的最后一个前驱节点。
为什么node节点就是那一层的前驱节点呢?
假设开始的时候node = header,i=_cur_level,它的nexts[i]表示的是header中第i层的节点,如果该节点的key大于要插入的key,那么_last[i] = node,存放的是第i层中最后一个小于要插入key的节点,然后i--,此时开始索引下一层的节点。如此循环直到i=0。此时_lasts[i]存放的就是要插入节点的0~_cur_level层的前驱节点。
新节点的level
由于新节点的level取值范围为0~min(_cur_level+1,_max_level),因此需要根据以下情况来更改前驱节点和后续节点的指向:
- 如果level小于等于_cur_level,那么直接将新节点的nexts[i] = _lasts[i]->nexts[i]即可,也就是将第i层的节点指向第i层的后续节点,然后将_lasts[i]->nexts[i]指向新节点即可
- 如果level大于_cur_level,则level只能取_cur_level+1,然后除了第一步,还需要将header[_cur_level+1]指向指向新节点,新节点的_cur_level+1层指向nullptr
引入了_lasts的插入代码如下:
template <typename K, typename V>
bool SkipList<K, V>::insert(const K &k, const V &v)
{
Node<K, V> *cur_node = _header;
int i = _cur_level;
memset(_lasts, 0, sizeof(sizeof(Node<K, V> *) * (_max_level + 1)));
while (i >= 0)
{
while (cur_node->nexts[i] != nullptr)
{
if (cur_node->nexts[i]->key < k)
{
cur_node = cur_node->nexts[i];
}
else if (cur_node->nexts[i]->key == k)
{
cur_node->nexts[i]->value = v;
return false;
}
}
_lasts[i] = cur_node; // 保存第i层的最后一个比k小的元素
i--;
}
int node_level = generate_level();
// 如果生成的节点的level大于当前的level,只新增一层
// 这里我们把它保存在_lasts中后续统一处理
if (node_level > _cur_level)
{
node_level = ++_cur_level;
_lasts[node_level] = _header;
}
Node<K, V> *node = new Node<K, V>(k, v, node_level);
// 将node插入到_lasts[i]后面
for (int i = 0; i <= node_level; ++i)
{
node->nexts[i] = _lasts[i]->nexts[i]; // 将新建节点的forward节点指向前驱节点的后续节点
_lasts[i]->nexts[i] = node;
}
// 更新tail节点
if (node->nexts[0] == nullptr)
{
_tail = node;
}
_size++;
return true;
}
生成level的代码:
template <typename K, typename V>
int SkipList<K, V>::generate_level() const
{
int level = 0;
while (level < _max_level && _default_random_engine(_int_distribute) == 1) // 使用0、1随机生成器,为1则增加level
{
level++;
}
return level;
}
跳表的查找操作
由于已经建立好跳表,要查找一个key则只需要先从_cur_level层开始查找,步骤如下:
- 从ndoe = header,i = _cur_level层开始往后查找,如果nexts[_i]->key == key,返回nexts[i]即可
- 如果nexts[i]->key > key,则i--,开始找下一层
- 如果i == 0 ,并且 nexts[i] == nullptr 或者 nexts[i]->key > key,则表示跳表中不存在该Key
代码如下:
template <typename K, typename V>
Node<K, V> *SkipList<K, V>::find(const K &k)const
{
if (size() == 0)
{
return nullptr;
}
// 如果要找的元素小于第一个元素,或者大于最后一个元素,则超出范围,直接返回false
if (k < _header->nexts[0]->key || k > _tail->nexts[0]->key)
{
return false;
}
int i = _cur_level;
Node<K, V> *node = _header;
while (i >= 0)
{
while (node->nexts[i] != nullptr)
{
if (node->nexts[i]->key < k)
{
node = node->nexts[i];
}
else if (node->nexts[i]->key == k)
{
return node;
}
else if (i == 0 && node->nexts[i]->key > k) // 已经找到最底层,但是下一个元素已经大于要找的key了,说明不存在
{
return nullptr;
}
}
i--;
}
return nullptr;
}
跳表的删除操作
跳表的删除需要先找到要删除的节点,然后使用查找过程中的前驱节点将要删除节点的后续节点链接起来,实现和插入操作大致相同,代码如下:
template <typename K, typename V>
bool SkipList<K, V>::erase(const K &k)
{
if (size() == 0)
{
return false;
}
// 如果要找的元素小于第一个元素,或者大于最后一个元素,则超出范围,直接返回false
if (k < _header->nexts[0]->key || k > _tail->nexts[0]->key)
{
return false;
}
int i = _cur_level;
Node<K, V> *node = _header;
memset(_lasts, 0, sizeof(Node<K, V> *) * (_max_level + 1));
while (i >= 0)
{
while (node->nexts[i] != nullptr)
{
if (node->nexts[i]->key < k)
{
node = node->nexts[i];
}
else if (node->nexts[i]->key == k)
{
break;
}
else if (i == 0 && node->nexts[i]->key > k) // 已经找到最底层,但是下一个元素已经大于要找的key了,说明不存在
{
return false;
}
}
_lasts[i] = node;
i--;
}
// 按照逻辑,永远不会走这个分支。在进入while循环前k一定是在现有区间中的
assert(i >= 0);
assert(node->nexts[i] != nullptr);
Node<K, V> *to_delete = node->nexts[i];
// 将要删除节点的前驱节点的forward[i]指向要删除的后继节点,只需要处理to_delete->level层
for (int i = 0; i <= to_delete->level; ++i)
{
_lasts[i]->nexts[i] = to_delete->nexts[i];
}
// 如果要删除的节点是tail节点
if (to_delete->nexts[0] == nullptr)
{
_tail = _lasts[0];
}
_size--;
delete to_delete;
return true;
}
完整代码
说明:这里的代码是个人为了学习跳表结构写的练习代码,不保证代码没有逻辑上的错误,如果使用该代码造成的不良后果和本人没有任何关系。
#pragma once
#include <random>
#include <cassert>
#include <cstring>
template <typename K, typename V>
struct Node
{
K key;
V value;
int level;
Node **nexts;
Node() = default;
Node(const K &k, const V &v, int lv) : key(k), value(v), level(lv)
{
nexts = new Node<K, V> *[level + 1];
memset(nexts, 0, sizeof(sizeof(Node<K, V> *) * (level + 1)));
}
~Node()
{
delete[] nexts;
}
};
template <typename K, typename V>
class SkipList
{
public:
SkipList(int max_level);
// 插入一个键值对,如果已经存在该key更新value然后返回false,插入成功返回true
bool insert(const K &k, const V &v);
// 查找key是否存在,存在返回该节点指针
Node<K, V> *find(const K &k)const;
bool erase(const K &k);
int size() const;
private:
int generate_level() const;
private:
int _max_level{20}; // 跳表允许的最大level
int _cur_level{0}; // 跳表的当前最大level
int _size{0};
std::uniform_int_distribution<int> _int_distribute(0, 1);
mutable std::default_random_engine _default_random_engine;
Node<K, V> *_header{nullptr}; // 链表的头节点
Node<K, V> *_tail{nullptr}; // 链表的尾节点
Node<K, V> *_lasts{nullptr}; // 用于遍历链表的时候存放每一层的最后一个节点
};
template <typename K, typename V>
bool SkipList<K, V>::erase(const K &k)
{
if (size() == 0)
{
return false;
}
// 如果要找的元素小于第一个元素,或者大于最后一个元素,则超出范围,直接返回false
if (k < _header->nexts[0]->key || k > _tail->nexts[0]->key)
{
return false;
}
int i = _cur_level;
Node<K, V> *node = _header;
memset(_lasts, 0, sizeof(Node<K, V> *) * (_max_level + 1));
while (i >= 0)
{
while (node->nexts[i] != nullptr)
{
if (node->nexts[i]->key < k)
{
node = node->nexts[i];
}
else if (node->nexts[i]->key == k)
{
break;
}
else if (i == 0 && node->nexts[i]->key > k) // 已经找到最底层,但是下一个元素已经大于要找的key了,说明不存在
{
return false;
}
}
_lasts[i] = node;
i--;
}
// 按照逻辑,永远不会走这个分支。在进入while循环前k一定是在现有区间中的
assert(i >= 0);
assert(node->nexts[i] != nullptr);
Node<K, V> *to_delete = node->nexts[i];
// 将要删除节点的前驱节点的forward[i]指向要删除的后继节点,只需要处理to_delete->level层
for (int i = 0; i <= to_delete->level; ++i)
{
_lasts[i]->nexts[i] = to_delete->nexts[i];
}
// 如果要删除的节点是tail节点
if (to_delete->nexts[0] == nullptr)
{
_tail = _lasts[0];
}
_size--;
delete to_delete;
return true;
}
template <typename K, typename V>
Node<K, V> *SkipList<K, V>::find(const K &k)const
{
if (size() == 0)
{
return nullptr;
}
// 如果要找的元素小于第一个元素,或者大于最后一个元素,则超出范围,直接返回false
if (k < _header->nexts[0]->key || k > _tail->nexts[0]->key)
{
return false;
}
int i = _cur_level;
Node<K, V> *node = _header;
while (i >= 0)
{
while (node->nexts[i] != nullptr)
{
if (node->nexts[i]->key < k)
{
node = node->nexts[i];
}
else if (node->nexts[i]->key == k)
{
return node;
}
else if (i == 0 && node->nexts[i]->key > k) // 已经找到最底层,但是下一个元素已经大于要找的key了,说明不存在
{
return nullptr;
}
}
i--;
}
return nullptr;
}
template <typename K, typename V>
SkipList<K, V>::SkipList(int max_level) : _max_level(max_level)
{
// 初始化header,header不存放具体的数据,保证每次插入的时候都插入在header的右侧
_header = new Node<K, V> *p[_max_level + 1];
memset(_header, 0, sizeof(sizeof(Node<K, V> *) * (_max_level + 1)));
// 初始化lasts,lasts也不存放具体的数据,用于每次遍历的时候存放当前层的最后一个节点
_lasts = new Node<K, V> *p[_max_level + 1];
memset(_lasts, 0, sizeof(sizeof(Node<K, V> *) * (_max_level + 1)));
}
template <typename K, typename V>
bool SkipList<K, V>::insert(const K &k, const V &v)
{
Node<K, V> *cur_node = _header;
int i = _cur_level;
memset(_lasts, 0, sizeof(sizeof(Node<K, V> *) * (_max_level + 1)));
while (i >= 0)
{
while (cur_node->nexts[i] != nullptr)
{
if (cur_node->nexts[i]->key < k)
{
cur_node = cur_node->nexts[i];
}
else if (cur_node->nexts[i]->key == k)
{
cur_node->nexts[i]->value = v;
return false;
}
}
_lasts[i] = cur_node; // 保存第i层的最后一个比k小的元素
i--;
}
int node_level = generate_level();
// 如果生成的节点的level大于当前的level,只新增一层
// 这里我们把它保存在_lasts中后续统一处理
if (node_level > _cur_level)
{
node_level = ++_cur_level;
_lasts[node_level] = _header;
}
Node<K, V> *node = new Node<K, V>(k, v, node_level);
// 将node插入到_lasts[i]后面
for (int i = 0; i <= node_level; ++i)
{
node->nexts[i] = _lasts[i]->nexts[i]; // 将新建节点的forward节点指向前驱节点的后续节点
_lasts[i]->nexts[i] = node;
}
// 更新tail节点
if (node->nexts[0] == nullptr)
{
_tail = node;
}
_size++;
return true;
}
template <typename K, typename V>
int SkipList<K, V>::generate_level() const
{
int level = 0;
while (level < _max_level && _default_random_engine(_int_distribute) == 1) // 使用0、1随机生成器,为1则增加level
{
level++;
}
return level;
}