adlist
同学们好,我是三儿,在这个雾霾极度严重,气温骤降的今天继续研读redis源码。今天带来的是链表。
链表是一种线性表,很多设计模式,算法,高级数据结构都用到了链表,比如发布订阅模式,LRU算法,哈希表,跳表,树的冲突解决都使用到了链表。同学们可以自己实现以下,都是比较简单的。有兴趣的我们可以讨论。
首先声明几个概念,因为有可能我们每个人对某些概念理解的不一样。
- 头结点:辅助节点,并不作为数据节点
- 首元节点:第一个数据节点
- 尾节点:最后一个数据节点
1 数据结构
- 是一个双链表,没有头结点
- 没有形成环,使用头尾指针分别指向首元结点和尾节点
1.1 节点的表示
标准的双链表定义:前驱,后继,值
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
值采用void*的类型是为了可以达到泛型的效果。
1.2 迭代单元
迭代器用来遍历,所以肯定有一个游标指向当前遍历到的节点。除此之外redis支持正反两个方向进行迭代。
typedef struct listIter {
listNode *next;
int direction;
} listIter;
1.3 链表
一系列的节点连接起来,返回首元节点的地址就可以,为什么redis还要提供一个结构来表示链表呢?因为这样更方便。三儿在平常构造数据结构的时候,对于由一系列节点构成的结构,也习惯特意为该结构提供一个数据结构来进行描述。
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
- head:首元节点
- tail:尾节点
- len:节点个数
- dup:节点值的复制函数
- free:节点值释放函数
- match:节点值比较函数
节点的三个函数是为了达到多态的效果
2 宏解析
貌似C的程序都喜欢提供宏操作,因为宏的执行没有代价,相较于函数不需要建立栈帧,但是比较容易出错,但是一些简短的操作显然利用宏更合适。
adlist中的这些宏基本就是在操作链表结构中的那些成员变量,更体现了使用一些辅助成员构建链表相比于只使用一些列节点更有优势。
#define listLength(l) ((l)->len) //获取链表长度
#define listFirst(l) ((l)->head) //获取首元节点
#define listLast(l) ((l)->tail) //获取尾节点
#define listPrevNode(n) ((n)->prev) //获取一个节点的前驱
#define listNextNode(n) ((n)->next) //获取一个节点的后继
#define listNodeValue(n) ((n)->value) //获取一个节点的值
//设置链表节点的三个处理方法
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))
//获取链表节点的三个处理方法
#define listGetDupMethod(l) ((l)->dup)
#define listGetFree(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)
这些宏都是比较简单直观的,三儿原来看某个开源程序,里面的宏看到想死。
3 函数
重点就是看迭代器,其它的CRUD都是正常思路,和我们处理不同的就是大神的扣边界能力及其的可怕。记得在企业安全实习的时候看golang的context源码,思路很简单,但是大神们抽象的能力的真的让人佩服。我们写出来的是代码,大神书写的是艺术。
3.1 创建
你没有对象吗,那就new一个出来啊。我没有链表,那我Create一个出来吧。
3.1.1 listCreate
在宏解析的部分看到了有设置节点方法的宏,所以初始创建的时候全部为对应类型的零值就好。
list *listCreate(void)
{
struct list *list;
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;
list->head = list->tail = NULL;
list->len = 0;
list->dup = NULL;
list->free = NULL;
list->match = NULL;
return list;
}
3.2 增加
链表的插入,分以下三种:
- 头插法
- 尾插法
- 节点前后法
灵活组合插入和删除的位置就构成了栈和队列,方向相同的是栈,相反的是队列。
类型 | 插入位置 | 删除位置 |
---|---|---|
栈 | 头部 | 头部 |
栈 | 尾部 | 尾部 |
队列 | 头部 | 尾部 |
队列 | 尾部 | 头部 |
3.2.1 头插法
因为没有哨兵节点,所以插入的时候需要区分当前链表是否有数据节点。同时需要注意的是一点要记得更新长度信息。
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;
}
3.2.2 尾插法
尾插法和头插法基本一致。
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;
}
3.2.3 节点前后法
这个名字只是三儿平常用的,大家知道什么意思就好,就是根据一个节点,选择在这个节点前面或后面插入新的节点。
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
listNode *node;
//申请节点初始化value成员
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;
}
其实这段代码中最后两个if用的比较好,如果不用的话,就会在上面的if else中嵌套if else语句,而且嵌套的if else中还会有重复的代码。
抽取出来后起到了代码复用的效果(虽然复用的只有一两行),而且减少了else。
在开发中尽量减少if else的嵌套和并且使用if时如果不需要用else就可以,千万不用使用else,看起来很影响阅读。下面举个例子。
//例子1
func doSomething(state int) {
if state == A {
doA()
return
}
doElse()
}
//例子2
func doSomething(state int) {
for {
if state == A {
doA()
break
}
if state == B {
doB()
break
}
doElse()
break
}
}
看了这个两个简单的例子应该能体会到代码阅读性了吧,实习期间三儿可是因为代码组织和可读性的问题被leader说了好几次呢。
3.3 删除
如果你在阅读后要自己实现一个adlist,并且你使用的是C/C++,删除节点的时候记得释放指针,C和C++里不释放指针可是会早上内存泄漏的。
- 删除单个节点
- 删除所有节点
3.3.1 删除单个节点
void listDelNode(list *list, listNode *node)
{
if (node->prev)
node->prev->next = node->next;
else
list->head = node->next;
if (node->next)
node->next->prev = node->prev;
else
list->tail = node->prev;
if (list->free) list->free(node->value);
zfree(node);
list->len--;
}
3.3.2 删除所有节点
从头遍历链表,挨个删除(释放值的内存,释放节点内存)。保留链表结构。
因为需要保留链表的结构,所以删除节点后需要更改链表成员的值,可以看出来这并不是线程安全的,不过redis是单进程单线程的,使用的异步模型。
void listEmpty(list *list)
{
unsigned long len;
listNode *current, *next;
current = list->head;
len = list->len;
while(len--) {
next = current->next;
if (list->free) list->free(current->value);
zfree(current);
current = next;
}
list->head = list->tail = NULL;
list->len = 0;
}
3.4 更改
- 旋转链表
- 合并链表
3.4.1 旋转链表
在没有看这个源码之前,一度以为这是一个反转链表的实现,看之后心里想what,代码实现不对啊,没用递归也没用循环,三儿一度以为眼镜该换了。看了一下注释后,发现理解错了,旋转链表是将尾节点移动到首元节点。
void listRotate(list *list) {
listNode *tail = list->tail;
//如果没有数据节点或者只有一个节点肯定不需要操作了
if (listLength(list) <= 1) return;
//以下的两个注释让我省了一副换眼镜的钱。。
/* Detach current tail */
list->tail = tail->prev;
list->tail->next = NULL;
/* Move it as head */
list->head->prev = tail;
tail->prev = NULL;
tail->next = list->head;
list->head = tail;
}
3.4.2 合并链表
对于这个操作,我目前自己写的话,肯定写的和redis的不一样,再一次膜拜大神的扣边界能力。
void listJoin(list *l, list *o) {
if (o->head)
o->head->prev = l->tail;
if (l->tail)
l->tail->next = o->head;
else
l->head = o->head;
if (o->tail) l->tail = o->tail;
l->len += o->len;
/* Setup other as an empty list. */
o->head = o->tail = NULL;
o->len = 0;
}
3.5 查询
- 获取索引节点
- 判断节点存在
3.5.1 获取索引节点
redis支持了负数索引,-1代表尾节点。如果是负数索引的话,那么这个操作会直接从尾部开始向前遍历查询,所以负数索引转换并不是转换成正方向的索引。
listNode *listIndex(list *list, long index) {
listNode *n;
//负数索引首先需要转换
if (index < 0) {
index = (-index)-1;
n = list->tail;
while(index-- && n) n = n->prev;
} else {
n = list->head;
while(index-- && n) n = n->next;
}
return n;
}
3.5.2 判断节点存在
遍历整个链表,查找相匹配的key。这个过程用到列迭代器,暂时单个黑盒使用吧,下面会有迭代器的源码部分。
listNode *listSearchKey(list *list, void *key)
{
listIter iter;
listNode *node;
listRewind(list, &iter);
while((node = listNext(&iter)) != NULL) {
if (list->match) {
if (list->match(node->value, key)) {
return node;
}
} else {
if (key == node->value) {
return node;
}
}
}
return NULL;
}
3.6 释放
天下没有不散的宴席,我们要学会离别,这是成长的代价。程序也一样啊,操作系统为了能良好的运行,支持更多的任务,必须有足够的空间啊,所以我们使用完了就把空间还回去吧。
3.6.1 释放链表
上面删除的部分有一个删除所有节点的操作,那么这里直接复用了上面的代码,删除后再释放链表结构的内存。
void listRelease(list *list)
{
listEmpty(list);
zfree(list);
}
3.7 迭代器
- 获取迭代器
- 重置迭代器
- 迭代器遍历
- 释放迭代器
3.7.1 获取迭代器
获取迭代器,根据迭代的方向将迭代器中的指针指向正确的节点。
listIter *listGetIterator(list *list, int direction)
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
3.7.2 listRewind&&listRewindTail
有可能在使用迭代器完成某个操作后,我们还需要进行迭代的操作,这时我们无需释放迭代器重新创建,重置迭代器的状态即可。
void listRewind(list *list, listIter *li) {
li->next = list->head;
li->direction = AL_START_HEAD;
}
void listRewindTail(list *list, listIter *li) {
li->next = list->tail;
li->direction = AL_START_TAIL;
}
3.7.3 listNext
迭代器的作用就是用来遍历链表的,所以这个是最重要的一步。Next并不是真正的后继哦,而是根据迭代器迭代的方向来获得下一个需要遍历的节点。
listNode *listNext(listIter *iter)
{
listNode *current = iter->next;
if (current != NULL) {
if (iter->direction == AL_START_HEAD)
iter->next = current->next;
else
iter->next = current->prev;
}
return current;
}
3.7.4 释放迭代器
void listReleaseIterator(listIter *iter) {
zfree(iter);
}
3.7.5 迭代器总结
redis的迭代器比较的简单,因为是单线程的,不需要考虑在迭代的过程中迭代器指向的节点被释放的情况。如果你设计链表的迭代器完全可以考虑在redis的基础上进行优化,比如支持步长。这些三儿原来用C++实现过一个迭代器,考虑到了删除节点,和步长的因素。如果你喜欢研究,可以和三儿一起探讨。或者加入三儿的qq群(521625004)。
总结
今天的文章里面源码比较多,但思路都是平常使用的,对于链表的处理就是在看你的扣边界能如何,但是扣边界和代码阅读性之间肯定需要平衡。希望三儿的文章能帮到大家理解。
关于作者
大四学生一枚,分析数据结构,面试题,golang,C语言等知识。QQ交流群:521625004。微信公众号:后台技术栈。