当前位置: 首页 > article >正文

【数据结构学习笔记】19:跳表(Skip List)

介绍

跳表是一个能在 O ( n l o g n ) O(nlogn) O(nlogn)时间完成查找、插入、删除的数据结构,相比于树形结构优点就是很好写(所以也用于实现Redis ZSet)。其核心思想就是维护一个元素有序的,能随机提升索引层数的链表。最下面一层就是一个普通的链表,存了所有的元素,而每次提升索引高度都一定会从最下面一层开始提升连续的若干层,因此从最上面的层到最下面的层,索引一定是从稀疏到稠密,所以在查询的时候就能从上层开始,很快的跳过一些元素,再向下一层走,逐渐定位到元素的位置。

实现思路

结构

固定跳表层数后,跳表的每个结点(存某个元素)都在每一层有一个next指针,指向这一层的下一个结点。

为了查询方便,设定一个虚拟头结点,这个虚拟头节点作为查询的起始节点,每一层会指向实际的这一层的起始节点。

内部操作:find

引入一个特殊的内部操作,用于定位结点在每一层的位置,不管是接下来要删除这个结点,还是在这个结点附近(前或者后)插入一个元素结点都能借用这个操作方便的找到它。

考虑到每一层都是一个单向链表,所以find操作一定是返回目标结点在每一层的前置结点。

因为这个操作希望得到的是一个Node指针的数组,这个数组会在下一步的操作(插入/删除/查询)中被用到,所以可以给这个跳表引入一个prev数组缓存这个信息,每次使用前直接清空就可以了。

查询操作:search

find得到目标元素的prev数组,然后就能找到最下面那层的结点,即prev[0]->next[0],根据结点是否存在以及是否是要找的元素就可以得到search的结果了。

添加操作:add

因为跳表是有序的,所以一定会按序添加到指定的位置上。先先find得到目标元素的prev数组,然后从最下层开始向上,除了最下面那层一定要插入,上面的层i如果(连续且随机地)需要派生这层的索引,就根据prev[i]是该节点在第i层的前置结点,在此层按照链表的方式插入这个索引就可以了。

删除操作:erase

先做一遍serach操作,确认要删除的元素存在,且构造好了prev数组,接下来从下层向上,对于每一层i,按照链表的方式判断要删除的结点在此层存在索引,就将索引删除。因为索引从下到上是连续存在的,所以删到找不到索引就一定已经删干净了,最后记得回收结点即可。

例题:LeetCode 1206. 设计跳表

class Skiplist {
private:
    static const int MAX_LEVEL = 8;

    struct Node {
        int val;
        Node** next;
        Node(int _val) :val(_val) {
            next = new Node*[MAX_LEVEL];
            memset(next, NULL, sizeof(Node*) * MAX_LEVEL);
        }
    } *head, **prev;

    void find(int target, Node** prev) {
        Node* p = head;
        for (int i = MAX_LEVEL - 1; i >= 0 ; i -- ) {
            while (p->next[i] && p->next[i]->val < target) p = p->next[i];
            prev[i] = p;
        }
    }

public:
    Skiplist() {
        this->head = new Node(-1);
        this->prev = new Node*[MAX_LEVEL];
    }

    ~Skiplist() {
        delete this->head;
        delete[] this->prev;
    }
    
    bool search(int target) {
        memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);
        find(target, prev);

        auto p = prev[0]->next[0];
        return p && p->val == target;
    }
    
    void add(int num) {
        memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);
        find(num, prev);

        auto p = new Node(num);
        for (int i = 0; i < MAX_LEVEL; i ++ ) {
            p->next[i] = prev[i]->next[i];
            prev[i]->next[i] = p;
            if (rand() % 2) break;
        }
    }
    
    bool erase(int num) {
        memset(prev, NULL, sizeof(Node*) * MAX_LEVEL);
        find(num, prev);
        
        auto p = prev[0]->next[0];
        if (!p || p->val != num) return false;

        for (int i = 0; i < MAX_LEVEL && prev[i]->next[i] == p; i ++ )
            prev[i]->next[i] = p->next[i];
        
        delete p;
        return true;
    }
};

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

http://www.kler.cn/a/504102.html

相关文章:

  • Oracle EBS GL定期盘存WIP日记账无法过账数据修复
  • 国内源快速在线安装qt5.15以上版本。(10min安装好)(图文教程)
  • 网络层协议-----IP协议
  • vim基本命令(vi、工作模式、普通模式、插入模式、可视模式、命令行模式、复制、粘贴、插入、删除、查找、替换)
  • 设计模式02:结构型设计模式之适配器模式使用情景及其基础Demo
  • FPGA EDA软件的位流验证
  • 浅谈计算机网络02 | SDN控制平面
  • 一个使用 Golang 编写的新一代网络爬虫框架,支持JS动态内容爬取
  • 【漫话机器学习系列】047.指数型线性单元(Exponential Linear Units,ELU)
  • 1.4 给应用添加service,执行扩容和滚动更新
  • TDSQL 内存占用解析一例
  • Golang|单机并发缓存
  • 24. 【.NET 8 实战--孢子记账--从单体到微服务】--记账模块--预算扣除、退回、补充
  • 华为2024嵌入式研发面试题
  • Adobe与MIT推出自回归实时视频生成技术CausVid。AI可以边生成视频边实时播放!
  • Oracle 终止正在执行的SQL
  • 下载导出Tomcat上的excle文档,浏览器上显示下载
  • Web前端------HTML块级和行内标签之块级标签
  • kube-prometheus监控Linux主机
  • 关于H5复制ios没有效果
  • JavaScript系列(25)--性能优化技术详解
  • 如何通过NMudbus读取寄存器数据
  • Vue环境变量配置指南:如何在开发、生产和测试中设置环境变量
  • mysql 与Redis 数据强一致方案
  • Jenkins简单的安装运行
  • 线程间通信