热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

隔壁老王都会了,你竟然还不知道?Rediszset底层—SkipList跳跃列表(面试超级加分项)

一、简介跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引

一、简介


跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

Skip List(跳跃列表)这种随机的数据结构,可以看做是一个二叉树的变种,它在性能上与红黑树、AVL树很相近;但是Skip List(跳跃列表)的实现相比前两者要简单很多,目前Redis的zset实现采用了Skip List(跳跃列表)(其它还有LevelDB等也使用了跳跃列表)。

RBT红黑树与Skip List(跳跃列表)简单对比:
RBT红黑树

  1. 插入、查询时间复杂度O(logn)
  2. 数据天然有序
  3. 实现复杂,设计变色、左旋右旋平衡等操作
  4. 需要加锁

Skip List跳跃列表

  1. 插入、查询时间复杂度O(logn)
  2. 数据天然有序
  3. 实现简单,链表结构
  4. 无需加锁

二、Skip List算法分析


2.1 Skip List论文

这里贴出Skip List的论文,需要详细研究的请看论文,下文部分公式、代码、图片出自该论文。
Skip Lists: A Probabilistic Alternative to Balanced Trees

https://www.cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf


2.2 Skip List动态图

先通过一张动图来了解Skip List的插入节点元素的流程,此图来自维基百科。

a50f862e502f718e490117c7a6541784.gif

a50f862e502f718e490117c7a6541784.gif


2.3 Skip List算法性能分析

2.3.1 计算随机层数算法

首先分析的是执行插入操作时计算随机数的过程,这个过程会涉及层数的计算,所以十分重要。对于节点他有如下特性:

  • 节点都有第一层的指针
  • 节点有第i层指针,那么第i+1层出现的概率为p
  • 节点有最大层数限制,MaxLevel

计算随机层数的伪代码:

论文中的示例

image.png


Java版本

1public int randomLevel(){
2    int level = 1;
3    // random()返回一个[0...1)的随机数
4    while (random() < p && level < MaxLevel){ 
5        level &#43;&#61; 1;
6    }
7    return level;
8}

代码中包含两个变量P和MaxLevel&#xff0c;在Redis中这两个参数的值分别是&#xff1a;

1p &#61; 1/4
2MaxLevel &#61; 64

2.3.2 节点包含的平均指针数目

Skip List属于空间换时间的数据结构&#xff0c;这里的空间指的就是每个节点包含的指针数目&#xff0c;这一部分是额外的内内存开销&#xff0c;可以用来度量空间复杂度。random()是个随机数&#xff0c;因此产生越高的节点层数&#xff0c;概率越低&#xff08;Redis标准源码中的晋升率数据1/4&#xff0c;相对来说Skip List的结构是比较扁平的&#xff0c;层高相对较低&#xff09;。其定量分析如下&#xff1a;

  • level &#61; 1 概率为1-p
  • level >&#61;2 概率为p
  • level &#61; 2 概率为p(1-p)
  • level >&#61; 3 概率为p^2
  • level &#61; 3 概率为p^2(1-p)
  • level >&#61;4 概率为p^3
  • level &#61; 4 概率为p^3(1-p)
  • ……

得出节点的平均层数&#xff08;节点包含的平均指针数目&#xff09;&#xff1a;

2195619-b17ca123586244cc.webp

设C(k) &#61; 在无限列表中向上攀升k个level的搜索路径的预期成本&#xff08;即长度&#xff09;那么推演如下&#xff1a;

1C(0)&#61;0
2C(k)&#61;(1-p)×(情况b的查找长度) &#43; p×(情况c的查找长度)
3C(k)&#61;(1-p)(C(k)&#43;1) &#43; p(C(k-1)&#43;1)
4C(k)&#61;1/p&#43;C(k-1)
5C(k)&#61;k/p

上面推演的结果可知&#xff0c;爬升k个level的预期长度为k/p&#xff0c;爬升一个level的长度为1/p。

