【C++高阶数据结构】跳表(skiplist)

🏆个人主页企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【C++高阶数据结构】并查集

【C++高阶数据结构】图

【C++高阶数据结构】LRU

【C++高阶数据结构】B树、B+树、B*树



💎一、概念

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

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

  3. 实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。在这里插入图片描述

  4. 不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数

在这里插入图片描述

一个结点到底应该给几层?

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

在这里插入图片描述

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

💎二、实现

设计跳表

在这里插入图片描述

img

在这里插入图片描述

思路:

  1. 首先设计节点,包含节点值和存储next指针
  2. 跳表设计,定义一个头节点,最大层数,生成层数概率
struct SkiplistNode
{
    int _val;
    //用来存next指针的vector
    vector<SkiplistNode*> _nextV;
    SkiplistNode(int val,int level)
        :_val(val)
        ,_nextV(level,nullptr)
    {}
};
class Skiplist {
    typedef SkiplistNode Node;
public:
    Skiplist() {
        srand(time(0));
        //头结点,层数是1
        _head=new Node(-1,1);
    }
    
    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)
			{
                //只有当需要转移到下一层的时候,我们才找到了当前层的前置结点
				// 更新level层前一个
				prevV[level] = cur;

				// 向下走
				--level;
			}
		}

		return prevV;
	}
    //查找数据
    bool search(int target) {
        Node* cur=_head;
        //往最高层走
        int level=_head->_nextV.size()-1;
        while(level>=0)
        {
            //目标值比下一个结点值要大,向右走
            //下一个节点是空(尾),目标值比下一个结点值要小,向下走
            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;
    }
    
    void add(int num) {
        vector<Node*> prev=FindPrevNode(num);
        //产生新节点的层数
        int n=RandomLevel();
        //数据是num,有n层的结点
        Node* newnode=new Node(num,n);
        //如果n超过了当前最大的层数,那就升高一下head的层数
        if(n>_head->_nextV.size())
        {
            //将头结点的next数组的大小开辟到我们当前的最大的层数大小,新的部分用nullptr填补
             _head->_nextV.resize(n,nullptr);
            //将我们的前置结点也同样开辟到n的大小
            prev.resize(n,_head);
        }
           

        //链接前后节点
        //每一层我们都是要更新的
        for(size_t i=0;i<n;++i)
        {
            newnode->_nextV[i]=prev[i]->_nextV[i];
            prev[i]->_nextV[i]=newnode;
        }
    }
    
    bool erase(int num) {
        vector<Node*> prev=FindPrevNode(num);
        //第一层下一个不是val,或者val不在表中
        if (prev[0]->_nextV[0]==nullptr ||prev[0]->_nextV[0]->_val!=num)
        {
            return false;
        }
        else{
            Node* del =prev[0]->_nextV[0];
            //del结点每一层的前后指针链接起来
            for(size_t i=0;i<del->_nextV.size();i++)
            {
                prev[i]->_nextV[i]=del->_nextV[i];
            }
            delete del;

            // 如果删除最高层节点,把头节点的层数也降一下
			int i = _head->_nextV.size() - 1;
			while (i >= 0)
			{
				if (_head->_nextV[i] == nullptr)
					--i;
				else
					break;
			}
			_head->_nextV.resize(i + 1);
            return true;
        }
    }

    int RandomLevel()
    {
        size_t level=1;

        // rand()最大值在[0,RAND_MAX]之间
        //相当于[0, 1]的范围
        while(rand()<RAND_MAX*_p&&level<_maxLevel)
        {
            ++level;
        }
        return level;
    }
private:
    Node* _head;//头结点
    size_t _maxLevel=32;//最大层数
    double _p=0.5;//创建新层的概率
};

/**
 * 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);
 */

💎三、性能分析

  1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。skiplist的优势是:a、skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂。b、skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。skiplist中p=1/2时,每个节点所包含的平均指针数目为2;skiplist中p=1/4时,每个节点所包含的平均指针数目为1.33;
  2. skiplist相比哈希表而言,就没有那么大的优势了。相比而言a、哈希表平均时间复杂度是O(1),比skiplist快。b、哈希表空间消耗略多一点。skiplist优势如下:a、遍历数据有序b、skiplist空间消耗略小一点,哈希表存在链接指针和表空间消耗。c、哈希表扩容有性能损耗。d、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penguin_bark

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值