Redis 跳表(一种高性能的动态数据结构)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 一、前言 跳表(Skip List)这种数据结构在一般的数据结构书籍和算法书籍里都不怎么涉及----至少我大学数据结构课程没有讲这种结构。但是跳表确实是一种性能比较优秀的动态数据结构,并且Redis中的有序集合(Sorted Set)就是用跳表实现的。

一、前言

 跳表(Skip List)这种数据结构在一般的数据结构书籍和算法书籍里都不怎么涉及----至少我大学数据结构课程没有讲这种结构。但是跳表确实是一种性能比较优秀的动态数据结构,并且Redis中的有序集合(Sorted Set)就是用跳表实现的。本文先大致了解一下跳表。

二、跳表

1、引出

对于一组有序数据,我们想要在其中查找某个数,如果数据使用数组存储,显然二分法是再适合不过了,但是如果数据是用链表存储的呢?难道我们用从头遍历吗?这样时间复杂度会达到O(n)级别,相比二分法O(logn)级别简直天壤地别。那么如何提高效率呢?

链表的查找时间复杂度算法为1/n(1+2+3+…+n)=O(n),数据出现的位置从第一个到最后一个的概率均为1/n,但是查询时间分别是1,2,,,n,所以平均时间复杂度为O(n)。

2、跳表

我用图片形式来理解跳表。

如下图,对初始链表做一层“索引”,每两个节点提取一个节点到上一层,然后用down指针连接到下一层。

现在我们查询16这个节点。从第一级索引开始,找到13,并且下一个为17,显然16在这两个节点之间,利用down指针下降一层,这次我们遍历2次即可。利用索引后,遍历了5+2=7次,而原始链表需要10次,这里加一层索引遍历次数减少了,效率提高了一点,但还不够,我们继续往上添加索引层。

这里我不再算了,结果是6次,效率又提高了!

那么这种链表加多级索引就是跳表的结构了。

3、链表查询的时间复杂度是多少?

①假设链表有N个节点,按图所示依次往上级添加索引,第一级有N/2个节点,第二级有N/4个节点。。。。

那么第k级索引有N/(2^k)个节点。

②假设索引有h级,最高级有2个节点。就有N/(2^h)=2--->h=log2N-1(以2为底N的对数)。加上原始链表,那么高度就是log2N了。

③查询时,如果每层都遍历m次(最高级最多遍历2次,其他级最多遍历m次)那么复杂度就是O(mlgN)(在大O表示法里,logaN级别的复杂度等于lnN复杂度,去掉m就是O(logn)级别了),我们求一下m的值。

在第k级时,我们遍历到了y和z,查询值介于两者之间,通过down指针,到达第k-1级,而这一级y和z之间最多有3个节点,那么m就等于3了。

综上跳表的查询时间复杂度就是O(logn)了。

4、跳表的耗费空间吗?

这个问题就很简单了,空间复杂度就是每层节点和,即n/2+n/4+...+4+2=n-2,空间复杂度就是O(n)了。

当然了这里也可以扩大索引间隔,减少一点索引空间,但是我们还是按照上面的求时间复杂度方法,得到的结果仍是O(logn)。实际应用中,向来是比较乐意用空间换时间的,所以这种做法的意义并不大,因为时间复杂度还是没变!

并且这里我们使用整数作为例子来讲得,软件开发中,链表存的可能是一个很大的对象,索引只需要记录关键字和一些指针,那么额外的空间和原数据相比完全可以忽略!

5、高效的动态插入和删除

我想汉字没有图片表达的清楚,所以还是用图片来表述!

时间复杂度仍是O(logn)。我们说单链表插入复杂度为O(1),但是这是指插入动作,并不包含查找插入点所耗的时间,加上查找时间O(n),跳表效率还是高一点!

删除要麻烦一点,因为删除的节点要是在索引中,我们还得更新索引,更新索引就得找到前驱节点,当然双链表可以不用考虑了!

6、索引动态更新

考虑这样一种情况

更极端的可以退化成单链表,所以索引的动态更新是必要的!

