C++【跳表】

一、什么是跳表

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

skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A
Probabilistic Alternative to Balanced Trees》。对细节感兴趣的同学可以下载论文原文来阅读。

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

William Pugh开始的优化思路:

  1. 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所
    示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。
  2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。
  3. skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。
    在这里插入图片描述
  4. skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数, 这样就好处理多了。细节过程入下图:

在这里插入图片描述

对于上图中的c,假设查找19
1、比9大,向右走,跳跃到9
2、比21小,向下走
3、比17大,向右走,跳跃17
4、比21小,向下走
5、跟19相等,找到了

那一个结点到底应该给几层?
随机出1000我就给1000?不太可能吧。
一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:
在这里插入图片描述
我们需要创建一个随机函数生成[0 ,1)之间的数据。
如果我们假设p=0.25(25%的概率会增加一层)
那么创建一层的概率:1-p=0.75
两层:(1-p)×p
三层:(1-p)2 ×p

一个节点的平均层数(也即包含的平均指针数目),计算如下:
在这里插入图片描述

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

二、跳表的实现

跳表leetcode

不使用任何库函数,设计一个 跳表 。

跳表 是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。

例如,一个跳表包含 [30, 40, 50, 60, 70, 90] ,然后增加 80、45 到跳表中,以下图的方式操作:

在这里插入图片描述

跳表中有很多层,每一层是一个短的链表。在第一层的作用下,增加、删除和搜索操作的时间复杂度不超过 O(n)。跳表的每一个操作的平均时间复杂度是 O(log(n)),空间复杂度是 O(n)。

了解更多 : https://en.wikipedia.org/wiki/Skip_list

在本题中,你的设计应该要包含这些函数:

bool search(int target) : 返回target是否存在于跳表中。
void add(int num): 插入一个元素到跳表。
bool erase(int num): 在跳表中删除一个值,如果 num 不存在,直接返回false. 如果存在多个 num ,删除其中任意一个即可。
注意,跳表中可能存在多个相同的值,你的代码需要处理这种情况。

示例 1:

输入
[“Skiplist”, “add”, “add”, “add”, “search”, “add”, “search”, “erase”, “erase”, “search”]
[[], [1], [2], [3], [0], [4], [1], [0], [1], [1]]
输出
[null, null, null, null, false, null, true, false, true, false]

解释
Skiplist skiplist = new Skiplist();
skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0); // 返回 false
skiplist.add(4);
skiplist.search(1); // 返回 true
skiplist.erase(0); // 返回 false,0 不在跳表中
skiplist.erase(1); // 返回 true
skiplist.search(1); // 返回 false,1 已被擦除

提示:

0 <= num, target <= 2 * 104
调用search, add, erase操作次数不大于 5 * 104
通过次数27,426提交次数39,687

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/design-skiplist
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

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]之间
        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、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力
  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
跳表是一种数据结构,可以高效地实现有序链表和有序集合,可以用来实现排行榜。跳表是通过在原始链表中添加多级索引节点来加速查找操作的。 跳表的实现思路如下: 1. 创建一个带有头节点的链表,头节点的值为负无穷大,尾节点的值为正无穷大。 2. 在原始链表中,插入新的节点时,根据节点的值,决定是否在当前层级上添加索引节点。添加索引节点的概率可以根据需求进行调整。 3. 使用索引节点可以将跳表分为多个层级(level),每一层级都是一个有序链表。 4. 查询操作时,从最高层级开始,从左向右逐层搜索,直到找到目标值所在的区间(比目标值大的最小节点和比目标值小的最大节点之间)。 5. 对于插入和删除操作,首先在最底层进行,然后根据概率决定是否在上层级插入或删除对应的节点。 使用跳表来实现排行榜的步骤如下: 1. 创建一个跳表,每个节点存储着用户的信息,包括用户的排名、分数等。 2. 初始化排行榜时,将所有用户按照分数从大到小顺序插入跳表中。 3. 当有新的用户加入或者用户的分数发生变化时,根据新的分数更新用户节点的位置。 4. 当需要查询某个用户的排名时,可以通过跳表中的索引节点,快速定位到该用户所在的层级,然后在该层级中按照顺序遍历找到目标节点,并返回排名。 通过以上步骤,我们可以使用跳表高效地实现排行榜功能。跳表的插入、删除和查找操作的时间复杂度都可以达到O(log n),在大数据量下具有较高的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

桜キャンドル淵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值