图解Redis之数据结构篇——链表

文章导航-readme

前言

    Redis链表为双向无环链表!html

    图解Redis之数据结构篇——简单动态字符串SDS提到Redis使用了简单动态字符串,链表,字典(散列表),跳跃表,整数集合,压缩列表这些数据结构来操做内存,而且简单介绍了Redis简单动态字符串。本篇文章咱们继续来分析链表。面试

    链表是一种很是常见的数据结构,在Redis中使用很是普遍,列表对象的底层实现之一就是链表。其它如慢查询,发布订阅,监视器等功能也用到了链表。redis

Redis对象结构

系列文章

1、复习链表

1.1 数组与链表

    数组须要一块连续的内存来存储,这个特性有利也有弊。好处是其支持根据索引下标"随机访问"(时间复杂度为O(1)),可是其插入与删除操做为了保证在内存中的连续性将会变得很是低效(时间复杂度为O(N)),而且其一经声明就要占用整块连续内存空间,若是声明过大,系统可能内存不足,声明太小又可能致使不够用,而当数组的空间不足的时候须要对其进行扩容(申请一个更大的空间,将原数组拷贝过去)。编程

    而链表偏偏相反,其不须要一块连续的内存空间,其经过"指针"将一组零散的内存链接起来使用。其优势在于自己没有大小限制,自然支持扩容,插入删除操做高效(时间复杂度为O(1)),但缺点是随机访问低效(时间复杂度为O(N))。而且因为须要额外的空间存储指针。数组

数组与链表

    链表的实现方式有不少种,常见的主要有三个,单向链表、双向链表、循环链表。数据结构

1.2 单链表

单链表

    单链表中每一个节点除了包含数据以外还包含一个指针,叫后继指针,所以须要额外的空间来存储后继节点的地址。有两个特殊的节点,头结点和尾节点,其中头节点用来记录链表的基地址,有了它就能够遍历整个链表,尾节点的后继指针不是指向下一个节点,而是指向一个空地址NULL表示这是链表上最后一个节点。与数组同样,单链表也支持数据的查找、插入和删除操做,其中插入和删除操做只须要考虑相邻节点指针的变化,所以为常数级时间复杂度O(1)。要想随机访问第 k 个元素,就没有数组那么高效了。由于链表中的数据并不是连续存储的,因此没法像数组那样,根据首地址和下标,经过寻址公式就能直接计算出对应的内存地址,而是须要根据指针一个结点一个结点地依次遍历,直到找到相应的结点,所以时间复杂度为O(N)。运维

单链表插入与删除操做

1.3 双向链表

双向链表

    双向链表和单链表不一样的是多了一个前驱指针,双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。所以存储一样多的数据,双向链表占用比单链表更多的空间。但其优势在于支持双向遍历,体如今如下两个方面。编程语言

  • 在有序链表中查找某个元素,单链表因为只有后继指针,所以只能从前日后遍历查找时间复杂度为O(N),而双向链表能够双向遍历。
  • 删除给定指针指向的结点。假设已经找到要删除的节点,要删除就必须知道其前驱节点和后继节点,单链表想要知道其前驱节点只能从头开始遍历,时间复杂度为0(n),而双向链表因为保存了其前驱节点的地址,所以时间复杂度为0(1)。

1.4 循环链表

循环链表

    顾名思义。循环链表与单、双链表不一样的是其呈环状,单循环链表中其尾节点并不是指向NULL而是指向头结点。双循环链表中其头节点的前驱指针指向尾节点,尾节点的后继指针指向头结点。循环链表的优点在于链尾到链头,链头到链尾比较方便适合处理的数据具备环型结构特色。函数

2、Redis链表

2.1 双向无环链表

    Redis链表使用双向无环链表。大数据

Redis双向无环链表

如图所示,Redis使用一个listNode结构来表示。

typedef struct listNode
{ 
    // 前置节点 
    struct listNode *prev; 
    // 后置节点 
    struct listNode *next; 
    // 节点的值 
    void *value; 
} listNode;

2.2 list结构

    同时Redis为了方便的操做链表,提供了一个list结构来持有链表。以下图所示

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链表结构其主要特性以下:

  • 双向:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为0(1)。
  • 无环: 链表为非循环链表表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:经过list结构中的head和tail指针,获取表头和表尾节点的时间复杂度都为O(1)。
  • 带链表长度计数器:经过list结构的len属性获取节点数量的时间复杂度为O(1)。
  • 多态:链表节点使用void*指针保存节点的值,而且能够经过list结构的dup、free、match三个属性为节点值设置类型特定函数,因此链表能够用来保存各类不一样类型的值。

2.3 双向无环链表在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官方文档》

相关文章
相关标签/搜索