数据结构和算法之——跳表

之前我们知道,二分查找依赖数组的随机访问,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找了吗?而实际上,我们只需要对链表稍加改造,就可以实现类似“二分”的查找算法,这种改造之后的数据结构叫作跳表(Skip List)

1. 何为跳表?

对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低。

假如我们对链表每两个结点提取一个结点到上一级,然后建立一个索引指向原始结点,如下图所示。

这时候,我们要查找某一个数据的时候,就可以先在索引里面查找出一个大的范围,然后再下降到原始链表中精确查找。

比如,我们要查找 16,我们发现 16 位于 13 和 17 之间,这时候,我们就从 13 的地方下降到原始链表,然后再往后查询。原来我们查找 16,需要遍历 10 个结点,现在只需要遍历 7 个结点。

我们发现,加一层索引后,查找一个结点需要遍历的次数减少了,也就是查找效率提高了

那么我们再多加一级索引呢?效果会不会有更大提升?

这一次,我们只需要遍历 6 个结点了。

数据量不大的时候这种方法可能效率提高得还不是很明显,下面看一个包含 64 个结点的例子,这次我们建立了五级索引。

查找 62 的时候原来需要遍历 62 次,现在只需要 11 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显

2. 跳表查询的分析?

如果链表中总共有 \(n\) 个结点,那么第一级索引就有 \(\frac{n}{2}\) 个结点,第二级索引就有 \(\frac{n}{4}\) 个结点,以此类推,那么第 \(k\) 级索引就有 \(\frac{n}{2^k}\) 个结点。如果最高级索引有 2 个结点,那总的索引级数 \(k = log_2n - 1\),如果我们算上原始链表的话,那也就是总共有 \(log_2n\) 级。

在第 \(k\) 级索引中,假设我们要查找的数据为 \(x\),当我们查找到 \(y\) 结点时,发现 \(y < x < z\) 时此时我们就要下降到 \(k-1\) 级索引继续查找。在第 \(k-1\) 级索引中,\(y\) 和 \(z\) 之间只有三个结点,因此,我们最多只需要查找 3 个结点。以此类推,每一级的索引最多都只需要遍历 3 个结点

而总的级别数为 \(log_2n\),因此查找的时间复杂度就为 \(3* log_2n = logn\)。跳表查找的时间复杂度和二分查找一样,但这其实是以空间来换时间的设计思路。

跳表的所有额外索引结点总数为 \(\frac{n}{2} + \frac{n}{4} + \frac{n}{8} + ... + 4 + 2 = n-2\),所以跳表的空间复杂度为 \(O(n)\)

但如果我们每三个结点建立一个索引,这时候额外需要的结点总数为 \(\frac{n}{2}\),虽然空间复杂度依然为 \(O(n)\),但减少了一半的索引节点存储空间。

实际上,在实际开发中,原始链表中存储的可能是很大的对象,而索引结点只需要存储关键值和几个指针,其额外占用的空间可以被忽略掉

3. 跳表高效的动态插入和删除?

在链表中,如果我们知道要插入数据的位置,那么插入的时间复杂度就为 \(O(1)\)。在跳表中,查找的时间复杂度为 \(O(logn)\),因此,动态插入数据的时间复杂度也就是 \(O(logn)\) 了。

从链表中删除结点的时候,如果结点在索引中也有出现,那么我们除了要删除原始链表中的结点,还要删除索引中的。

当我们不停地往跳表中插入数据的时候,如果我们不更新索引,就有可能出现某两个结点之间数据非常多的情况。极端情况下,跳表还会退化为单链表。

因此,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表结点变多了,索引值就相应地增加一些

当我们往跳表中插入数据的时候,我们可以选择同时也将这个数据插入到部分索引层中。而插入到哪些索引层中,则由一个随机函数生成一个随机数字来决定。如果这个数字为 K,那我们就将数据插入到第一级到第 K 级索引中。

4. 为什么 Redis 要用跳表来实现有序集合而不是红黑树?

Redis 中的有序集合支持的核心操作主要有以下几个:

  • 插入一个数据
  • 删除一个数据
  • 查找一个数据
  • 按照区间查找数据
  • 迭代输出有序序列

其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度和跳表是一样的。

但是,按照区间查找数据这个操作,红黑树的效率没有跳表高。跳表可以在 \(O(logn)\) 时间复杂度定位区间的起点,然后在原始链表中顺序向后查询就可以了,这样非常高效。

此外,相比于红黑树,跳表还具有代码更容易实现、可读性好、不容易出错、更加灵活等优点,因此 Redis 用跳表来实现有序集合。

参考资料-极客时间专栏《数据结构与算法之美》

获取更多精彩,请关注「seniusen」!

原文地址:https://www.cnblogs.com/seniusen/p/9870398.html

时间: 2024-08-30 05:32:35

数据结构和算法之——跳表的相关文章

java数据结构与算法之顺序表与链表深入分析