AVL树是通过左右旋转保持平衡性,而跳表是通过随机函数生成一个值K,然后将节点添加到第一级到第K级索引中。

关于这个随机函数,我没有做深入研究(水平有限,然后有兴趣可以参考redis中关于有序集合的跳表实现)

三、Redis为什么选择跳表?

严格讲Redis还用到了散列表,但是本文讲的是跳表,所以暂时忽略!

在Redis开发手册中,有序集合支持的操作有

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

前面可以用其他数据结构完成----比如红黑树,但是最后一个显然用跳表去定位起点(然后逐条输出)更好实现!再者跳表的代码虽然很难但是比起红黑树相对起来要好实现。

但是。。。。。。。红黑树有现成的实现,直接拿来用,而跳表却需要自己实现。。。。。。。

四、简单的跳表实现代码

代码由王争老师完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package skiplist;
 
import java.util.Random;
 
/**
 * 跳表的一种实现方法。
 * 跳表中存储的是正整数,并且存储的是不重复的。
 *
 * Author:ZHENG
 */
public class SkipList {
 
  private static final int MAX_LEVEL = 16;
 
  private int levelCount = 1;
 
  private Node head = new Node();  // 带头链表
 
  private Random r = new Random();
 
  public Node find(int value) {
    Node p = head;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
    }
 
    if (p.forwards[0] != null && p.forwards[0].data == value) {
      return p.forwards[0];
    } else {
      return null;
    }
  }
 
  public void insert(int value) {
    int level = randomLevel();
    Node newNode = new Node();
    newNode.data = value;
    newNode.maxLevel = level;
    Node update[] = new Node[level];
    for (int i = 0; i < level; ++i) {
      update[i] = head;
    }
 
    // record every level largest value which smaller than insert value in update[]
    Node p = head;
    for (int i = level - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;// use update save node in search path
    }
 
    // in search path node next node become new node forwords(next)for (int i = 0; i < level; ++i) {
      newNode.forwards[i] = update[i].forwards[i];
      update[i].forwards[i] = newNode;
    }
 
    // update node hightif (levelCount < level) levelCount = level;
  }
 
  public void delete(int value) {
    Node[] update = new Node[levelCount];
    Node p = head;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;
    }
 
    if (p.forwards[0] != null && p.forwards[0].data == value) {
      for (int i = levelCount - 1; i >= 0; --i) {
        if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
          update[i].forwards[i] = update[i].forwards[i].forwards[i];
        }
      }
    }
  }
 
  // 随机 level 次,如果是奇数层数 +1,防止伪随机private int randomLevel() {
    int level = 1;
    for (int i = 1; i < MAX_LEVEL; ++i) {
      if (r.nextInt() % 2 == 1) {
        level++;
      }
    }
 
    return level;
  }
 
  public void printAll() {
    Node p = head;
    while (p.forwards[0] != null) {
      System.out.print(p.forwards[0] + " ");
      p = p.forwards[0];
    }
    System.out.println();
  }
 
  public class Node {
    private int data = -1;
    private Node forwards[] = new Node[MAX_LEVEL];
    private int maxLevel = 0;
 
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("{ data: ");
      builder.append(data);
      builder.append("; levels: ");
      builder.append(maxLevel);
      builder.append(" }");
 
      return builder.toString();
    }
  }
 
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
10天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
14天前
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
52 8
|
14天前
|
存储 NoSQL Java
介绍下Redis 的基础数据结构
本文介绍了Redis的基础数据结构,包括动态字符串(SDS)、链表和字典。SDS是Redis自实现的动态字符串,避免了C语言字符串的不足;链表实现了双向链表,提供了高效的操作;字典则类似于Java的HashMap,采用数组加链表的方式存储数据,并支持渐进式rehash,确保高并发下的性能。
介绍下Redis 的基础数据结构
|
9天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树
|
10天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
|
30天前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
67 6
|
1月前
|
缓存 NoSQL 关系型数据库
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
本文深入探讨了Redis缓存的相关知识,包括缓存的概念、使用场景、可能出现的问题(缓存预热、缓存穿透、缓存雪崩、缓存击穿)及其解决方案。
154 0
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
|
7天前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
61 22