当前位置:首页 > 技术分析 > 正文内容

数据结构之跳表:链表+索引表,解决链表查找效率低的问题

ruisui884周前 (04-01)技术分析9

我们知道,不借助额外空间的情况下,在链表中查找一个值,需要按照顺序一个个查找,时间复杂度为 O(N),其中 N 为链表长度。

当链表长度很大的时候, 这种时间是很难接受的。 一种常见的的优化方式是建立哈希表,将所有节点都放到哈希表中,以空间换时间的方式减少时间复杂度,这种做法时间复杂度为 O(1),但是空间复杂度为 O(N)。

为了防止链表中出现重复节点带来的问题,我们需要序列化节点,再建立哈希表,这种空间占用会更高,虽然只是系数级别的增加,但是这种开销也是不小的 。更重要的是,哈希表不能解决查找极值的问题,其仅适合根据 key 来获取内容。

为了解决上面的问题,跳表(skip list)应运而生。

如下图所示,我们从链表中每两个元素抽出来,加一级索引,一级索引指向了原始链表,即:通过一级索引 7 的 down 指针可以找到原始链表的 7 。那怎么查找 10 呢?

注意这个算法要求链表是有序的。

我们可以:

通过现在一级跳表中搜索到 7,发现下一个 18 大于 10 ,也就是说我们要找的 10 在这两者之间。

通过 down 指针回到原始链表,通过原始链表的 next 指针我们找到了 10。

这个例子看不出性能提升。但是如果元素继续增大, 继续增加索引的层数,建立二级,三级……索引,使得链表能够实现二分查找,从而获得更好的效率。但是相应地,我们需要付出额外空间的代价。

理解了上面的点,你可以形象地将跳表想象为玩游戏的存档

一个游戏有 10 关。如果我想要玩第 5 关的某一个地方,那么我可以直接从第五关开始,这样要比从第一关开始快。我们甚至可以在每一关同时设置很多的存档。这样我如果想玩第 5 关的某一个地方,也可以不用从第 5 关的开头开始,而是直接选择离你想玩的地方更近的存档,这就相当于跳表的二级索引。

跳表的时间复杂度和空间复杂度不是很好分析。由于时间复杂度 = 索引的高度 * 平均每层索引遍历元素的个数,而高度大概为 logn,并且每层遍历的元素是常数,因此时间复杂度为 logn,和二分查找的空间复杂度是一样的。

在单链表中,一旦定好要插入的位置,时间复杂度是很低的为O(1),但是,为了保持原始链表中数据的有序性,通常需要遍历链表找到需要插入的位置,这个查找操作会比较耗时。但是在跳表中,时间复杂度为O(logn)。

空间复杂度就等同于索引节点的个数,以每两个节点建立一个索引为例,大概是 n/2 + n/4 + n/8 + … + 8 + 4 + 2 ,因此空间复杂度是 O(n)。当然你如果每三个建立一个索引节点的话,空间会更省,但是复杂度不变。

跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 O(logn)。跳表的空间复杂度是 O(n)。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。跳表可以用来实现堆(区别于基于链表的实现,另一种是基于数组的实现 - 二叉堆)。

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

作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。

而跳表是通过随机函数来维护前面提到的“平衡性”。当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。


如果一个节点存在K个向前的指针的话,那么该节点就是K层的节点。跳表的最大层数为节点中所以节点中最大的层数。

#define SKIP_LIST_MALLOC(size)   rt_malloc(size);
#define SKIP_LIST_CALLOC(n,size) rt_calloc(n,size);
#define SKIP_LIST_FREE(p)        rt_free(p);
 
struct skip_list_node
{
	/*key是唯一的*/
	int key;       
	/*存储的内容*/
	int value;     
	/*当前节点最大层数*/
	int max_level; 
	/*柔性数组,根据该节点层数的不同指向大小不同的数组
	 *next[0]表示该节点的第一层下一节点的索引地址
	 *next[1]表示该节点的第二层下一节点的索引地址
	 *next[n]表示该节点的第n层下一节点的索引地址
	 */
	struct skip_list_node *next[];
};
 
struct skip_list
{
	int level; /*跳表的索引层数*/
	int num;   /*节点数目*/
	struct skip_list_node *head;
};
 
extern struct skip_list* skip_list_creat(int max_level);
extern int skip_list_insert (struct skip_list *list, int key, int value);
extern int skip_list_delete (struct skip_list *list, int key);
extern int skip_list_modify (struct skip_list *list, int key, int value);
extern int skip_list_search (struct skip_list *list, int key, int *value);
extern int skip_list_destroy(struct skip_list *list);

ref:

https://blog.csdn.net/m0_37845735/article/details/103691814

https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/thinkings/heap#tiao-biao

-End-

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3219.html

标签: 查看表索引
分享给朋友:

“数据结构之跳表:链表+索引表,解决链表查找效率低的问题” 的相关文章

如何在 Linux 发行版中安装微信和 QQ?

很多人因为工作沟通的原因需要用到微信和 QQ,那么如何在 Linux 发行版中安装微信和 QQ 呢?以下是一些尝试的解决方法。QQ上一个版本的 QQ Linux 版还是在2009年,而在现在,基于 NT 架构的全新 QQ Linux版已经被正式推出,为所有用户提供下载。新版本提供了deb、rpm、A...

2024前端面试真题之—VUE篇

添加图片注释,不超过 140 字(可选)1.vue的生命周期有哪些及每个生命周期做了什么? beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。 created在实例创建完成后发生,当前阶段已...

面试官:聊聊你知道的Vue与React的区别

最近面到很多大公司的时候,小编都会碰到一个很尴尬的问题,很多大公司的技术栈都是React,但是小编学的是Vue,其实从本质上来说两者都是比较优秀的前端框架,所以有些面试官会问到Vue和React的区别。小编认真整理了一些自己所知道的Vue和React的区别,给大家分享分享。1. 模板语法 vs JS...

Vue.js 组件通信的 3 大妙招

在 Vue.js 中,组件化是其核心概念之一,允许你将复杂的界面拆分成多个独立的、可复用的组件。在构建大型应用时,如何高效地在组件之间传递数据和触发事件是非常重要的。Vue.js 提供了多种方式来处理组件间的通信,下面是最常用的 3 种方式:1.父子组件通信:通过 Props 和 Events在 V...

继Yuzu后,任天堂要求移除多个Switch模拟器项目

IT之家 7 月 11 日消息,任天堂美国分公司 (Nintendo of America) 已要求移除多个用于模拟 Nintendo Switch 游戏的开源模拟器项目,其中包括 Suyu、Nzu、Uzuy、Torzu、Sudachi 和 Yuzu-vanced 等。这些模拟器均被指控包含绕过任天...

深度解析!AI智能体在To B领域应用,汽车售后服务落地全攻略

在汽车售后服务领域,AI智能体的应用正带来一场效率和专业度的革命。本文深度解析了一个AI智能体在To B领域的实际应用案例,介绍了AI智能体如何通过提升服务顾问和维修技师的专业度及维修效率,优化汽车售后服务流程。上周我分享了AI智能体+AI小程序To C的AI应用场景《1000%增长!我仅用一个小时...