C++数据结构--跳表的思想--手把手教你实现跳表--0721

1、 跳表--skiplist

skiplist本质上是一种查找结构,跟平衡搜索树和哈希表的价值是一样的。跳表首先是一个链表,它是在链表的基础上发展的。但一般的链表进行查找数据只能全部遍历,时间复杂度为O(n)。

William Pugh的优化:

  • 假如每相邻两个节点升高一层,增加一个指针,让该指针指向下下个节点。

所有新增加的指针连成了一个新的链表,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。

  • 在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。查找效率可以进一步提升

  •  按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)

但问题在于在插入或者删除时,如果严格遵守上述规则就需要把后续被影响的节点的指向全部修改,就又需要重新遍历一遍。时间复杂度又上升为O(n)。

  • William Pugh做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数。

 2、 随机的层数

一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

C++数据结构--跳表的思想--手把手教你实现跳表--0721_第1张图片

节点层数恰好等于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 _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 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 增

如果我们要新增一个节点,首先需要的就是知道插入节点的前后节点,以便将这些节点相互链接起来。

C++数据结构--跳表的思想--手把手教你实现跳表--0721_第2张图片


 此时prevV中就存放了 所有前一个指针。

当我们随机好了新节点的层数时,可以从最底层开始逐个链接,直至到达了新节点的层数。(如果新节点的层数超过了根节点的层数,根节点的层数需要更新)

要实现add 需要先实现确定prevV的函数

vector FindPrevNode(int num)

vector FindPrevNode(int num)
{
    Node* cur=_head;
    int level=_head->_nextV.size()-1;//先从最高层开始找 
    vector 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 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_nextV[i]=prevV[i]->_nextV[i];
        prevV[i]->_nextV[i]=newnode;
    }

}

3.5 删

删除同样是需要拿到prevV数组 并修改指针的指向 最后Delete掉当前节点

bool erase(int num)
{
    vector 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_nextV.size();i++)
    {
        prevV[i]->_nextV[i]=cur->_nextV[i];
    }
    delete cur;
    return true;
}

3.6 测试代码及结果

由于跳表的打印要想打出图片的结果比较复杂 这里不再给出打印函数。可通过leetcode 题目编号1206.设计跳表进行判断。

力扣

C++数据结构--跳表的思想--手把手教你实现跳表--0721_第3张图片

 

你可能感兴趣的:(C++,数据结构,链表)