数据结构源码分析面试题

面试题

  • Redis 的跳跃列表了解吗?这个数据结构有什么缺点
  • Redis 项目里面怎么用?Redis 的数据结构都了解哪些?布隆过滤器怎么用?
  • Redis 的多路 IO 复用如何理解,为什么单线程还可以抗那么高的 QPS
  • Redis 的 zset 底层实现,压缩列表和跳表,这样设计的优缺点
  • Redis 的跳表说一下,解决了哪些问题,时间复杂度和空间复杂度如何
  • Redis 的 zset 用的什么数据结构

Redis 数据类型的底层数据结构

  • SDS 动态字符串
  • 双向链表
  • 压缩列表 ziplist
  • 哈希表 hashtable
  • 跳表 skiplist
  • 整数集合 intset
  • 快速列表 quicklist
  • 紧凑列表 listpack

skiplist 跳表面试题⭐️

为什么引出跳表

单链表

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高 O(N)

Reids-原始链表

痛点

数组和链表各有优缺点,链表遍历的时候,时间复杂度最差会出现 O(N) ,我们优化下一下,尝试(升维)空间换时间,给链表加个索引,称为“索引升级”,两两取首即可。

  • 优化1

    Redis-skiplist-索引升级

  • 优化2

    画了一个包含 64 个节点的链表,按照前面讲的这种思路,建立了五级索引

    Redis-skiplist-索引升级2

概述

  • 跳表是可以实现二分查找的有序链表

    skiplist 是一种以空间换取时间的结构。由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表。但是,由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多

  • 跳表 = 链表 + 多机索引

跳表时间/空间复杂度介绍

  • 跳表的时间复杂度 O(logN)

    跳表查询的时间复杂度分析,如果链表里有N个结点,会有多少级索引呢?

    按照我们前面讲的,两两取首。每两个结点会抽出一个节点作为上一级索引的节点,一次估算:

    第一级索引的结点个数大约就是 n/2,

    第二级索引的结点个数大约就是 n/4,

    第三级索引的结点个数大约就是 n/8,依次类推......

    也就是说,第k级索引的结点个数是第 k-1级索引的结点个数的 1/2,那第k级索引结点的个数就是 n/(2^k)

    假设索引有 h 级,最高级的索引有 2 个结点。通过上面的公式,我们可以得到 n / (2^h) = 2,从而求得 h=log2n - 1 ,如果包含原始链表这一层,整个跳表的高度就是 log2n

    n/2^k → n/2^k = 2 → h = log2n - 1 → O(logn)

  • 跳表的空间复杂度 O(N)

    跳表查询的空间复杂度分析

    比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间。那到底需要消耗多少额外的存储空间呢?

    第一步:首先原始链表长度为 n;

    第二步:两两取首,每层索引的节点数:n/2,n/4,n/8 ... 8,4,2 每上升一级就减少一半,直到剩下 2 个节点,以此类推;如果我们把每层索引的节点数写出来,就是一个等比数列。

    Redis-跳表空间复杂度分析1

    这几级索引的结点总和就是 n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是 O(n) 。也就是说,如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间。

    第三步:思考三三取首,每层索引的结点数:n/3, n/9, n/27 ... , 9, 3, 1 以此类推;

    第一级索引需要大约 n/3 个结点,第二级索引需要大约 n/9 个结点。每往上一级,索引结点个数都除以 3。为了方便计算,我们假设最高一级的索引结点个数是 1。我们把每级索引的结点个数都写下来,也是一个等比数列

    Redis-跳表空间复杂度分析2

    通过等比数列求和公式,总的索引结点大约就是 n/3+n/9+n/27+…+9+3+1=n/2。尽管空间复杂度还是 O(n) ,但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。所以空间复杂度是 O(n);

优缺点

  • 优点

    跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的

  • 缺点

    维护成本相对较高,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是 O(1),但是新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是 O(log n)