双端链表
链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型,因为 C 语言本身 不支持链表类型,大部分 C 程序都会自己实现一种链表类型,Redis 也不例外——它实现了一 个双端链表结构。
双端链表作为一种常见的数据结构,在大部分的数据结构或者算法书里都有讲解,因此,这一 章关注的是 Redis 双端链表的具体实现,以及该实现的 API ,而对于双端链表本身,以及双端 链表所对应的算法,则不做任何解释。
双端链表还是 Redis 列表类型的底层实现之一,当对列表类型的键进行操作——比如执行 RPUSH 、LPOP 或 LLEN 等命令时,程序在底层操作的可能就是双端链表。
redis> RPUSH brands Apple Microsoft Google (integer) 3
redis> LPOP brands "Apple"
redis> LLEN brands (integer) 2
redis> LRANGE brands 0 -1 1) "Microsoft" 2) "Google"
Redis 列表使用两种数据结构作为底层实现:
1. 双端链表
2. 压缩列表
因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,
列表会优先考虑使用压 缩列表作为底层实现,并且在有需要的时候,
才从压缩列表实现转换到双端链表实现。
Redis 自身功能的构建
除了实现列表类型以外,双端链表还被很多 Redis 内部模块所应用:
• 事务模块使用双端链表来按顺序保存输入的命令;
• 服务器模块使用双端链表来保存多个客户端;
• 订阅/发送模块使用双端链表来保存订阅模式的多个客户端;
• 事件模块使用双端链表来保存时间事件(time event);
1.2.2 双端链表的实现
双端链表的实现由 listNode 和 list 两个数据结构构成,下图展示了由这两个结构组成的一 个双端链表实例:
其中,listNode 是双端链表的节点: typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 值
void *value;
} listNode;
而 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;
list 类型保留了三个函数指针——dup 、free 和 match ,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处 理时,如果有给定这些函数,那么它们就会被调用。
举个例子:当删除一个 listNode 时,如果包含这个节点的 list 的 list->free 函数不为空,
再执行余下的删除 操作(比如说,释放节点)。
另外,从这两个数据结构的定义上,也可以它们的一些行为和性能特征:
• listNode 带有 prev 和 next 两个指针,因此,对链表的遍历可以在两个方向上进行:
从表头到表尾,或者从表尾到表头。
• list 保存了 head 和 tail 两个指针 ,LPUSH 、RPOP 、
RPOPLPUSH 等命令的关键。
• list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 θ(1) ,
这也保证 了 LLEN 命令不会成为性能瓶颈。
以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:
1.2.3 迭代器
Redis 为双端链表实现了一个迭代器 ,这个迭代器可以从两个方向对双端链表进行迭代: • 沿着节点的 next 指针前进,从表头向表尾迭代; • 沿着节点的 prev 指针前进,从表尾向表头迭代; 以下是迭代器的数据结构定义:
typedef struct listIter {
// 下一节点
listNode *next;
// 迭代方向
int direction;
} listIter;
direction 记录迭代应该从那里开始: :
• 如果值为 adlist.h/AL_START_HEAD ,那么迭代器执行从表头到表尾的迭代;
• 如果值为 adlist.h/AL_START_TAIL ,那么迭代器执行从表尾到表头的迭代
1.2.4 小结
1)Redis 实现了自己的双端链表结构。
2) 双端链表主要有两个作用:
– 作为 Redis 列表类型的底层实现之一;
– 作为通用数据结构,被其他功能模块所使用;
3) 双端链表及其节点的性能特性如下:
– 节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,
并且对链表 的迭代可以在从表头到表尾和从表尾到表头两个方向进行;
– 链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;
– 链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长 度);
重点API源码实现
listCreate
list *listCreate(void)
{
struct list *list;
// 申请空间,zmalloc函数是redis定义的空间配置函数,我们后面会分析其原理,现在就暂时把它等同于malloc
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;
// 进行“常规”的初始化,这些跟我们在《数据结构》中学习的过程基本一致,注意这里还把三个函数指针设为null
list->head = list->tail = NULL;
list->len = 0;
list->dup = NULL;
list->free = NULL;
list->match = NULL;
return list;
}
listRelease
// 释放整个链表
void listRelease(list *list)
{
// 先销毁链表中的所有节点
unsigned long len;
listNode *current, *next;
current = list->head;
len = list->len;
while(len--) {
next = current->next;
// 通过list中的回调函数来释放每一节点数据域的内存空间
if (list->free) list->free(current->value);
// 销毁节点空间,zfree是redis定义的空间释放函数,和zmalloc对应
zfree(current);
current = next;
}
// 最后把链表也销毁
zfree(list);
}
listGetIterator
//获取一个给定方向上的迭代器
listIter *listGetIterator(list *list, int direction)
{
// 定义一个迭代器并为其分配空间
listIter *iter;
// 这里需要注意,调用该函数时分配了动态内存,使用完需要手动释放。这一点我们可以在后面代码中看到
// listGetIterator和listReleaseIterator是成对出现的
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
// 如果该迭代器的方向是从前往后,则迭代器的next指针指向头结点
iter->next = list->head;
else
// 如果该迭代器的方向是从后往前,则迭代器的next指针指向尾节点
iter->next = list->tail;
// 设置迭代器方向
iter->direction = direction;
return iter;
}
listAddNodeHead
// 在表头插入一个节点
list *listAddNodeHead(list *list, void *value)
{
// 定义一个新的节点并为其申请空间
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
// 设置节点的数据域
node->value = value;
// 往链表头部插入一个新节点需要考虑链表为空的情况
if (list->len == 0) { // 链表为空
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
// 链表不为空时的插入过程,主要是指针操作,最后新节点成为头结点。
node->prev = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
}
// 最后别忘了更新链表长度
list->len++;
return list;
}
listAddNodeTail
// 在表尾插入一个节点
list *listAddNodeTail(list *list, void *value)
{
// 定义一个新的节点并为其申请空间
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
// 设置节点的数据域
node->value = value;
// 往链表尾部插入一个新节点需要考虑链表为空的情况
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
// 链表不为空时的插入过程,主要是指针操作,最后新节点成为尾节点
node->prev = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
// 最后更新链表长度
list->len++;
return list;
}
listInsertNode
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
// 定义一个新的节点并为其申请空间
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
// 设置节点的数据域
node->value = value;
if (after) {
// 在目标节点的后面插入一个新节点,需要考虑目标节点为尾节点的情况
node->prev = old_node;
node->next = old_node->next;
if (list->tail == old_node) {
list->tail = node;
}
} else {
// 在目标节点的前面插入一个新节点,需要考虑目标节点为头结点的情况
node->next = old_node;
node->prev = old_node->prev;
if (list->head == old_node) {
list->head = node;
}
}
// 将插入节点前后的节点链接起来
if (node->prev != NULL) {
node->prev->next = node;
}
if (node->next != NULL) {
node->next->prev = node;
}
// 更新链表长度
list->len++;
return list;
}
listDelNode
// 删除一个节点
void listDelNode(list *list, listNode *node)
{
// 删除链表中的一个节点要考虑被删除节点是否为头结点或尾节点
if (node->prev)
// 如果node->prev存在,说明该节点不是头结点,直接将prev节点的next指向被删除节点的下一个
node->prev->next = node->next;
else
// 被删除节点是头结点,让被删除节点的下一个节点成为头结点
list->head = node->next;
if (node->next)
// 如果node->next存在,说明该节点不是尾节点
node->next->prev = node->prev;
else
// 被删除节点是尾节点
list->tail = node->prev;
// 回调free函数删除节点的数据域
if (list->free) list->free(node->value);
// 释放节点空间
zfree(node);
// 最后更新链表长度
list->len--;
}
其他的实现请到:
https://blog.csdn.net/Xiejingfa/article/details/50938028