【C++从0到王者】第五十二站:跳表

文章目录

一、什么是跳表

skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。

skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。
William Pugh开始的优化思路:

  1. 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。

  2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。

  3. skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。

  1. skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,这样就好处理多了。细节过程入下图:

比如在下图的第三个跳表中,如果我们想要查找19的话是这样进行的

  1. 比9大,向右走,跳跃到9
  2. 比21小,向下走
  3. 比17大,向右走,跳跃到17
  4. 比21小,向下走
  5. 根19相等,找到了

如果我们采用每个节点的高度是随机的,那么这样的话,每个节点插入和删除就跟其他节点没有关系了,都是独立的,不需要调整其他节点的层数了

二、skiplist的效率

这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

在Redis的skiplist实现中,这两个参数的取值为:

cpp 复制代码
p = 1/4
maxLevel = 32

根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析

如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。

  • 节点层数恰好等于1的概率为1-p。

  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。

  • 节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2*(1-p)。

  • 节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3*(1-p)。

    ......

  • 因此,一个节点的平均层数(也即包含的平均指针数目),计算如下

  • 现在很容易计算出:

  • 当p=1/2时,每个节点所包含的平均指针数目为2;

  • 当p=1/4时,每个节点所包含的平均指针数目为1.33。

  • 跳表的平均时间复杂度为O(logN)

三、skiplist的实现

这里我们使用这道题目来进行实现

跳表

我们的完整代码为

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
struct SkiplistNode //跳表节点
{
    int _val;       //该节点所存储的值
    vector<SkiplistNode*> _nextV; //表明该节点所指向的下面的节点的指针。因为跳表会有多个指针,这个数量是不确定的,所以我们使用一个vector
    SkiplistNode(int val, int level) //一个跳表节点被创建出来以后,需要它的值和该节点的层数,这是它最关键的两个信息
        :_val(val)
        , _nextV(level, nullptr) //这里姑且先将新开的一个跳表节点的所有指针全部置空,后序在进行处理
    {}
};
class Skiplist {
    typedef SkiplistNode Node;
public:
    Skiplist() {
        srand(time(nullptr)); //因为跳表节点的层数是随机的,所以我们一定会用到rand函数,所以就要生成随机数种子,而它只需要调用一次,所以我们不妨直接在构造函数里面去调用
        //头节点,层数是1
        _head = new SkiplistNode(-1, 1); //当我们的跳表生成以后,我们让跳表姑且只有一个节点,并且这个节点不存储任何有效值,且其层数为1
    }
    //查找一个目标值是否在跳表中,如果存在,则返回true
    bool search(int target) {
        Node* cur = _head; //从头节点开始一直往下去遍历
        int level = _head->_nextV.size() - 1; //head的最高层数,其实也就是我们整个跳表的最高层数已经被确定了
        //因为寻找逻辑是向右和像下去跑的。如果向右去跑,一定是target太大了导致的,这时候一定会导致的是最终cur->_nextV[level]为nullptr。
        //此时跟据我们内部的逻辑也会向下走。最终level一定会降低到-1,此时就是没有找到了
        //如果原来的值太小,那么一定是一直往下跳,最终level也会降低到-1
        while (level >= 0)  
        {
            //cur的第level层所指向的那个结点的val小于目标结点
            //注意,这里cur的第level层可能指向空,但是右边可能还有结点,所以我们也需要让它向下移动
            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;
    }

    //这个函数的功能是,寻找指向num节点的所有指针。即前面的指向它的节点我们都会通过这个函数最终找到,返回一个vector,这个vector就是按照层去排好的
    vector<Node*> FindPrevNode(int num)
    {
        //需要知道插入位置每一层的前一个结点指针。
        Node* cur = _head;
        int level = _head->_nextV.size() - 1; //先算出当前最大层数
        //我们要将每一层的前一个节点指针放入prevV中,注意level这个其实是下标,我们这里要是个数,所以要+1,并且它的初始时刻一定为_head。
        //prevV的数量为_head的层数的原因是,_head一定是当前跳表中层数最大的节点之一,即便后序num的比_head的的层数要高,我们后序可以通过resize去再次拔高_head
        //而初始时刻设置为_head的原因是,任何一个节点,如果它的层数
        //如果它和_head之间某一层没有相隔的节点,那么它此时的该层的上一个节点就是_head,而我们并不知道我们要找的num有几层(因为还没有定下来),所以我们可以直接将全部值设置为_head
        //然后如果它的prevV[level]不是_head了,那么直接覆盖即可。
        vector<Node*> prevV(level + 1, _head);
        //num存在的位置一定是要比cur的后面节点小于等于,但是又比cur节点处的位置大的值。
        while (level >= 0)
        {
            //目标值比下一个节点值要大,向右走
            if (cur->_nextV[level] && cur->_nextV[level]->_val < num)
            {
                //cur向右走
                cur = cur->_nextV[level];
            }
            //比num小于等于cur处,就可以更新它的前一个节点了,就是cur,然后我们这一层就找好了,去找下一层了。
            else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num)
            {
                //更新前一个结点    
                //如果等于nullptr,那么其实该处已经映射到头了,只要num是够高的,那么该节点就是指向num的。对于num小于等于,也是一样的道理。说明num就存在于该处
                //他的节点一定不会收到后面的影响了。所以只需要将前面所有节点的投影给拿出来即可
                prevV[level] = cur; 

                level--;
            }
        }
        return prevV;
    }

