1、 跳表--skiplist
skiplist本质上是一种查找结构,跟平衡搜索树和哈希表的价值是一样的。跳表首先是一个链表,它是在链表的基础上发展的。但一般的链表进行查找数据只能全部遍历,时间复杂度为O(n)。
William Pugh的优化:
- 假如每相邻两个节点升高一层,增加一个指针,让该指针指向下下个节点。
所有新增加的指针连成了一个新的链表,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。
- 在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。查找效率可以进一步提升
- 按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)
但问题在于在插入或者删除时,如果严格遵守上述规则就需要把后续被影响的节点的指向全部修改,就又需要重新遍历一遍。时间复杂度又上升为O(n)。
- William Pugh做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数。
2、 随机的层数
一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:
节点层数恰好等于1的概率为1-p。
节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
节点层数大于等于3的概率为p^2,而节点层数恰好等于3的概率为p^2*(1-p)。
节点层数大于等于4的概率为p^3,而节点层数恰好等于4的概率为p^3*(1-p)。 ...
一个节点的平均层数计算结果为 1/(1-p)
跳表的平均时间复杂度为O(logN),推导过程可查询其他大佬。
3、跳表的模拟实现
准备工作
跳表节点的设想:首先跳表有一个层数,每一层都有存有一个指针指向下一个位置。我们以vector作为容器进行存储。在初始化列表阶段直接使用vector的构造函数。
struct SkiplistNode
{
vector<SkiplistNode*> _nextV;
int _val;
SkiplistNode(int val,int level)
:_val(val)
,_nextV(level,nullptr)
{}
};
综上,我们创建一个节点都需要一个随机数来充当层数,在设计跳表时,要注意设置最大层数_maxlevel和概率_p
class Skiplist
{
typedef SkiplistNode Node;
public:
Skiplist()
{
_head=new Node(-1,1);//头结点的值设为-1 层数为1
//也可以不是1 直接设为最大层数
}
//开始画饼
int Randomlevel()
{}
bool search(int target)
{}
void add(int num)
{}
bool erase(int num)
{}
private:
double _p=0.25;
size_t _maxlevel=32;//2^32次方是 unsigned int能存下的最大的数
Node* _head;//跳表需要一个头结点
};
3.2 随机函数
C++11新增有库可以实现随机值,但比较难记。
int Randomlevel()
{
static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
size_t level = 1;
while (distribution(generator) <= _p && level < _maxLevel)
{
++level;
}
return level;
}
C语言相比就比较简单,C语言的rand()函数会有一个最大值,是用宏定义的 RAND_MAX
int Randomlevel()
{
size_t level=1;
while(rand()<= RAND_MAX*_p && level<_maxlevel)
{
level++;
}
return level;
}
3.3 查
bool search(int target)
{
Node*cur=_head;
int level=_head->_nextV.size()-1;
//我们从最高层的下一个指向开始找 这样找的快
while(level>=0) //是有第0层的
{
//我比你大 那就直接横着跨走
//注意当下一层是nullptr时 在访问_val就报错了
if(cur->_nextV[level] && cur->_nextV[level]->_val < target)
{
cur=cur->_nextV[level];
}
//我比你小 那就往下走一层
//如果横跨已经是空了 那也得往下走一层
else if(cur->_nextV[level]==nullptr || cur->_nextV[level]->_val > target)
{
level--;
}
else
{
return true;
}
}
return false;
}
3.4 增
如果我们要新增一个节点,首先需要的就是知道插入节点的前后节点,以便将这些节点相互链接起来。
此时prevV中就存放了 所有前一个指针。
当我们随机好了新节点的层数时,可以从最底层开始逐个链接,直至到达了新节点的层数。(如果新节点的层数超过了根节点的层数,根节点的层数需要更新)
要实现add 需要先实现确定prevV的函数
vector<Node*> FindPrevNode(int num)
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)
{
//先更新prevV
prevV[level]=cur;
--level;
}
}
return prevV;
}
void add(int num)
void add(int num)
{
vector<Node*> prevV=FindPrevNode(num);
int n=Randomlevel();
Node* newnode=new Node(num,n);
if(n>_head->_nextV.size())
{
//头结点层数变高 新增层数直接指向nullptr
_head->_nextV.resize(n,nullptr);
//prevV更新 新增层数的前一个指向_head
prevV.resize(n,_head);
}
//链接前后节点
for(int i=0;i<n;i++)
{
newnode->_nextV[i]=prevV[i]->_nextV[i];
prevV[i]->_nextV[i]=newnode;
}
}
3.5 删
删除同样是需要拿到prevV数组 并修改指针的指向 最后Delete掉当前节点
bool erase(int num)
{
vector<Node*> prevV=FindPrevNode(num);
//查看一下在不在该跳表
//一定要注意判断是否为空 访问空指针是会出问题的
if(prevV[0]->_nextV[0]==nullptr || prevV[0]->_nextV[0]->_val !=num)
{
return false;
}
//保存要删除的节点
Node* cur=prevV[0]->_nextV[0];
for(int i=0;i<cur->_nextV.size();i++)
{
prevV[i]->_nextV[i]=cur->_nextV[i];
}
delete cur;
return true;
}
3.6 测试代码及结果
由于跳表的打印要想打出图片的结果比较复杂 这里不再给出打印函数。可通过leetcode 题目编号1206.设计跳表进行判断。
力扣