redis 链表结构相对简单一些 我们从各种链表的区别,到源码,最后到一次redis命令的底层操作来介绍它。
一、链表
1.1 介绍
关于链表的介绍,自己理解后组织的语言或者各种博客的介绍总觉得差点意思,所以直接引用维基百科的链表介绍。
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接("links")。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
1.2 图解
下面三图很好解释了介绍中三种链表的结构。文中GIF图使用mac自带keynote的导出gif功能制作。
1.2.1 单链表
链表中最简单的一种是单向链表,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
一个单向链表的节点被分成两个部分。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址。单向链表只可向一个方向遍历。
链表最基本的结构是在每个节点保存数据和到下一个节点的地址,在最后一个节点保存一个特殊的结束标记,另外在一个固定的位置保存指向第一个节点的指针,有的时候也会同时储存指向最后一个节点的指针。一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。但是也可以提前把一个节点的位置另外保存起来,然后直接访问。当然如果只是访问数据就没必要了,不如在链表上储存指向实际数据的指针。这样一般是为了访问链表中的下一个或者前一个(需要储存反向的指针,见下面的双向链表)节点。
相对于下面的双向链表,这种普通的,每个节点只有一个指针的链表也叫单向链表,或者单链表,通常用在每次都只会按顺序遍历这个链表的时候(例如图的邻接表,通常都是按固定顺序访问的)。
1.2.2 双向链表
每个节点有两个连接:一个指向前一个节点,(当此“连接”为第一个“连接”时,指向空值或者空列表);而另一个指向下一个节点,(当此“连接”为最后一个“连接”时,指向空值或者空列表)。Redis中的List结构也是基于双向链表实现的。
1.2.3 循环链表
在一个 循环链表中, 首节点和末节点被连接在一起。这种方式在单向和双向链表中皆可实现。要转换一个循环链表,你开始于任意一个节点然后沿着列表的任一方向直到返回开始的节点。再来看另一种方法,循环链表可以被视为“无头无尾”。这种列表很利于节约数据存储缓存, 假定你在一个列表中有一个对象并且希望所有其他对象迭代在一个非特殊的排列下。图就不搞了,偷个懒。
1.2.4 扩展
对于非线性的链表,可以参见相关的其他数据结构,例如树、图。另外有一种基于多个线性链表的数据结构:跳表,插入、删除和查找等基本操作的速度可以达到O(nlogn),和平衡树一样。
二、Redis中的List结构
2.1 为什么是双向链表
Redis为什么选择双向链表来实现List结构?几种链表的区别优缺点搜索引擎都详细的告诉了我们,先从需求点说起吧!我们需要List结构做什么?
-
堆栈,双向列表的前后指针支持实现先进先出和先进后出,而单链表只可以先进先出。
-
.插入,单链表只可以尾部插入,双链表可以头尾插入。
-
.查找,单链表只可以通过遍历查找,双链表可以随机从任意节点进行前后查找或者二分查找。
-
删除,虽然两种链表的删除都是O(1),但是我们需要先查找到需要删除的节点,同上。而且双链表支持删除尾部节点而不需要遍历。
当然也是有缺点的,不过对于追求性能的Redis来说,空间换时间已经是家常便饭。
2.2 如何实现
我们已经知道为什么Redis选择双向链表,好在redis的链表相关源码可读性相当奈斯,无需过多的文字解释下面我们从源码来看一下它在Redis中的运用。
2.2.1 List结构体定义
源码文件在 redis/src/adlist.h
2.2.2 方法及接口定义
源码文件在 redis/src/adlist.h
2.3 具体实现
2.3.1 创建列表
2.3.2 释放列表
2.3.3 添加头尾
2.3.4 插入节点
2.3.5 删除节点
2.3.6 迭代
2.3.7 寻找元素
2.3.8 通过index获取元素
2.3.9 连接列表
2.3.10 链表在Redis中的运用
如发布订阅、慢查询、监视器,日志队列,客户端信息,从服务器列表,慢日志记录都是使用list结构来储存。我们来搜索一下,很多地方都用到了奥
三、总结
Redis链表结构其主要特性如下:(摘自Redis设计与实现)
-
双向:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为0(1)。
-
无环: 链表为非循环链表表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以NULL为终点。
-
带表头指针和表尾指针:通过list结构中的head和tail指针,获取表头和表尾节点的时间复杂度都为O(1)。
-
带链表长度计数器:通过list结构的len属性获取节点数量的时间复杂度为O(1)。
-
多态:链表节点使用void*指针保存节点的值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用来保存各种不同类型的值。
分别使用数组、单链表和双向链表实现列表对象的时间复杂度对照如下:
操作\时间复杂度 | 数组 | 单链表 | 双向链表 |
---|---|---|---|
rpush(从右边添加元素) | O(1) | O(1) | O(1) |
lpush(从左边添加元素) | 0(N) | O(1) | O(1) |
lpop (从右边删除元素) | O(1) | O(1) | O(1) |
rpop (从左边删除元素) | O(N) | O(1) | O(1) |
lindex(获取指定索引下标的元素) | O(1) | O(N) | O(N) |
len (获取长度) | O(N) | O(N) | O(1) |
linsert(向某个元素前或后插入元素) | O(N) | O(N) | O(1) |
lrem (删除指定元素) | O(N) | O(N) | O(N) |
lset (修改指定索引下标元素) | O(N) | O(N) | O(N) |