转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/52953190 出自[zejian的博客] 关联文章: java数据结构与算法之顺序表与链表设计与实现分析 java数据结构与算法之双链表设计与实现 ??数据结构与算法这门学科虽然在大学期间就已学习过了,但是到现在确实也忘了不少,因此最近又重新看了本书-<数据结构与算法分析>加上之前看的<java数据结构>也算是对数据结构的进一步深入学习了,于是也就打算

C++数据结构与算法_2_线性表 --顺序表的应用示例

h2.western { font-family: "Liberation Sans",sans-serif; font-size: 16pt; }h2.cjk { font-family: "微软雅黑"; font-size: 16pt; }h2.ctl { font-family: "AR PL UMing CN"; font-size: 16pt; }h1 { margin-bottom: 0.21cm; }h1.western { fon

C++数据结构与算法_1_线性表 --顺序表的实现与分析

顺序表的实现与分析 引 --线性表的抽象基类: template <typename T> class LinearList { public: LinearList(); ~LinearList(); virtual int Size() const = 0; //返回线性表所能够存储的最大长度 virtual int Length() const = 0; //当前线性表的长度 virtual int Search(T &x) const = 0; virtual int Loca

数据结构与算法之线性表

前言 上一篇<数据结构和算法之时间复杂度和空间复杂度>中介绍了时间复杂度的概念和常见的时间复杂度,并分别举例子进行了一一说明.这一篇主要介绍线性表. 线性表属于数据结构中逻辑结构中的线性结构.回忆一下,数据结构分为物理结构和逻辑结构,逻辑结构分为线性结构.几何结构.树形结构和图形结构四大结构.其中,线性表就属于线性结构.剩余的三大逻辑结构今后会一一介绍. 线性表 基本概念 线性表(List):由零个或多个数据元素组成的有限序列. 注意: 1.线性表是一个序列. 2.0个元素构成的线性表是空表.

数据结构、算法、线性表总结

一.数据结构 1.概念 1.数据类型和抽象数据类型是不同的概念. 2.逻辑结构分为线性结构(一对一).树形结构(一对多).图形结构(多对多). 3.时间复杂度(T(n)=O(f(n)):空间复杂度(占用的内存空间). 4.算法 1.BF算法:时间复杂度最坏为O(n*m). 2.kmp算法:时间复杂度最坏为O(n+m). 二.线性表 1.概念 1.顺序表结点的存储地址计算公式:Loc(ai)=Loc(a1)+(i-1)*C. 2.操作主要是查找用顺序表,进行插入.删除操作的适合用链表. 3.存储密

python数据结构与算法——哈希表

哈希表 学习笔记 参考翻译自:<复杂性思考> 及对应的online版本:http://greenteapress.com/complexity/html/thinkcomplexity004.html 使用哈希表可以进行非常快速的查找操作,查找时间为常数,同时不需要元素排列有序 python的内建数据类型:字典,就是用哈希表实现的 为了解释哈希表的工作原理,我们来尝试在不使用字典的情况下实现哈希表结构. 我们需要定义一个包含 键->值 映射 的数据结构,同时实现以下两种操作: add(k

数据结构与算法--------哈希表

1.概述: 数据结构:哈希表 插入时间复杂度:O(1) 删除时间复杂度:O(1) 查找时间复杂度: 优点: 哈希表的操作速度比较快,如插入.删除的时间复杂度都是常量O(1),可以在一秒内查找上千条记录 哈希表的编程实现相对容易 缺点: 哈希表不能被填满.哈希表被基本填满的时候,性能会急剧下降,所以为了保证性能,你一定要确保你哈希表的容量的大小是足够的 哈希表到其他哈希表的数据迁移过程是一个费时的过程(如定期将数据迁移到更大的哈希表中去会很费时).如果一开始没有预估好你的数据量的大小,初始时创建的

数据结构、算法及线性表总结

思维导图 重要概念 数据:是能被输入进计算机中,并能被计算操作处理的对象的总称 数据元素:是数据结构中讨论的基本单位 数据类型:整型.浮点型.字符型等等变量所具有的不同的数据种类 存储结构:又称物理结构,是描述数据具体在内存中的存储结构,分为线性结构和非线性结构 逻辑结构:是描述数据之间的相互关系,分为线性结构(一对一).树形结构(一对多).图状结构/网状结构(多对多) 算法:时间复杂度表示算法执行时间与问题规模之间的关系,是对运算时间的一个大致估计:空间复杂度表示算法执行时占用的内存空间 线性

数据结构与算法之----线性表

01线性表 1.线性表的判断方式就是元素有且只有一个直接前驱和直接后继,元素可以为空,此时叫做空表 2.抽象数据类型标准格式 ADT 抽象数据类型名 DATA 数据元素之间逻辑关系的定义 Operation 操作 endADT 3.操作伪代码 Operation InitList(*L): 初始化操作,建立一个空的线性表L ListEmpty(L): 判断线性表是否为空表,如果为空返回true,否则返回false ClearList(*L): 将线性表清空(实际情况不是删除元素,而是将内存中的元