Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如果面试官问你“跳表”,这一篇直接往他嘴里怼就行了

如果面试官问你“跳表”,这一篇直接往他嘴里怼就行了

作者头像
老李秀
发布于 2021-05-13 02:27:06
发布于 2021-05-13 02:27:06
1.1K00
代码可运行
举报
运行总次数:0
代码可运行

大家好啊,摸完3月的?,果然4月要加倍还回去。前段时间脑子一热,我翻译了一篇《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

我记得上大学的时候,菜鸡如我,能用数组解的题我从不用链表,因为这种指针指来指去的东西,真的费脑。同寝室有个大佬,那时候就会玩什么十字链表、红黑树了, 而我那时候天天lol,酷爱VN,嘴里时常念叨着一句让我们来猎杀那些陷入黑暗中的人吧= =。而今,做了5年多curd,也逐渐意识到,了解点数据结构,确实能开阔一些思维,有些时候处理业务问题的时候,说不定能用上(虽然可能性很小)。所以,今天我们就来聊聊这个niu bi的数据结构——跳表。

先来说说为什么跳表niu bi。因为它在绝大多数情况下能代替平衡树,而且实现起来简单,性能还不比平衡树家族各位差(AVL树,红黑树,这些我都不懂)。我想试问下,比如面试官让你手写树的前序遍历、中序遍历、后序遍历,假如你回忆半天,好不容易用递归的方式写出来了,面试官继续刁难,说不要用递归,你换种方式给我实现。是不是瞬间就崩溃了,可见非递归代码来操作树的复杂度有多高,反正我感觉我身边应该基本上没有能手写的。但是跳表不一样,它简简单单,在你仔细读完我这篇文章的时候,百分百能手写出跳表的查询、插入、删除。

像redis这么牛逼的项目,都是用跳表来实现ZSet(有序集合)的。

跳表的起源

我们先从一个链表说起,对于一个单向链表,如果要你查找链表当中的一个元素,那么时间复杂度是O(n),我们只能从头节点开始遍历,直到找到为止。

如图a所示

那我们加以改造,每隔一个节点,我们增加一个指向下一个的下一个节点的指针。这样子,是不是我们查找的效率就可以提升到O(n/2 +1)了。如果b所示

我们再进一步,再每四节点加上指向前面4节点的指针,那么我们时间复杂度就更进一步降低为O(n/4 +2)了 如果图c所示

所以每2^i个节点都有指向前面2^i的前向指针(如图d),可以让我们查找变得方便跟快速,但是插入跟删除节点就日了狗了,超级难维护。

基于这样的背景,1990年有个叫William Pugh的计算机科学家,就发明了跳表。我们先来了解一个叫做level的概念,一个链表的节点有k个前向指针,那么就是level k。下图e的6节点就是level 4,9节点是level 3。如果我们每插入一个节点的时候,随机一个level,后续无需更改, 这种带有跳过额外节点的链表数据结构,就叫做跳表。就长的像下图e一样。

论文结论

在精通跳表算法之前,我们先来记住一些概念跟公式。

每个跳表有个头结点head和一个尾结点NIL,跳表初始化的时候level为1,然后头节点指向NIL。每个跳表都有一个maxLevel, 还有一个listLevel(当前跳表所有节点中最高的level值),maxLevel的定义就是当前跳表能达到的最高level。论文作者经过严密的数学计算,让我们选择一个常量p ,且p为0.25最佳。

maxLevel = log(1/p) n。

其中n代表跳表元素的个数。比如我们要存65536个元素,那么

maxLevel=log(1/0.25)65535 = 8。

我们每个节点的level就是会从1-8中通过随机算法产生一个。至于为什么要这样,别问,我也不懂,结论就是这样。当然你也可以去通读这篇论文,作者给出了详细的数学证明过程。还有,跳表并不是无敌的,这种随机性level的做法,在极小极小概率的情况下会导致性能很差,因为这概率非常小,小到可以忽略不计。还有,跳表的时间复杂度为O(logN)。

查找算法

比如我们要搜索上图e的节点17

我们从头节点的最高层level 4开始往前,前面是6 小于17,继续向前,6的level 4指针指向NIL 所以我们回来,在6节点减少到level 3,这时候往前是25,25大于17,我们继续减少level,6的level 2指向9,9小于17,继续从9的level2 往前,就搜到了17。

总结起来从头结点的最高层level开始搜索,遇到比他大的就减少level,一直遍历下去,直到找到为止。

插入&&删除

既然我们精通了查询,那么其实插入跟删除就是先查询,然后再拼接即可。

比如我们要插入一个17的节点,我们使用搜索算法,然后随机生成一个level值(随机算法下文会介绍),然后我们还要根据搜索路径保留一个update[i]的数组向量。这个update数组里面记录什么呢,比如刚才插入17这条搜索路径,update数组记录了搜索路径每层level最后的那个node节点,比如level 4最后那个是node 6。(数组下标从0开始,所以level4的跳表为update[3])

update[3] = node 6

update[2] = node 6

update[1] = node 9

update[0] = node 12

这时候准备工作都已经完毕,然后怎么拼接呢。这时候我们要随机生成一个level。比如这个17 生成的level为2 那么我们只需要这样拼接

17节点的level 2的next = node 9 level 2的next(update[1])

node 9的level 2 next指向node 17的level 2

node 17 level1的next = node 12 level 1的next (update[0])

node 12的的level 1 next指向 node 17的level 1

就像上图我标红的所示。

这时候有聪明的小伙伴要问了。安尼特老师,安尼特老师,你不是说level随机么,那我生成一个level 更高的,那怎么办呢。这简单。我们只要把大于当前跳表的level 都从头节点指向它。就像下图一样。

删除就更简单了,直接查询找到该节点,然后替换掉就行了。比如我们要删除节点17,我们只需要查找到17。把每层指向17节点的,直接指向下一个节点就行了。

比如这里,9的level 2就指向25了 12的level1直接指向19

但是有一种特殊情况比如我们删掉了一个节点,我们需要检查下,如果当前跳表的头节点的listLevel层指针指向NIL,那么我们需要把当前listLevel-1

talk is cheap show me the code

鉴于跳表的伪代码可能看起来吃力一些,我就用java代码演示。这样更容易理解。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.company.skiplist;

public class SkipList<T> {

    // 跳表能达到的最大level
    private final int MAX_LEVEL;
    // 当前跳表所有节点的最大level
    private int listLevel;
    // 表头
    private SkipListNode<T> listHead;
    // 表尾
    private SkipListNode<T> NIL;
    // 生成randomLevel用到的概率值
    private final double P;
    // 论文里给出的最佳概率值
    private static final double OPTIMAL_P = 0.25;

    public SkipList() {
        // 假如我们的跳表最大元素个数为65536个
        // 那么p = 0.25 MAX_LEVEL = 8
        // 因为java没有直接log能自定义底数的函数所以用这样计算 log(1/p) n = ln(65536)/ln(1/0.25) 这个高中应该学过
        this(OPTIMAL_P, (int) Math.ceil(Math.log(65536) / Math.log(1 / OPTIMAL_P)));
    }

    public SkipList(double probability, int maxLevel) {
        P = probability;
        MAX_LEVEL = maxLevel;

        listLevel = 1;
        listHead = new SkipListNode<T>(Integer.MIN_VALUE, null, maxLevel);
        NIL = new SkipListNode<T>(Integer.MAX_VALUE, null, maxLevel);
        for (int i = listHead.forward.length - 1; i >= 0; i--) {
            listHead.forward[i] = NIL;
        }
    }

    // 内部类
    class SkipListNode<T> {
        int key;
        T value;
        SkipListNode[] forward;

        public SkipListNode(int key, T value, int level) {
            this.key = key;
            this.value = value;
            this.forward = new SkipListNode[level];
        }
    }

    public T search(int searchKey) {
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
        }

        curNode = curNode.forward[0];
        if (curNode.key == searchKey) {
            return curNode.value;
        } else {
            return null;
        }
    }

    public void insert(int searchKey, T newValue) {
        SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
            // curNode.key < searchKey <= curNode.forward[i].key
            update[i] = curNode;
        }

        curNode = curNode.forward[0];

        if (curNode.key == searchKey) {
            curNode.value = newValue;
        } else {
            int lvl = randomLevel();

            if (listLevel < lvl) {
                for (int i = listLevel; i < lvl; i++) {
                    update[i] = listHead;
                }
                listLevel = lvl;
            }

            SkipListNode<T> newNode = new SkipListNode<T>(searchKey, newValue, lvl);

            for (int i = 0; i < lvl; i++) {
                newNode.forward[i] = update[i].forward[i];
                update[i].forward[i] = newNode;
            }
        }
    }

    public void delete(int searchKey) {
        SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
            // curNode.key < searchKey <= curNode.forward[i].key
            update[i] = curNode;
        }

        curNode = curNode.forward[0];

        if (curNode.key == searchKey) {
            for (int i = 0; i < listLevel; i++) {
                if (update[i].forward[i] != curNode) {
                    break;
                }
                update[i].forward[i] = curNode.forward[i];
            }

            while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) {
                listLevel--;
            }
        }
    }

    //生成随机level的算法
    private int randomLevel() {
        int lvl = 1;
        while (lvl < MAX_LEVEL && Math.random() < P) {
            lvl++;
        }
        return lvl;
    }

    public void print() {
        for (int i = listLevel - 1; i >= 0; i--) {
            SkipListNode<T> curNode = listHead.forward[i];
            while (curNode != NIL) {
                System.out.print(curNode.key + "->");
                curNode = curNode.forward[i];
            }
            System.out.println("NIL");
        }
    }

    public static void main(String[] args) {
        SkipList<Integer> sl = new SkipList<Integer>();
        sl.insert(20, 20);
        sl.insert(5, 5);
        sl.insert(10, 10);
        sl.insert(1, 1);
        sl.insert(100, 100);
        sl.insert(80, 80);
        sl.insert(60, 60);
        sl.insert(30, 30);
        Integer sKey = sl.search(10);
        System.out.println(sKey);
        System.out.println("---");
        sl.print();
        System.out.println("---");
        sl.delete(20);
        sl.delete(100);
        sl.print();
    }
}