    void add(int num) 
    {
        //num将要插入位置的每一层的上一个节点指针数组
        vector<Node*> prevV = FindPrevNode(num);

        int n = RandomLevel(); //随机生成一个层数
        Node* newnode = new Node(num, n); //创建好新的跳表节点
        if (n > _head->_nextV.size())//如果新的层数已经超出了原有的层数,那么_head需要拔高,且prevV里面的数据也要拔高
        {
            _head->_nextV.resize(n, nullptr);
            prevV.resize(n, _head);
        }
        //连接前后节点
        for (int i = 0; i < n; i++)
        {
            newnode->_nextV[i] = prevV[i]->_nextV[i];
            prevV[i]->_nextV[i] = newnode;
        }
    }

    bool erase(int num) 
    {
        //找到num对应的上一个节点指针数组
        vector<Node*> prevV = FindPrevNode(num);
        //最底层的下一个不是val,没有这个节点
        if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)
        {
            return false;
        }
        else
        {
            //要删除的节点就是最底层的节点指向的下一个节点
            Node* del = prevV[0]->_nextV[0];
            //del每一层的前后指针进行连接
            for (size_t i = 0; i < del->_nextV.size(); i++)
            {
                prevV[i]->_nextV[i] = del->_nextV[i];
            }
            delete del;
            
            //如果删除了最高层的节点,降低一下头节点的层数
            int i = _head->_nextV.size() - 1;
            while (i > 0) //注意,起码要给这个节点留上一层。
            {
                if (_head->_nextV[i] == nullptr)
                {
                    --i; //寻找_head的高度
                }
                else
                {
                    break;
                }
            }
            _head->_nextV.resize(i + 1); //降低_head的高度

            return true;
        }
    }
    //通过概率去控制层数的函数
    int RandomLevel()
    {
        size_t level = 1;
        while (rand() < RAND_MAX * _p && level < _maxLevel)
        {
            ++level;
        }
        return level;
    }


    //方便我们去观察跳表,去打印跳表
    void Print()
    {
        //int level = _head->_nextV.size();
        //for (int i = level - 1; i >= 0; i--)
        //{
        //    Node* cur = _head;
        //    while (cur)
        //    {
        //        printf("%d->", cur->_val);
        //        cur = cur->_nextV[i];
        //    }
        //    cout << endl;
        //}
        Node* cur = _head;
        while (cur)
        {
            for (auto e : cur->_nextV)
            {
                printf("%2d", cur->_val);
            }
            cout << endl;
            // 打印每个每个cur节点
            for (auto e : cur->_nextV)
            {
                printf("%2s", "↓");
            }
            printf("\n");

            cur = cur->_nextV[0];
        }



    }
private:
    Node* _head; //跳表的第一个节点指针,即头节点,不存储有效数据
    size_t _maxLevel = 32; //最高的层数
    double _p = 0.5; //一层的概率
};

int main()
{
    Skiplist sl;
    sl.Print();
    cout << "-------------------" << endl;

    int a[] = { 5,2,3,8,9,6 };
    for (auto e : a)
    {
        sl.add(e);
        sl.Print();
        cout << "-------------------" << endl;
    }
    for (auto e : a)
    {
        sl.erase(e);
        sl.Print();
        cout << "-------------------" << endl;
    }

    return 0;
}


/**
 * Your Skiplist object will be instantiated and called as such:
 * Skiplist* obj = new Skiplist();
 * bool param_1 = obj->search(target);
 * obj->add(num);
 * bool param_3 = obj->erase(num);
 */
//int main()
//{
//    Skiplist sl;
//    int max = 0;
//    for (size_t i = 0; i < 1000000000; i++)
//    {
//        int r = sl.RandomLevel();
//        if (max < r)
//        {
//            max = r;
//        }
//    }
//    cout << max << endl;
//    
//	return 0;
//}

在力扣上是可以通过测试用例的。

相关推荐
0白露1 小时前
Apifox Helper 与 Swagger3 区别
开发语言
Tanecious.2 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐2 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
想跑步的小弱鸡2 小时前
Leetcode hot 100(day 3)
算法·leetcode·职场和发展
战族狼魂2 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
Tttian6223 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
xyliiiiiL3 小时前
ZGC初步了解
java·jvm·算法
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
爱的叹息4 小时前
RedisTemplate 的 6 个可配置序列化器属性对比
算法·哈希算法
Merokes4 小时前
关于Gstreamer+MPP硬件加速推流问题:视频输入video0被占用
c++·音视频·rk3588