如果不想看我唠我的心得的话,请直接下面看代码。
刚学了跳表这个数据结构,相信很多人跟我一样,在刚开始看了跳表的介绍后会觉得这种数据结构很好了解,而且也会在心里想“好像实现起来应该也不难”,我带着这样的心情开始手动实现跳表,结果…整整花了我两天多的时间才搞了出来,所以想写一篇博客来记录一下我实现跳表这个过程中遇到的问题以及我如何解决遇到的这些问题。废话不多说,开搞。
在刚开始我学了跳表后,我没有去看别人的实现代码,想着自己做出来,然后就照着下面这张图去思考。
相信很多人在学习跳表的时候都会看到与这张类似的图,但是我想说,如果你是按照这张图去实现跳表的,那么你非常有可能会实现出不来,或者实现起来很麻烦(这只是我的个人心得,不排除是我太蠢了)。
我刚开始的想法就是建立一条数据链表(图中的原始链表)和根据需要建立索引链表,也就是两种不同的链表,这就是我刚开始看到这张图的时候马上就产生的想法,按照这种想法去做,你会发现非常麻烦,主要就是索引链表的实现,这里我就不说了,你可以自己思考一下。我就在这样的想法下过了一整个下午,然后还是没解决跳表这个类的基础框架。然后就一直在思考解决方案,最后在吃晚饭的时候想出来了。
我发现我就是被上面那个图给误导了(当然我不是说那个图不好/不对,只是对于实现来说这个图不太行),因为我们建立索引实际要的就只是对应那个结点的key,也就是用来比较的那个值,而这个值也就在原始的数据链表中,那么我们的__索引链表的结点只需要存储指向结点的指针__,并且这样的实现方式可以直接在数据链表的结点上进行改造来实现。如果能够想到这里的话,那么实现起来就变得非常简单了。
因为怕有对跳表不太了解的人看这篇文章,所以首先说明一下,在链表插入元素的时候是通过随机数来决定插入的这个结点拥有多少级索引的,所以不可能在插入节点后能保证跳表还能够满足像图示一样每两个结点(或索引)就有新的一层索引。
下面是我根据这个想法重新画的一个图(比较简陋),如果要实现跳表的话,我觉得这张图更合适(对我来说是这样,别人就不知道了)
接下来就直接看我的代码吧。
#include<iostream>
#include<vector>
#include<random>
#include<cmath>
//索引的最高层级
#define MaxLevel 15
class SkipList;
class SNode
{
friend class SkipList;
typedef int ValueType;
public:
SNode() :data(), maxIdeNode(0) { indexs.push_back(nullptr); }
SNode(const ValueType &d) :data(d), maxIdeNode(0) { indexs.push_back(nullptr); }
ValueType get_val()const { return data; }
private:
ValueType data;
std::size_t maxIdeNode; //当前节点的最高索引级别
//indexs存储的是索引,indexs[0]指向的是下一个数据结点(即充当next指针)
std::vector<SNode*> indexs;
};
class SkipList
{
friend void print_sl(const SkipList& sl);
public:
SkipList();
SkipList(const SkipList&) = delete;
SkipList &operator=(const SkipList&) = delete;
//插入结点——保持有序
void insert(const SNode::ValueType &val);
//删除结点
void erase(const SNode::ValueType &val);
//查找
SNode *search(const SNode::ValueType &val) const;
~SkipList();
private:
//找到某一层最后一个小于val的数据结点的指针
SNode *find_final_less(const int &lev, const SNode::ValueType &val);
private:
/*
*索引链表的解释:
*其实只是从形式上看来是链表而已,在代码里实现的时候并不需要写成链表的形式
*因为索引只是一个数据结点的指针而已,它甚至都不需要像数据结点一样在插入新元素的时候需要额外分配空间:
*因为它是指向原来就已经存在的数据结点,这个结点在插入的时候是在数据链表上插入的,插入索引只是插入一个指向这个新元素的指针
*/
//指向各级索引的头结点的指针(pid_head[0]就为指向数据链表的头结点的指针)
std::vector<SNode*> pid_head;
std::size_t sz;
std::size_t maxIdeList; //当前链表的最大索引级别
static std::default_random_engine rand_e; //随机数引擎
};
std::default_random_engine SkipList::rand_e;
//pid_head[0]为数据链表,只有原始数据链表需要为结点分配新空间,其它链表头指针都指向pid_head[0]所指向的空间,也就是原始数据链表的头结点。
SkipList::SkipList() :pid_head(MaxLevel + 1, nullptr), sz(0), maxIdeList(0) {
pid_head[0] = new SNode();
for (int i = 1; i < pid_head.size(); ++i)
pid_head[i] = pid_head[0];
//把头结点的indexs的空间都先申请好,方便后面使用
pid_head[0]->indexs.resize(MaxLevel + 1, nullptr);
}
//插入结点——保持有序
void SkipList::insert(const SNode::ValueType &val) {
//查找插入位置
SNode *dpos = find_final_less(0,val);
SNode *pn = new SNode(val);
//-------------------------处理数据链表层-------------------------------------
//插入第一个结点或者在头结点之后插入结点
if (dpos == pid_head[0]) {
//在第一个数据结点之前插入新元素
if (sz) {
//让新元素的indexs承接原先第一个结点的indexs,将原先第一个结点的indexs清空(这个操作除了indexs[0]之外)
//这个操作我觉得应该改善的,将原先的结点的indexs清空,我觉得不太合理,但是目前也没想出什么好的方案
for (int i = 1; i <= maxIdeList; ++i) {
pn->indexs.push_back(dpos->indexs[i]->indexs[i]);
dpos->indexs[i]->indexs[i] = nullptr;
--(dpos->indexs[i]->maxIdeNode);
}
pn->indexs[0] = dpos->indexs[0];
}
//让头结点的indexs都指向第一个数据结点,方便插入操作(其实就是类似哨兵结点)
for (int i = 0; i <= MaxLevel; ++i) pid_head[0]->indexs[i] = pn;
++sz;
return;
}
else {
pn->indexs[0] = dpos->indexs[0];
dpos->indexs[0] = pn;
++sz;
}
//-----------------------------------处理索引层----------------------------------------------
//判断当前索引层级是否足以容纳所有数据结点,以此得出随机数的最大值
//索引的不同取法就会有不同的计算方式,我这里是跟上面的图一样,每两个点取一个索引,计算方法在下面有解释
int rn = (std::pow(2, maxIdeList + 1) < sz) ? maxIdeList + 1 : maxIdeList;
//如果需要增加层级,那么需要先将第一个数据结点的indexs先增大,因为每一层的索引必定是从第一个数据结点开始的
if (rn == maxIdeList + 1) {
pid_head[0]->indexs[0]->indexs.push_back(nullptr);
++maxIdeList;
}
if(rn){ //如果最高层不为0(最高层为0即没有索引层,不需要处理)
std::uniform_int_distribution<unsigned> u(0, rn);
if (rn = u(rand_e)) {
pn->maxIdeNode = rn;
while (pn->indexs.size() <= rn) pn->indexs.push_back(nullptr);
//indexs[0]不需要处理,前面已经处理了(数据链表层)
for (int i = rn; i > 0; --i) {
//查找索引的插入位置
SNode *pre_pos = find_final_less(i, val);
pn->indexs[i] = pre_pos->indexs[i];
pre_pos->indexs[i] = pn;
}
}
}
}
//删除结点
void SkipList::erase(const SNode::ValueType &val) {
if (sz == 0) return;
//找到数据结点的各级索引的前驱结点,并将其前驱结点的indexs指向待删除结点中indexs所对应的指针指向的地方
SNode *pre = pid_head[0];
for (int i = maxIdeList; i >= 0; --i) {
while (pre->indexs[i]) {
if (pre->indexs[i]->data == val) {
SNode *pdel = pre->indexs[i];
pre->indexs[i] = pdel->indexs[i];
if (i == 0) delete pdel;
else pdel->indexs[i] = nullptr;
--(pre->indexs[i]->maxIdeNode);
break;
}
else if (pre->indexs[i]->data < val) {
pre = pre->indexs[i];
}
else break;
}
}
}
//查找
SNode *SkipList::search(const SNode::ValueType &val) const
{
if (sz == 0) return nullptr;
SNode *pos = pid_head[0]->indexs[0];
for (int i = maxIdeList; i >= 0; --i) {
while (pos) {
if (pos->data == val) return pos;
else if (pos->data < val) {
if (!pos->indexs[i]) break; //如果没有下一个索引,直接走下一级索引查找
else {
//注意是 <= 不是 < :当下一个结点的值等于val时,要让pos指向下一个结点
if (pos->indexs[i]->data <= val) pos = pos->indexs[i];
else break; //如果下一个索引指向的值比val大,直接走下一级索引查找
}
}
}
}
return nullptr;
}
SkipList::~SkipList() {
SNode *pdel = pid_head[0];
while (pdel->indexs[0]) {
//我的思路:不要使用erase,因为那样做太慢,需要结点的indexs
//直接以删除单链表的形式来进行析构,因为内存也只分配到数据链表的结点中
SNode *p = pdel;
pdel = pdel->indexs[0];
delete p;
}
delete pdel;
}
//找到指定层级最后一个小于val的数据结点的指针
SNode *SkipList::find_final_less(const int &lev, const SNode::ValueType &val) {
if (lev < 0 || lev > MaxLevel || lev >maxIdeList) return nullptr;
SNode *pre = pid_head[0];
if (sz == 0) return pre;
for (int i = maxIdeList; i >= lev; --i) {
while (pre->indexs[i]) {
if (pre->indexs[i]->data == val) return pre;
else if (pre->indexs[i]->data < val) pre = pre->indexs[i];
else break;
}
if (i == lev && (pre->indexs[i] == nullptr || pre->indexs[i]->data > val)) return pre;
}
return nullptr;
}
当前索引层数所能表示的元素个数的解释:建立新的一层索引需要有一定的数据个数才可以建立,比方说在我跳表的规则中(每两个点取一个索引),如果要建立第一层索引,那么必须至少要有三个节点,如果要建立第二层索引那么至少需要5个结点,其它依此类推。
用n表示索引层数,N表示数据个数,那么2n + 1 <= N <= 2(n+1)
如果有发现什么问题的话,还望大神斧正,或者有什么建议的话也可以留言跟我说。