当前位置: 首页> 科技> 数码 > 百年建筑网站_电商该怎么做起_西安网站seo服务_网店推广有哪些方法

百年建筑网站_电商该怎么做起_西安网站seo服务_网店推广有哪些方法

时间:2025/7/14 11:27:28来源:https://blog.csdn.net/2403_87122707/article/details/144151966 浏览次数:0次
百年建筑网站_电商该怎么做起_西安网站seo服务_网店推广有哪些方法

前言

近几年针对 Redis 面试时会涉及常见数据结构的底层设计,其中就有这么一道比较有意思的面试题:“Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。

本文就以这道大厂常问的面试题为切入点,带大家详细了解一下跳表这个数据结构。

本文整体脉络如下图所示,笔者会从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握。

跳表在 Redis 中的运用

这里我们需要先了解一下 Redis 用到跳表的数据结构有序集合的使用,Redis 有个比较常用的数据结构叫有序集合(sorted set,简称 zset),正如其名它是一个可以保证有序且元素唯一的集合,所以它经常用于排行榜等需要进行统计排列的场景。

这里我们通过命令行的形式演示一下排行榜的实现,可以看到笔者分别输入 3 名用户:xiaomingxiaohongxiaowang,它们的score分别是 60、80、60,最终按照成绩升级降序排列。


127.0.0.1:6379> zadd rankList 60 xiaoming
(integer) 1
127.0.0.1:6379> zadd rankList 80 xiaohong
(integer) 1
127.0.0.1:6379> zadd rankList 60 xiaowang
(integer) 1# 返回有序集中指定区间内的成员,通过索引,分数从高到低
127.0.0.1:6379> ZREVRANGE rankList 0 100 WITHSCORES
1) "xiaohong"
2) "80"
3) "xiaowang"
4) "60"
5) "xiaoming"
6) "60"

此时我们通过 object 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是ziplist(压缩列表)

127.0.0.1:6379> object encoding rankList
"ziplist"

因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间,在有序集合元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。

zset-max-ziplist-value 64
zset-max-ziplist-entries 128

一旦有序集合中的某个元素超出这两个其中的一个阈值它就会转为 skiplist(实际是 dict+skiplist,还会借用字典来提高获取指定元素的效率)。

也就是说,ZSet 有两种不同的实现,分别是 ziplist 和 skiplist,具体使用哪种结构进行存储的规则如下:

  • 当有序集合对象同时满足以下两个条件时,使用 ziplist:
    1. ZSet 保存的键值对数量少于 128 个;
    2. 每个元素的长度小于 64 字节。
  • 如果不满足上述两个条件,那么使用 skiplist 。

手写一个跳表

为了更好的回答上述问题以及更好的理解和掌握跳表,这里可以通过手写一个简单的跳表的形式来帮助读者理解跳表这个数据结构。

我们都知道有序链表在添加、查询、删除的平均时间复杂都都是 O(n) 即线性增长,所以一旦节点数量达到一定体量后其性能表现就会非常差劲。而跳表我们完全可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为 O(log n)

可能这里说的有些抽象,我们举个例子,以下图跳表为例,其原始链表存储按序存储 1-10,有 2 级索引,每级索引的索引个数都是基于下层元素个数的一半。

对应的添加也是一个道理,假如我们需要在这个有序集合中添加一个元素 7,那么我们就需要通过跳表找到小于元素 7 的最大值,也就是下图元素 6 的位置,将其插入到元素 6 的后面,让元素 6 的索引指向新插入的节点 7,其工作流程如下:

  1. 从 2 级索引开始定位到了元素 4 的索引。
  2. 查看索引 4 的后继索引为 8,索引向下推进。
  3. 来到 1 级索引,发现索引 4 后继索引为 6,小于插入元素 7,指针推进到索引 6 位置。
  4. 继续比较 6 的后继节点为索引 8,大于元素 7,索引继续向下。
  5. 最终我们来到 6 的原始节点,发现其后继节点为 7,指针没有继续向下的空间,自此我们可知元素 6 就是小于插入元素 7 的最大值,于是便将元素 7 插入。

这里我们又面临一个问题,我们是否需要为元素 7 建立索引,索引多高合适?

我们上文提到,理想情况是每一层索引是下一层元素个数的二分之一,假设我们的总共有 16 个元素,对应各级索引元素个数应该是:

 

 由此我们用数学归纳法可知:

而 Redis 又是内存数据库,我们假设元素最大个数是65536,我们把65536代入上述公式可知最大高度为 16。所以我们建议添加一个元素后为其建立的索引高度不超过 16。

因为我们要求尽可能保证每一个上级索引都是下级索引的一半,在实现高度生成算法时,我们可以这样设计:

  1. 跳表的高度计算从原始链表开始,即默认情况下插入的元素的高度为 1,代表没有索引,只有元素节点。
  2. 设计一个为插入元素生成节点索引高度 level 的方法。
  3. 进行一次随机运算,随机数值范围为 0-1 之间。
  4. 如果随机数大于 0.5 则为当前元素添加一级索引,自此我们保证生成一级索引的概率为 50% ,这也就保证了 1 级索引理想情况下只有一半的元素会生成索引。
  5. 同理后续每次随机算法得到的值大于 0.5 时,我们的索引高度就加 1,这样就可以保证节点生成的 2 级索引概率为 25% ,3 级索引为 12.5% ……

我们回过头,上述插入 7 之后,我们通过随机算法得到 2,即要为其建立 1 级索引:

 