不知道多少人还看到了这里,可能你感觉学完也没什么卵用,我感觉可能对于面试还是有点用的。我再举个最实际的应用场景,比如你要上一个大型应用,里面大量使用了redis,那熟悉熟悉各种数据结构,也差不多能估算下会占用多少内存吧- -。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
面试官:为何Redis使用跳表而非红黑树实现SortedSet?
知道跳表(Skip List)是在看关于Redis的书的时候,Redis中的有序集合使用了跳表数据结构。接着就查了一些博客,来学习一下跳表。后面会使用Java代码来简单实现跳表。
JavaEdge
2021/12/07
5910
面试官:为何Redis使用跳表而非红黑树实现SortedSet?
​ C++实现跳跃表查找(Skip List Search)算法
以下是用 C++ 实现跳跃表查找(Skip List Search)算法的示例代码:
用户7492006
2024/07/23
1480
​
 C++实现跳跃表查找(Skip List Search)算法
手写了个可能是Github性能最强的Go跳表
作者:phongchen,腾讯 IEG 后台开发工程师 2022 年 3 月,广大人民期盼已久的支持的泛型的 go1.18 发布了。但是目前基于泛型的容器实现还不多。我实现了一套类似 C++中 STL 的容器和算法库。其中有序的 Map 选择用跳表来实现,并优化到了相当好的性能。在此分享一下优化的思路和心得,供大家参考借鉴,如果发现有错误也欢迎指出。 一、背景 首先为标题党致歉,不过确实没吹牛 😊。 最近一年我所负责的业务系统中,用 Go 语言的实现的占了至少 70%的比例,因此 Review 了大量的 G
腾讯技术工程官方号
2022/09/02
1.8K0
手写了个可能是Github性能最强的Go跳表
SkipList跳表:高效查找的利器
在计算机科学中,数据结构的选择对于程序的性能至关重要。对于需要高效查找、插入和删除操作的场景,跳跃表(SkipList)是一种非常实用的数据结构。本文将详细介绍跳跃表的原理、实现和应用场景。
笑傲码湖
2025/04/11
1120
数据结构:跳跃链表
开发时经常使用的平衡数据结构有B数、红黑数,AVL数。但是如果让你实现其中一种,很难,实现起来费时间。而跳跃链表一种基于链表数组实现的快速查找数据结构,目前开源软件 Redis 和 LevelDB 都有用到它。它的效率和红黑树以及 AVL 树不相上下
呆呆
2021/08/01
2220
Redis源码剖析之跳表(skiplist)
计算机领域有很多种数据结构,数据结构的存在要么是为了节省时间、要么是为了节省空间,或者二者兼具,所以就有部分数据结构有时间换空间,空间换时间之说。其实还有某些以牺牲准确性来达到节省时间空间的数据结构,像我之间讲过的bloomfilter就是其中的典型。而今天要讲的skiplist也是一种概率性数据结构,它以一种随机概率降数据组织成多级结构,方便快速查找。
xindoo
2021/01/21
9760
Redis源码之跳表数据结构
跳跃表(skiplist)是一种随机化的数据, 由 William Pugh 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出, 跳跃表以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操作都可以在对数期望时间下完成, 并且比起平衡树来说, 跳跃表的实现要简单直观得多。
杨永贞
2022/05/07
6900
Redis源码之跳表数据结构
【数据结构】线性表(五)跳表及其基本操作(定义、创建、查找、插入、删除)
链表中的结点用存储单元(若干个连续字节)来存放,存储单元之间既可以是(存储空间上)连续的,也可以是不连续的,甚至可以零散地分布在存储空间中的任何位置。换言之,链表中结点的逻辑次序和物理次序之间并无必然联系。最重要的是,链表可以在不移动结点位置的前提下根据需要随时添加删除结点,动态调整。
Qomolangma
2024/07/30
2300
【数据结构】线性表(五)跳表及其基本操作(定义、创建、查找、插入、删除)
data_structure_and_algorithm -- 跳表:python & java & c-cpp 实现
版权声明:本文为博主原创文章,未经博主允许不得转载。有问题可以加微信:lp9628(注明CSDN)。 https://blog.csdn.net/u014365862/article/details/84311162
MachineLP
2019/05/26
8670
golang刷leetcode 经典(4) 实现跳表
跳表是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。
golangLeetcode
2022/08/02
2430
golang刷leetcode 经典(4) 实现跳表
跳表很难吗?手把手教你如何跳跃它!
​ skiplist是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为 O(logN)(大多数情况下,因为是实现上是概率问题),因为其性能匹敌红黑树且实现较为简单,因此在很多著名项目都用 skiplist 来代替红黑树,例如 LevelDB、RocksDB、Redis中的有序集合zset 的底层存储结构就是用的skiplist。
利刃大大
2023/10/17
7310
跳表很难吗?手把手教你如何跳跃它!
深夜学算法之SkipList:让链表飞
上次写Python操作LevelDB时提到过,有机会要实现下SkipList。摘录下wiki介绍:
sunsky
2020/08/20
3420
深夜学算法之SkipList:让链表飞
数据结构(9)-- 跳表
让你现场手写一棵红黑树、AVL树、伸展树之类的,你行吗? 要不让我查资料,我估计只能扯皮。
看、未来
2021/09/18
3610
详解全网最快Go泛型跳表【内附源码】
导读| 今年开发者期盼已久的、泛型的go1.18发布了,但目前基于泛型的容器实现案例很稀缺。腾讯后台开发工程师陈峰实现了一套类似C++中STL的容器和算法库。其中有序的Map用跳表实现,并优化到极致性能。本文作者将分享优化的思路并公开源码,供各位开发者参考。
腾讯云开发者
2022/12/31
7390
详解全网最快Go泛型跳表【内附源码】
跳跃表(skiplist )详解及其C++编程实现
目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。
全栈程序员站长
2022/11/19
1.5K0
跳跃表(skiplist )详解及其C++编程实现
DS高阶:跳表
skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》
小陈在拼命
2024/05/26
960
DS高阶:跳表
浅析SkipList跳跃表原理及代码实现
SkipList在leveldb以及lucence中都广为使用,是比较高效的数据结构。由于它的代码以及原理实现的简单性,更为人们所接受。我们首先看看SkipList的定义,为什么叫跳跃表?
sunsky
2020/08/20
6780
c++实现skipList「建议收藏」
Skip List是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间)。基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。所有操作都以对数随机化的时间进行。Skip List可以很好解决有序链表查找特定值的困难。
全栈程序员站长
2022/09/27
4300
c++实现skipList「建议收藏」
hash+跳表,玩转Redis有序集合
导语 | Redis有序集合是复合数据结构,它是由一个双hashmap构成的字典和跳表实现的,本文将为大家详细介绍Redis有序集合实现的原理以及使用场景和案例,希望与大家一同交流。文章作者:邓国东,腾讯游戏运营研发工程师。 一、Redis有序集合介绍 Redis有序集合(sorted set)是复合数据结构,它是由一个双hashmap构成的字典和跳表实现的。 跟集合一样,有序集合也是string类型元素的集合,不同的是每个元素都会关联一个权。通过权值可以有序的获取集合中的元素。 Red
腾讯云开发者
2021/01/27
1.2K0
【数据结构】跳表SkipList代码解析(C++)
跳表SkipList解析 原项目链接——基于跳表实现的轻量级键值数据库 添加注释后——SkipList 什么是跳表 这里不做介绍,详见: 跳表──没听过但很犀利的数据结构 拜托,面试别再问我跳表了! 代码解析 主要理解点 先来张图 各个节点是如何相连接(关联)的? 通过每个节点的forward数组,forward数组存储当前节点,在每一层的下一个节点。 以头节点为例,头结点的forward存储的是每一层的第一个节点。然后通过第一个节点的forward[level],拿到该层的后面元
半生瓜的blog
2023/05/13
3400
【数据结构】跳表SkipList代码解析(C++)
相关推荐
面试官:为何Redis使用跳表而非红黑树实现SortedSet?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验