由于MaxLevel &#61; L(n)&#xff0c; C(k) &#61; k / p&#xff0c;因此期望值为&#xff1a;(L(n) – 1) / p&#xff1b;将L(n) &#61; log(1/p)^n 代入可得&#xff1a;(log(1/p)^n - 1) / p&#xff1b;将p &#61; 1 / 2 代入可得&#xff1a;2 * log2^n - 2&#xff0c;即O(logn)的时间复杂度。


三、Skip List特性及其实现


3.1 Skip List特性

Skip List跳跃列表通常具有如下这些特性

  1. Skip List包含多个层&#xff0c;每层称为一个level&#xff0c;level从0开始递增
  2. Skip List 0层&#xff0c;也就是最底层&#xff0c;应该包含所有的元素
  3. 每一个level/层都是一个有序的列表
  4. level小的层包含level大的层的元素&#xff0c;也就是说元素A在X层出现&#xff0c;那么 想X>Z>&#61;0的level/层都应该包含元素A
  5. 每个节点元素由节点key、节点value和指向当前节点所在level的指针数组组成


3.2 Skip List查询

假设初始Skip List跳跃列表中已经存在这些元素&#xff0c;他们分布的结构如下所示&#xff1a;

Skip List 跳跃列表初始.png

此时查询节点88&#xff0c;它的查询路线如下所示&#xff1a;
Skip List 跳跃列表查询.png

  1. 从Skip List跳跃列表最顶层level3开始&#xff0c;往后查询到10 <88 && 后续节点值为null && 存在下层level2
  2. level2 10往后遍历&#xff0c;27 <88 && 后续节点值为null && 存在下层level1
  3. level1 27往后遍历&#xff0c;88 &#61; 88&#xff0c;查询命中

3.3 Skip List插入

Skip List的初始结构与2.3中的初始结构一致&#xff0c;此时假设插入的新节点元素值为90&#xff0c;插入路线如下所示&#xff1a;

  1. 查询插入位置&#xff0c;与Skip List查询方式一致&#xff0c;这里需要查询的是第一个比90大的节点位置&#xff0c;插入在这个节点的前面&#xff0c; 88 <90 <100
  2. 构造一个新的节点Node(90)&#xff0c;为插入的节点Node(90)计算一个随机level&#xff0c;这里假设计算的是1&#xff0c;这个level时随机计算的&#xff0c;可能时1、2、3、4…均有可能&#xff0c;level越大的可能越小&#xff0c;主要看随机因子x &#xff0c;层数的概率大致计算为 (1/x)^level &#xff0c;如果level大于当前的最大level3&#xff0c;需要新增head和tail节点
  3. 节点构造完毕后&#xff0c;需要将其插入列表中&#xff0c;插入十分简单步骤 -> Node(88).next &#61; Node(90); Node(90).prev &#61; Node(80); Node(90).next &#61; Node(100); Node(100).prev &#61; Node(90);

Skip List插入节点.png

3.4 Skip List删除

删除的流程就是查询到节点&#xff0c;然后删除&#xff0c;重新将删除节点左右两边的节点以链表的形式组合起来即可&#xff0c;这里不再画图

四、手写实现一个简单Skip List

实现一个Skip List比较简单&#xff0c;主要分为两个步骤&#xff1a;

  1. 定义Skip List的节点Node&#xff0c;节点之间以链表的形式存储&#xff0c;因此节点持有相邻节点的指针&#xff0c;其中prev与next是同一level的前后节点的指针&#xff0c;down与up是同一节点的多个level的上下节点的指针
  2. 定义Skip List的实现类&#xff0c;包含节点的插入、删除、查询&#xff0c;其中查询操作分为升序查询和降序查询&#xff08;往后和往前查询&#xff09;&#xff0c;这里实现的Skip List默认节点之间的元素是升序链表
     

3.1 定义Node节点

Node节点类主要包括如下重要属性&#xff1a;

  1. score -> 节点的权重&#xff0c;这个与Redis中的score相同&#xff0c;用来节点元素的排序作用
  2. value -> 节点存储的真实数据&#xff0c;只能存储String类型的数据
  3. prev -> 当前节点的前驱节点&#xff0c;同一level
  4. next -> 当前节点的后继节点&#xff0c;同一level
  5. down -> 当前节点的下层节点&#xff0c;同一节点的不同level
  6. up -> 当前节点的上层节点&#xff0c;同一节点的不同level

1package com.liziba.skiplist;23/**4 * 

5 *      跳表节点元素6 * 

7 *8 * &#64;Author: Liziba9 * &#64;Date: 2021/7/5 21:01
10 */
11public class Node {
12
13    /** 节点的分数值&#xff0c;根据分数值来排序 */
14    public Double score;
15    /** 节点存储的真实数据 */
16    public String value;
17    /** 当前节点的 前、后、下、上节点的引用 */
18    public Node prev, next, down, up;
19
20    public Node(Double score) {
21        this.score &#61; score;
22        prev &#61; next &#61; down &#61; up &#61; null;
23    }
24
25    public Node(Double score, String value) {
26        this.score &#61; score;
27        this.value &#61; value;
28    }
29}

3.2 SkipList节点元素的操作类

SkipList主要包括如下重要属性&#xff1a;

  1. head -> SkipList中的头节点的最上层头节点&#xff08;level最大的层的头节点&#xff09;&#xff0c;这个节点不存储元素&#xff0c;是为了构建列表和查询时做查询起始位置的&#xff0c;具体的结构请看2.3中的结构
  2. tail -> SkipList中的尾节点的最上层尾节点&#xff08;level最大的层的尾节点&#xff09;&#xff0c;这个节点也不存储元素&#xff0c;是查询某一个level的终止标志
  3. level -> 总层数
  4. size -> Skip List中节点元素的个数
  5. random -> 用于随机计算节点level&#xff0c;如果 random.nextDouble() <1/2则需要增加当前节点的level&#xff0c;如果当前节点增加的level超过了总的level则需要增加head和tail(总level)