最后我们再来说说删除,假设我们这里要删除元素 10,我们必须定位到当前跳表各层元素小于 10 的最大值,索引执行步骤为:

  1. 2 级索引 4 的后继节点为 8,指针推进。
  2. 索引 8 无后继节点,该层无要删除的元素,指针直接向下。
  3. 1 级索引 8 后继节点为 10,说明 1 级索引 8 在进行删除时需要将自己的指针和 1 级索引 10 断开联系,将 10 删除。
  4. 1 级索引完成定位后,指针向下,后继节点为 9,指针推进。
  5. 9 的后继节点为 10,同理需要让其指向 null,将 10 删除。

 

模板定义

有了整体的思路之后,我们可以开始实现一个跳表了,首先定义一下跳表中的节点Node,从上文的演示中可以看出每一个Node它都包含以下几个元素:

  1. 存储的value值。
  2. 后继节点的地址。
  3. 多级索引。

为了更方便统一管理Node后继节点地址和多级索引指向的元素地址,笔者在Node中设置了一个forwards数组,用于记录原始链表节点的后继节点和多级索引的后继节点指向。

以下图为例,我们forwards数组长度为 5,其中索引 0记录的是原始链表节点的后继节点地址,而其余自底向上表示从 1 级索引到 4 级索引的后继节点指向。

于是我们的就有了这样一个代码定义,可以看出笔者对于数组的长度设置为固定的 16**(上文的推算最大高度建议是 16),默认data为-1,节点最大高度maxLevel初始化为 1,注意这个maxLevel**的值代表原始链表加上索引的总高度。

元素添加

定义好节点之后,我们先实现以下元素的添加,添加元素时首先自然是设置data这一步我们直接根据将传入的value设置到data上即可。

然后就是高度maxLevel的设置 ,我们在上文也已经给出了思路,默认高度为 1,即只有一个原始链表节点,通过随机算法每次大于 0.5 索引高度加 1,由此我们得出高度计算的算法

然后再设置当前要插入的NodeNode索引的后继节点地址,这一步稍微复杂一点,我们假设当前节点的高度为 4,即 1 个节点加 3 个索引,所以我们创建一个长度为 4 的数组maxOfMinArr ,遍历各级索引节点中小于当前value的最大值。

假设我们要插入的value为 5,我们的数组查找结果当前节点的前驱节点和 1 级索引、2 级索引的前驱节点都为 4,三级索引为空。

元素查询

查询逻辑比较简单,从跳表最高级的索引开始定位找到小于要查的 value 的最大值,以下图为例,我们希望查找到节点 8:

  1. 跳表的 3 级索引首先找找到 5 的索引,5 的 3 级索引 forwards[3] 指向空,索引直接向下。
  2. 来到 5 的 2 级索引,其后继 forwards[2] 指向 8,继续向下。
  3. 5 的 1 级索引 forwards[1] 指向索引 6,继续向前。
  4. 索引 6 的 forwards[1] 指向索引 8,继续向下。
  5. 我们在原始节点向前找到节点 7。
  6. 节点 7 后续就是节点 8,继续向前为节点 8,无法继续向下,结束搜寻。
  7. 判断 7 的前驱,等于 8,查找结束。

元素删除

最后是删除逻辑,需要查找各层级小于要删除节点的最大值,假设我们要删除 10:

  1. 3 级索引得到小于 10 的最大值为 5,继续向下。
  2. 2 级索引从索引 5 开始查找,发现小于 10 的最大值为 8,继续向下。
  3. 同理 1 级索引得到 8,继续向下。
  4. 原始节点找到 9。
  5. 从最高级索引开始,查看每个小于 10 的节点后继节点是否为 10,如果等于 10,则让这个节点指向 10 的后继节点,将节点 10 及其索引交由 GC 回收。

和其余三种数据结构的比较 

 

最后,我们再来回答一下文章开头的那道面试题: “Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。

平衡树 vs 跳表

先来说说它和平衡树的比较,平衡树我们又会称之为 AVL 树,是一个严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过 1,即平衡因子为范围为 [-1,1])。平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)

对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。

跳表诞生的初衷就是为了克服平衡树的一些缺点,跳表的发明者在论文《Skip lists: a probabilistic alternative to balanced trees》中有详细提到: 

红黑树 vs 跳表

红黑树(Red Black Tree)也是一种自平衡二叉查找树,它的查询性能略微逊色于 AVL 树,但插入和删除效率更高。红黑树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)

红黑树是一个黑平衡树,即从任意节点到另外一个叶子叶子节点,它所经过的黑节点是一样的。当对它进行插入操作时,需要通过旋转和染色(红黑变换)来保证黑平衡。不过,相较于 AVL 树为了维持平衡的开销要小一些。关于红黑树的详细介绍,可以查看这篇文章:红黑树。

相比较于红黑树来说,跳表的实现也更简单一些。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。

 

B+树 vs 跳表

想必使用 MySQL 的读者都知道 B+树这个数据结构,B+树是一种常用的数据结构,具有以下特点:

  1. 多叉树结构:它是一棵多叉树,每个节点可以包含多个子节点,减小了树的高度,查询效率高。
  2. 存储效率高:其中非叶子节点存储多个 key,叶子节点存储 value,使得每个节点更够存储更多的键,根据索引进行范围查询时查询效率更高。-
  3. 平衡性:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为 O(log n)
  4. 顺序访问:叶子节点间通过链表指针相连,范围查询表现出色。
  5. 数据均匀分布:B+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。

所以,B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。

关键字:百年建筑网站_电商该怎么做起_西安网站seo服务_网店推广有哪些方法

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: