前几天在看Redis的时候,看到网上说,Redis采用的是 Skiplist 而不是红黑树,但当时就给我整懵了。
啥子东西叫Skiplist,一查,翻译还挺直白——跳表。看起来很高级啊,于是嗖嗖嗖马上查资料
0X 10 Skiplist的设计初衷
在看到Skiplist庐山真面目之前,我们先来了解它是怎么出现的,或者说为何要出现
链表想必大家都熟悉,一般来讲,它长这样:
在这里插入图片描述
上面是一个有序链表,我们知道链表有一个好处就是在删除和插入的时候很方便。
但是,美中不足的就是,查询是它的一个软肋。
因为我们在链表中查找,只能从头开始一个一个往下找,所以一般来讲,链表的查找效率是 O(n)
所以基于这一点,有大神就开始打算盘了。如何很将链表的便捷与数组的查找优势(说的是有序数组下的二分查找)结合起来?
于是,于是,神奇的跳表就出来了(大神不愧是大神,分分钟出来:) )
0X 20 Skiplist的真面目
上面的想法很好啊,但是是不是很难实现呢?
只能说只有我们想不到,没有大神做不到
跳表跳表,最主要的就是在一个跳字上面,那么靠什么来跳呢——索引,说白了就是指针
好嘛,也是以空间换效率(要不然捏,大神可不是真神仙,能凭空变出来)
好了,铺垫差不多了,也该“千呼万唤始出来”了
上图!
在这里插入图片描述
(是不是看起来很眼熟,没错,没准你们数据结构老师上课讲过!)
看起来挺古怪的啊,不过人家搜索起来可是杠杠的
来个例子
假设我们需要查找25,如果是一般的有序链表我们需要比较几次呢?没做,9次,25前面的全要遍历一遍
那么采用跳表呢,怎么搜索,又要多少次呢?
敲黑板,重点来了!
来我们开始
1、将当前指针curr指向头结点,然后查看最上面的指针指向的节点——21,第一次比较,21比25小,于是当前curr指向21这个节点
2、故技重施,再看21的第一个最上面的指针,指向NULL,说明到头了,不行,第二次比较
3、这时候,往下移一层,还是指向NULL,再移,第三次比较
4、第二层(从下往上数)指向的是26这个节点,第四次比较,比25大,说明还是要往下走
5、到了最后一层,这时候是就是,不是就不是了,于是进行第5次比较,25找到了
怎么样,从开始的9次变成了5次,差不过快了一半了吧
0X 30 Skiplist的庐山真面目
刚才我们也看到了,跳表还是很有用的。但是,其实刚才我们看到的跳表,并不是真正的跳表,为什么呢?
如果你认真观察的话,你会发现,上面的跳表有着严格的规定:上面一层的节点个数是下面一层个数的1/2
这样还是很有问题,比如当你插入一个节点的时候,那么结构不就破坏了?或者是又要用一定的办法来更改从而维持这样的结构。这样就和红黑树差不多了啊
大神的解决办法是什么呢?那就是不要”规定“,当然我们所的是不要上面那种严格的规定,而规则变成了一种随机
上下层不强制要求 1:2 ,一个节点要不要被索引,建几层的索引,都在节点插入时由抛硬币决定
当然,虽然索引的节点、索引的层数是随机的,为了保证搜索的效率,要大致保证每层的节点数目与上节的结构相当,因此随机出现的真正意义上的跳表是这样的:
在这里插入图片描述
发现比例已经不是严格的 1:2 了,但是总体来看还是比较规整的
下面是节点 17 插入的过程在这里插入图片描述
性能分析就不做啦(因为我不会2333),但是需要记住的是,跳表的 插入/删除/查找 都是 O(logn)
0X 30 硬核编码——Skiplist的C++实现
PS:以下代码99.99% 为copy(当然不是直接copy,是自己的一点点对着代码码上去的,然后加了点注释),但是希望大家能好好学习,自己敲一遍消化一下
可能敲的过程中有所纰漏,所以请大家多多留心!(后面会有代码出处链接)
skiplist.h
#ifndef _SKIPLSIT_H
#define _SKIPLSIT_H
#include
#include
template <typename K,typename V,int MAXLEVEL>
class skiplist_node // 跳表的节点类
{
private:
K key;
V value;
skiplist_node<K,V,MAXLEVEL>* forwards;
public:
skiplist_node()
{
forwards=new skiplist_node<K,V,MAXLEVEL> [MAXLEVEL];
for(int i=0;i<MAXLEVEL;i++)
forwards[i]=nullptr;
}
skiplist_node(K searchKey):key(searchKey)
{
for(int i=0;i<MAXLEVEL;i++)
forwards[i]=nullptr;
}
~skiplist_node()
{
delete [] forwards;
}
};
template<typename K,typename V,MAXLEVEL=16>
class skiplist //跳表类
{
protected:
K m_minKey; //最小键值
K m_maxKey; //最大键值
int man_curr_level; //当前最大层数
skiplist_node<K,V,MAXLEVEL>* m_pHeader; //头指针
skiplist_node<K,V,MAXLEVEL>* m_pTail; //尾指针
double uniformRandom(); //随机函数
int randomLevel(); //生成随机层数
public:
//别名
typedef K KeyType;
typedef V ValueType;
typedef skiplist_node<K,V,MAXLEVEL> NodeType;
const int max_level; //最大层数
skiplist(K minKey,K maxKey);
virtual ~skiplist_node();
void insert(K searchKey,V newValue); //插入
void erase(K searchKey); //删除
const NodeType* find(K searchKey); //查找
bool empty() const; //判断是否为空
std::string printList(); //打印
};
skiplist.cc
#include “stdlib.h”
double skiplist::uniformRandom() //随机函数
{
return rand()/double(RAND_MAX);
}
int skiplist::randomLevel() //生成随机层数
{
int level=1;
double p=0.5;
while(uniformRandom()<p && level < MAXLEVEL)
level++;
return level;
}
skiplist::skiplist(K minKey,K maxKey):m_pHeader(NULL),m_pTail(NULL), //构造函数
max_curr_level(1),max_level(MAXLEVEL),
m_minKey(minKey),m_maxKey(maxKey)
{
m_pHeader = new NdoeType(m_minKey);
m_pTail = new NodeType(m_maxKey);
for(int i=0;i<MAXLEVEL;i++)
m_pHeader->forwards[i]=m_pTail;
}
skiplist::~sikplist() //析构函数
{
NodeType* currNode =m_pHeader->forwards[0];
while(currNode != m_pTail)
{
NodeType* tempNode = currNode;
currNode = currNode->forwards[0];
delete tempNode;
}
delete m_pHeader;
delete m_pTail;
}
void skiplist::insert(K searchKey,V newValue) //插入函数
{
skiplist_node<K,V,MAXLEVEL>* update[MAXKLEVEL];
NodeType* currNode= m_pHear;
for(int level=max_curr_level-1; level>=0; level–) //这个循环用来查找待插入的位置
{ //若第level指向的小于 待查找(searchKey) 的值,则在本层一直往下走
while (currNdoe->forwards[level]->key<searchKey)
currNode = currNode->forwards[level];
//否则,到下一层
update[level] = currNode; //存储走过的路径
}
currNode = currNode->forwards[0]; //此时便是待插入的位置
if(currNode->key == searchKey) //若键值 key 相同
currNode->value = newValue; //更新值
else //否则进行插入
{
int newlevel = randmoLevel(); //或者随机层数
if(newlevel > max_curr_level) //大于当前最大层数,则需要更新
{
for(int level = max_curr_level; level < newlevel; level++)
upadte[level]=m_pHeader;
max_curr_level=newlevel;
}
currNode=new NodeType(searchKey,newValue);
//更新指针的指向,比新节点高的则指向updata后面的,比新节点低的则将updata指向新节点
for(int lv=0;lv<max_curr_level;lv++)
{
currNode->forwards[lv]=update[lv]->forwards[lv];
update[lv]->forwards[lv]=currNode;
}
}
}
void listskip::erase(K searchKey)
{
skiplist_node<K,V,MAXLEVEL>* update[MAXLEVEL];
NodeType* currNode = mpHeader;
for(int level=max_curr_level-1; level>=0; level–) //同上面的 insert
{
while(currNode->forwards[level]->key < searchKey)
currNode = currNode->forwards[level];
update[level]=currNode;
}
currNode = currNode->forwards[0]; //找到需要查找的位置
if(currNode->key == searchKey)//如果相等则说明存在需要删除
{
for(int lv=0;lv<max_curr_level;lv++) //更新指针指向
{
if(update[lv]->forwards[lv]!=currNode) break;
update[lv]->forwards[lv]=currNode->forwards[lv];
}
delete currNode;
// update the max level
while(max_curr_level > 0 && m_pHeader->forwards[max_curr_level]==nullptr)
max_curr_level--;
}
}
const NodeType* skiplist::find(K searchKey)
{
NodeType* currNode = m_pHeader;
for(int level=max_curr_level-1;level>=0;level–) //同上,但寻找无需存储走过的路径
while(currNode->forwards[level]->key < searchKey)
currNode = currNode->forwards[level];
currNode = currNode->forwards[0];
if(currNode->key==searchKey) //找到返回 找到的节点
return currNode;
else return nullptr; //否则返回空
}
bool skiplist::empty() const //判断是否为空
{
return (m_pHeader->forwards[0]==m_pTail);
}
std::string skiplist::printList() //打印
{
std::stringstream sstr;
NodeTpye* currNode = m_pHeader->forwards[0];
while(currNode != m_pTail)
{
sstr << “(” << currNode->key << “,” << currNode->value << “)” << endl;
currNode = currNode->forwards[0];
}
return sstr.str();
}
因为代码和上面图解有点差别,所以这里有几点需要说明:
1、代码存储的是<key,value>的形式,因此不单单是上述单个值。key是用来比较的
2、代码的头、尾节点存储的是特定的 min_value 和 max_value ,而不是图解中的 NULL,因此key需要有一个特定的范围