Redis链表为双向无环链表!html
图解Redis之数据结构篇——简单动态字符串SDS提到Redis使用了简单动态字符串,链表,字典(散列表),跳跃表,整数集合,压缩列表这些数据结构来操做内存,而且简单介绍了Redis简单动态字符串。本篇文章咱们继续来分析链表。面试
链表是一种很是常见的数据结构,在Redis中使用很是普遍,列表对象的底层实现之一就是链表。其它如慢查询,发布订阅,监视器等功能也用到了链表。redis
数组须要一块连续的内存来存储,这个特性有利也有弊。好处是其支持根据索引下标"随机访问"(时间复杂度为O(1)),可是其插入与删除操做为了保证在内存中的连续性将会变得很是低效(时间复杂度为O(N)),而且其一经声明就要占用整块连续内存空间,若是声明过大,系统可能内存不足,声明太小又可能致使不够用,而当数组的空间不足的时候须要对其进行扩容(申请一个更大的空间,将原数组拷贝过去)。编程
而链表偏偏相反,其不须要一块连续的内存空间,其经过"指针"将一组零散的内存链接起来使用。其优势在于自己没有大小限制,自然支持扩容,插入删除操做高效(时间复杂度为O(1)),但缺点是随机访问低效(时间复杂度为O(N))。而且因为须要额外的空间存储指针。数组
链表的实现方式有不少种,常见的主要有三个,单向链表、双向链表、循环链表。数据结构
单链表中每一个节点除了包含数据以外还包含一个指针,叫后继指针,所以须要额外的空间来存储后继节点的地址。有两个特殊的节点,头结点和尾节点,其中头节点用来记录链表的基地址,有了它就能够遍历整个链表,尾节点的后继指针不是指向下一个节点,而是指向一个空地址NULL表示这是链表上最后一个节点。与数组同样,单链表也支持数据的查找、插入和删除操做,其中插入和删除操做只须要考虑相邻节点指针的变化,所以为常数级时间复杂度O(1)。要想随机访问第 k 个元素,就没有数组那么高效了。由于链表中的数据并不是连续存储的,因此没法像数组那样,根据首地址和下标,经过寻址公式就能直接计算出对应的内存地址,而是须要根据指针一个结点一个结点地依次遍历,直到找到相应的结点,所以时间复杂度为O(N)。运维
双向链表和单链表不一样的是多了一个前驱指针,双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。所以存储一样多的数据,双向链表占用比单链表更多的空间。但其优势在于支持双向遍历,体如今如下两个方面。编程语言
顾名思义。循环链表与单、双链表不一样的是其呈环状,单循环链表中其尾节点并不是指向NULL而是指向头结点。双循环链表中其头节点的前驱指针指向尾节点,尾节点的后继指针指向头结点。循环链表的优点在于链尾到链头,链头到链尾比较方便适合处理的数据具备环型结构特色。函数
Redis链表使用双向无环链表。大数据
如图所示,Redis使用一个listNode结构来表示。
typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value; } listNode;
同时Redis为了方便的操做链表,提供了一个list结构来持有链表。以下图所示
typedef struct list{ //表头节点 listNode *head; //表尾节点 listNode *tail; //链表所包含的节点数量 unsigned long len; //节点值复制函数 void *(*dup)(void *ptr); //节点值释放函数 void *(*free)(void *ptr); //节点值对比函数 int (*match)(void *ptr,void *key); }list;
Redis链表结构其主要特性以下:
链表在Redis中的应用很是普遍,列表对象的底层实现之一就是链表。此外如发布订阅、慢查询、监视器等功能也用到了链表。咱们如今简单想想Redis为何要使用双向无环链表这种数据结构,而不是使用数组、单向链表等。既然列表对象的底层实现之一是链表,那么咱们经过一个表格来分析列表对象的经常使用操做命令。若是分别使用数组、单链表和双向链表实现列表对象的时间复杂度对照以下:
操做\时间复杂度 | 数组 | 单链表 | 双向链表 |
---|---|---|---|
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) |
咱们能够看到在列表对象经常使用的操做中双向链表的优点所在。但双向链表由于使用两个额外的空间存储前驱和后继指针,所以在数据量较小的状况下会形成空间上的浪费(由于数据量小的时候速度上的差异不大,但空间上的差异很大)。这是一个时间换空间仍是空间换时间的思想问题,Redis在列表对象中小数据量的时候使用压缩列表做为底层实现,而大数据量的时候才会使用双向无环链表。(关于列表对象后续会有文章继续介绍可访问个人我的博客持续关注www.kxamm.com)
链表做为一种很是经常使用的数据结构,内置在许多编程语言里面,更是找工做过程当中常常问的面试题之一。本篇文章简单复习了链表这种数据结构常见的几种形式,而且简单分析了Redis中链表的使用。下篇文章将继续分享Redis中用到的数据结构Hash。敬请关注!
《Redis设计与实现》
《Redis开发与运维》
《Redis官方文档》