数据结构---跳表

目录

一、跳表的概念

为什么要使用随机值来确定层高

二、跳表的分析

(1)查找过程

(2)性能分析

三、跳表的实现

四、与红黑树哈希表的对比


skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是

一样的,可以作为key或者key/value的查找模型。

一、跳表的概念

跳表是受到有序链表的启发上发展而来的。

但是可以看到链表查找的效率是O(N)。那么有人就在想了,能不能让他每间隔几个节点就升高一层,这样可以更快的跳到后面,从而提高查找效率。

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

此时的效率就变成了O(logN)

那是不是意味着层数越高,查找的效率也就越高。我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。

但是人们发现,这样效率虽然极高。但是一旦涉及到插入和删除操作,就会改变该节点后面的所有节点的层高,因为我们规定了必须每隔几个就提高一层。这样即使在查找的时候效率很高,但是会由于插入操作,重新遍历后面的节点进行层高重构,导致效率又回归了O(N)。

为什么要使用随机值来确定层高

如果我们插入和删除不会改变后面的层高呢?举个例子,本来每次间隔1个要提高一层,但是现在我不再改变后面节点的层高了,我插入的新节点是多高就把他直接链接到链表中,是不是就规避了重构结构的损耗。

但是如果插入的高度是固定的,这就相当于没有利用到跳表的优化。最开始我想能不能给一个数组来决定该节点有多少层呢?答案发现是不行的。

所以这里我们采取的是随机值的办法来决定新节点到底有多高。

二、跳表的分析

(1)查找过程

查找19分析:

从头节点的最上面的节点开始, next=6,19大于6.直接向右跳到6. next=空,向下走,next=25.25大于19.再向下走. next=9.19大于9,向右走到9. next=17. 19大于17, 向右跳到17. next=25. 25大于19.向下走. next=19.找到19. 总结: 比它大, 向右走. 比它小, 向下走
插入/删除分析:

插入和删除操作的关键都是, 找到此位置的每一层节点的前一个和后一个节点. 插入和删除和其他节点无关, 只需要修改每一层的next指针指向即可. 比如现在要在节点7和9之间插入节点8. 节点8假设是三层. 那么插入只需要考虑节点8的第一层和第二层的前一个节点是6,而第三层的前一个节点是7. 第一层的后一个节点是25.第二层的后一个节点是9.第三次的后一个节点也是9. 依次改变指针知晓即可.

(2)性能分析

skiplist插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时的效率呢?这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

当数据量足够大的时候,他的时间复杂度就是O(logN),这个效率是和红黑树等搜索树相当的,非常可观。

三、跳表的实现

cpp 复制代码
struct SkipListNode {
    int _val;
    vector<SkipListNode*> _nextv;
    SkipListNode(int val, int height) :_val(val), _nextv(height, nullptr)
    {}
};
cpp 复制代码
class Skiplist {
    typedef SkipListNode node;
public:
    Skiplist() {
        //头节点层数先给1层
        _head = new node(-1, 1);
        srand(time(0));
    }

    bool search(int target) {
        node* cur = _head;
        int level = _head->_nextv.size() - 1;
        while (level >= 0)
        {
            //和cur->next[level]比较,比它小就向下走,比它大向右走
            if (cur->_nextv[level] && cur->_nextv[level]->_val < target)
                cur = cur->_nextv[level];
            //下一个节点是空,即是尾,也要向下走
            else if (!cur->_nextv[level] || cur->_nextv[level]->_val > target)
                level--;
            else return true;
        }
        return false;
    }

    vector<node*> FindPrevNode(int num)
    {
        node* cur = _head;
        int level = _head->_nextv.size() - 1;
        vector<node*> prev(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)
            {
                prev[level] = cur;
                --level;
            }
        }
        return prev;
    }

    void add(int num) {
        vector<node*> prev = FindPrevNode(num);
        int n = RandomLevel();
        node* newnode = new node(num, n);
        if (_head->_nextv.size() < n)
        {
            _head->_nextv.resize(n, nullptr);
            prev.resize(n, _head);
        }
        //链接前后节点即可
        for (int i = 0; i < n; i++)
        {
            //新节点的下一个是prev的下一个
            newnode->_nextv[i] = prev[i]->_nextv[i];
            prev[i]->_nextv[i] = newnode;
        }
    }

    bool erase(int num) {
        //要删除你,先找到此节点的每层的前一个,和插入时相似
        vector<node*> prev = FindPrevNode(num);
        //代表这个值不存在, 最下层找不到它,它就一定不存在
        if (prev[0]->_nextv[0] == nullptr || prev[0]->_nextv[0]->_val != num)
            return false;
        node* del = prev[0]->_nextv[0];
        for (int i = 0; i < del->_nextv.size(); i++)
            prev[i]->_nextv[i] = del->_nextv[i];
        delete del;
        return true;
    }

    int RandomLevel()
    {
        int level = 1;
        while (rand() < RAND_MAX * _p && level < _max)
            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];
            }
            printf("\n");
        }
    }
private:
    node* _head;
    size_t _max = 32;
    double _p = 0.5;
};

四、与红黑树哈希表的对比

总结:

  • 平均空间开销
    • 跳表的额外空间主要用于存储多层指针,平均每个节点增加的指针数为 O(log n)
    • 相比平衡树(如红黑树),跳表的空间开销更可控。(因为红黑树是一个三叉链,每一个节点都要要存储3个节点)
  • 对比哈希表
    • 哈希表需要额外的空间存储哈希桶和处理冲突,空间开销可能更高。
    • 极端情况下哈希表会退化成链表,从而要使用红黑树。
    • 哈希表一旦扩容,则需要对原来的每一个元素重新映射,并且还要拷贝到新表中。如果扩容策略较为激进,则会导致空间开销较大。
相关推荐
WL学习笔记7 分钟前
堆(数据结构)
数据结构·
wabs6664 小时前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl106 小时前
图的最短路径Dijkstra
数据结构
zander2587 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态7 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面7 小时前
栈在递归中的应用
数据结构·算法
皓月斯语7 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king8 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79416 小时前
设置交集大小至少为2
数据结构·算法·leetcode
来一碗刘肉面1 天前
栈的应用(表达式求值)
数据结构·算法