1package com.liziba.skiplist;23import java.util.Random;45/**6 * 

7 *      跳表实现8 * 

9 *10 * &#64;Author: Liziba11 */12public class SkipList {1314    /** 最上层头节点 */15    public Node head;16    /** 最上层尾节点 */17    public Node tail;18    /** 总层数 */19    public int level;20    /** 元素个数 */21    public int size;22    public Random random;2324    public SkipList() {25        level &#61; size &#61; 0;26        head &#61; new Node(null);27        tail &#61; new Node(null);28        head.next &#61; tail;29        tail.prev &#61; head;30    }3132    /**33     * 查询插入节点的前驱节点位置34     *35     * &#64;param score36     * &#64;return37     */38    public Node fidePervNode(Double score) {39        Node p &#61; head;40        for(;;) {41            // 当前层(level)往后遍历&#xff0c;比较score&#xff0c;如果小于当前值&#xff0c;则往后遍历42            while (p.next.value &#61;&#61; null && p.prev.score <&#61; score)43                p &#61; p.next;44            // 遍历最右节点的下一层(level)45            if (p.down !&#61; null)46                p &#61; p.down;47            else48                break;49        }50        return p;51    }5253    /**54     * 插入节点&#xff0c;插入位置为fidePervNode(Double score)前面55     *56     * &#64;param score57     * &#64;param value58     */59    public void insert(Double score, String value) {6061        // 当前节点的前置节点62        Node preNode &#61; fidePervNode(score);63        // 当前新插入的节点64        Node curNode &#61; new Node(score, value);65        // 分数和值均相等则直接返回66        if (curNode.value !&#61; null && preNode.value !&#61; null && preNode.value.equals(curNode.value)67                  && curNode.score.equals(preNode.score)) {68            return;69        }7071        preNode.next &#61; curNode;72        preNode.next.prev &#61; curNode;73        curNode.next &#61; preNode.next;74        curNode.prev &#61; preNode;7576        int curLevel &#61; 0;77        while (random.nextDouble() < 1/2) {78            // 插入节点层数(level)大于等于层数(level)&#xff0c;则新增一层(level)79            if (curLevel >&#61; level) {80                Node newHead &#61; new Node(null);81                Node newTail &#61; new Node(null);82                newHead.next &#61; newTail;83                newHead.down &#61; head;84                newTail.prev &#61; newHead;85                newTail.down &#61; tail;86                head.up &#61; newHead;87                tail.up &#61; newTail;88                // 头尾节点指针修改为新的&#xff0c;确保head、tail指针一直是最上层的头尾节点89                head &#61; newHead;90                tail &#61; newTail;91                &#43;&#43;level;92            }9394            while (preNode.up &#61;&#61; null)95                preNode &#61; preNode.prev;9697            preNode &#61; preNode.up;9899            Node copy &#61; new Node(null);
100            copy.prev &#61; preNode;
101            copy.next &#61; preNode.next;
102            preNode.next.prev &#61; copy;
103            preNode.next &#61; copy;
104            copy.down &#61; curNode;
105            curNode.up &#61; copy;
106            curNode &#61; copy;
107
108            &#43;&#43;curLevel;
109        }
110        &#43;&#43;size;
111    }
112
113    /**
114     * 查询指定score的节点元素
115     * &#64;param score
116     * &#64;return
117     */
118    public Node search(double score) {
119        Node p &#61; head;
120        for (;;) {
121            while (p.next.score !&#61; null && p.next.score <&#61; score)
122                p &#61; p.next;
123            if (p.down !&#61; null)
124                p &#61; p.down;
125            else // 遍历到最底层
126                if (p.score.equals(score))
127                    return p;
128                return null;
129        }
130    }
131
132    /**
133     * 升序输出Skip List中的元素 &#xff08;默认升序存储&#xff0c;因此从列表head往tail遍历&#xff09;
134     */
135    public void dumpAllAsc() {
136        Node p &#61; head;
137        while (p.down !&#61; null) {
138            p &#61; p.down;
139        }
140        while (p.next.score !&#61; null) {
141            System.out.println(p.next.score &#43; "-->" &#43; p.next.value);
142            p &#61; p.next;
143        }
144    }
145
146    /**
147     * 降序输出Skip List中的元素
148     */
149    public void dumpAllDesc() {
150        Node p &#61; tail;
151        while (p.down !&#61; null) {
152            p &#61; p.down;
153        }
154        while (p.prev.score !&#61; null) {
155            System.out.println(p.prev.score &#43; "-->" &#43; p.prev.value);
156            p &#61; p.prev;
157        }
158    }
159
160
161    /**
162     * 删除Skip List中的节点元素
163     * &#64;param score
164     */
165    public void delete(Double score) {
166        Node p &#61; search(score);
167        while (p !&#61; null) {
168            p.prev.next &#61; p.next;
169            p.next.prev &#61; p.prev;
170            p &#61; p.up;
171        }
172    }
173
174
175}

推荐阅读
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • EzPP 0.2发布,新增YAML布局渲染功能
    EzPP发布了0.2.1版本,新增了YAML布局渲染功能,可以将YAML文件渲染为图片,并且可以复用YAML作为模版,通过传递不同参数生成不同的图片。这个功能可以用于绘制Logo、封面或其他图片,让用户不需要安装或卸载Photoshop。文章还提供了一个入门例子,介绍了使用ezpp的基本渲染方法,以及如何使用canvas、text类元素、自定义字体等。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
author-avatar
mobiledu2502885323
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有