这玩意叫跳表?
引言
跳表是一种用于数据查找的数据结构,它虽然不是常见的数据结构,但是在 Redis、Hbase 等中间件中却被广泛使用,是一款性能比较优秀的底层数据结构,可以支持高速的数据查找、删除以及插入。这种数据结构是由 William Pugh 发明的,最早出现于他在 1990 年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》,以下是论文中关于跳表的描述。
Skip lists are a data structure that can be used in place of balanced trees. Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.
跳表介绍跳表结构
在说明跳表数据结构特点之前,我们先来回顾下链表的数据结构。如下图所示:
在链表的数据结构中,如果我们要检索某个数据,就需要从链表的头节点开始进行遍历,直到找到需要获取的节点。如果运气不好的话,目标节点在末尾的话检索的次数随着链表长度的增加而增加。因此链表的数据查找的时间复杂度为 O(n),时间复杂度比较高,那么有没有办法进行优化呢?
有一点算法基础的同学都知道,在数据检索的算法当中有一个二分查找法,它的大致思想就是在有序的列表中,将数据一分为二,左边的数据都是比比较点小的,右边都是比比较点大的。这样当进行数据比较检索的时候,先和这个中间的基准点数据进行比较,比基准点小的就往右边进行检索,比基准点大的话,就往右边进行检索,通过二分查找可以有效提升数据检索的效率。那么这种二分查找的思想可不可以运用到链表的数据结构上来进行检索效率优化呢?
通过中间节点进行比较,这样可以快速定位出目标值大致的区间,避免无效的数据检索过程,从而可以降借助于二分查找的思想,如果我们在检索数据的时候,不老老实实的从链表的头节点一个一个进行比较,而是减少检索数据所需要的步骤,达到降低时间复杂度的目的。
基于以上的分析,大神想出了在原始的链表数据之上,增加一层链表索引,增加的链表索引是对原始链表的数据抽取。如下图所示,我们检索数字 27,不再是一个一个链表的进行检索比较了而是先通过 L1 层的索引进行初步定位。先和 2 比较,比 2 大则直接与 13 比较,比 13 大则继续与 24 比较,比 24 大继续与 33 比较,结果比 33 小,那么进入 L0 层比较最终找到目标值 27。如果在 L1 层之上再增加一级索引 Level2 2-》24,那么首先直接和 2 比较,比 2 大直接跳到 24 节点。
复杂度分析
在上文中我们提到,对于单链表来说它的时间复杂度是 O(n)。那么增加了多级索引之后的跳表,它的时间复杂度是多少呢?在分析跳表的时间复杂度之前,我们先来分析下对于一个长度为 n 的链表来说,它的多级索引应该设置为多少比较合适。
假设我们在原始的链表中每两个节点提取出一个节点作为一级索引的节点的话,那么 Level1 层就有 n/2 个节点,而 Level2 层的索引就有个节点,那么以此类推,第 x 层索引的节点个数就是 n/个节点。
我们再假设最高层级为 m,对应的节点个数为 2,那么我们可以获得这样的等式,那么我们可以得到最高层的 m 值为 m=log2n-1,结合最初始的链表,整个跳表的高度就是 log2n,综合整个数据检索的过程,跳表检索的时间复杂度为 O(logn)。
跳表在 Redis 中的应用
Sorted Set 是 Redis 中比较重要的一种基础数据结构,而 Sorted Set 核心数据结构就采用了跳表结构来实现的,同时还使用了哈希表进行索引。那么为什么 Sorted Set 要实用两种数据结构呢?主要是 Sorted Set 既支持了范围查询也支持获取元素的权重值。当然,本文重点阐述跳表在 Sorted Set 中的应用。对于 Sorted Set 来说,在 Redis 的源码中对应的结构体是 zset,其中包含了哈希表结构 dict 以及跳表结构 zsl,具体如下所示:
通过上文介绍,我们知道跳表的本质就是一个拥有多层索引的有序链表,那么我们再来看下在 Redis 中是如何定义跳表结构的,如下所示:
上文中我们提到过跳表的本质是具备多级索引的链表,因此只要知道了头节点或者尾节点就可以访问整个跳表数据了。另外跳表长度表示跳表中包含的跳表节点数,而 level 则表示跳表拥有多少层检索索引。了解了跳表的结构之后,我们在深入看下跳表中的节点是怎样的结构,如下所示:
源码当中的 zskiplistNode 实际上就是链表中的节点结构体了,其中 Sorted Set 对应的数据值以及值对应的权重都是在 zskiplistNode 中进行定义的。另外在 zskiplistNode 结构体中还定义了后向指针 backward,它的作用是指向当前节点的前一个节点,主要作用是方便进行节点的倒序查找。
另外 zskiplistNode 还定义了 zskiplistLevel 数组,怎么理解它呢?如下面的跳表结构图所示,跳表的每一层中共同的节点如节点 24,所以这个 zskiplistLevel 数组中存储了向下个节点检索的指针以及跨度数据。
Redis 中跳表的数据检索流程大致如下:
总结
本文主要对跳表这个不常用的数据结构进行了介绍,同时分析了二分查找思想在跳表中的应用,重点阐述了跳表的数据结构特征。另外结合了 Redis 中 Sorted Set 分析了跳表在该基础数据结构中的实际应用,希望通过本文大家对于跳表这个数据结构能够有更加深刻的理解,同时切身体会用空间换时间的优化策略。
版权声明: 本文为 InfoQ 作者【慕枫技术笔记】的原创文章。
原文链接:【http://xie.infoq.cn/article/4818db5238df804efdd4bd776】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论