图解Redis之数据结构篇——链表
文章导航-readme
前言
Redis链表为双向无环链表!
图解Redis之数据结构篇——简单动态字符串SDS提到Redis使用了简单动态字符串,链表,字典(散列表),跳跃表,整数集合,压缩列表这些数据结构来操作内存,并且简单介绍了Redis简单动态字符串。本篇文章我们继续来分析链表。
链表是一种非常常见的数据结构,在Redis中使用非常广泛,列表对象的底层实现之一就是链表。其它如慢查询,发布订阅,监视器等功能也用到了链表。

系列文章
一、复习链表
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而是指向头结点。双循环链表中其头节点的前驱指针指向尾节点,尾节点的后继指针指向头结点。循环链表的优势在于链尾到链头,链头到链尾比较方便适合处理的数据具有环型结构特点。
二、Redis链表
2.1 双向无环链表
Redis链表使用双向无环链表。

如图所示,Redis使用一个listNode结构来表示。
typedef struct listNode
{
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
2.2 list结构
同时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链表结构其主要特性如下:
- 双向:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为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官方文档》
图解Redis之数据结构篇——链表的更多相关文章
- 图解Redis之数据结构篇——简单动态字符串SDS
图解Redis之数据结构篇--简单动态字符串SDS 前言 相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用.这个对象系统包括字符串对象 ...
- 图解Redis之数据结构篇——字典
前言 字典在Redis中的应用非常广泛,数据库与哈希对象的底层实现就是字典. 系列文章 图解Redis之数据结构篇--简单动态字符串SDS 图解Redis之数据结构篇--链表 图解Redis之 ...
- 图解Redis之数据结构篇——跳跃表
前言 跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.这么说,我们可能很难理解,我们可以先回忆一下链表. 一.复习跳跃表 1.1 什么 ...
- 图解Redis之数据结构篇——压缩列表
前言 同整数集合一样压缩列表也不是基础数据结构,而是 Redis 自己设计的一种数据存储结构.它有点儿类似数组,通过一片连续的内存空间,来存储数据.不过,它跟数组不同的一点是,它允许存储的数据 ...
- 图解Redis之数据结构篇——整数集合
前言 整数集合(intset)并不是一个基础的数据结构,而是Redis自己设计的一种存储结构,是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis i ...
- Redis设计与实现-内部数据结构篇
题记:这本书是2015年11月份开始读的,大约花了一个多月的时间通读了一遍,最近由于需要对redis做一些深入的了解,因此又花了两个多月仔细精读了一遍,由于本书设计的内容较多,且每部分的内容都比较细致 ...
- 新秀nginx源代码分析数据结构篇(两) 双链表ngx_queue_t
nginx源代码分析数据结构篇(两) 双链表ngx_queue_t Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.csdn. ...
- 菜鸟nginx源代码剖析数据结构篇(八) 缓冲区链表ngx_chain_t
菜鸟nginx源代码剖析数据结构篇(八) 缓冲区链表 ngx_chain_t Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog. ...
- 菜鸟nginx源码剖析数据结构篇(八) 缓冲区链表ngx_chain_t[转]
菜鸟nginx源码剖析数据结构篇(八) 缓冲区链表 ngx_chain_t Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.c ...
随机推荐
- 你不可不知的Java引用类型之——PhantomReference源码详解
定义 PhantomReference是虚引用,该引用不会影响不会影响对象的生命周期,也无法从虚引用中获取对象实例. 说明 源码介绍部分其实也没多大内容,主要内容都在前面介绍中说完了.PhantomR ...
- Bayboy功能详解
Bayboy功能详解 一.Badboy中的检查点 1.1以sogou.com搜索为例,搜索测试 步骤:打开Badboy工具,在地址栏中输入搜狗网址:输入 测试 进行搜索:点击红色按钮停止录制 1.2添 ...
- Python 实例方法、类方法、静态方法的区别与作用
Python中至少有三种比较常见的方法类型,即实例方法,类方法.静态方法.它们是如何定义的呢?如何调用的呢?它们又有何区别和作用呢?且看下文. 首先,这三种方法都定义在类中.下面我先简单说一下怎么定义 ...
- Python爬取网络图片
代码: import urllib import urllib.request import re #打开网页,下载器 def open_html ( url): require=urllib.req ...
- 安卓程序中手机后退键与标题栏后退键是不同的,前者回出发onBackPressed()函数,后者需要重重写temclick函数
安卓程序中手机后退键与标题栏后退键是不同的,前者回出发onBackPressed()函数,后者需要重重写temclick函数
- java----java垃圾回收算法
1.引用计数法(Reference Counting Collector) 1.1算法分析 引用计数是垃圾收集器中的早期策略.在这种方法中,堆中每个对象实例都有一个引用计数.当一个对象被创建时,且将该 ...
- [20170603]12c Top Frequency histogram.txt
---恢复内容开始--- [20170603]12c Top Frequency histogram.txt --//个人对直方图了解很少,以前2种直方图类型对于目前的许多应用来讲已经足够,或者讲遇到 ...
- Oracle EBS FA 本年折旧
FUNCTION get_ytd_deprn(p_asset_id IN NUMBER, p_book_type_code IN VARCHAR2, p_rate_source_rule IN VAR ...
- Hive中笔记 :三种去重方法,distinct,group by与ROW_Number()窗口函数
一.distinct,group by与ROW_Number()窗口函数使用方法 1. Distinct用法:对select 后面所有字段去重,并不能只对一列去重. (1)当distinct应用到多个 ...
- oracle USING 用法
提问 using(xx)中可以接多个列吗? using(xx)中的列可以接表名或别名吗? 在使用using的语句中,select * 可以使用吗? 如果表有别名t,select t.* from ta ...