一、简介
- 作为一种常用数据结构,Redis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。
- 链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
二、实现
- 单个链表节点:
typedef struct listNode {
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点的值
} listNode;
可以看到链表的节点有前后指针,因此Redis实现的链表是一个双向链表。
- 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;
list和listNode整体结构如下图所示:
三、源码
1.创建链表
创建链表过程,就是通过zmalloc函数申请了一个list结构体空间,各个字段设置为NULL即可。
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;
}
2.释放链表
释放链表首先拿到list的长度和头指针,然后依次遍历每个节点调用free函数。
void listRelease(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;
}
zfree(list);
}
3.新增节点
- 头部与尾部新增:
使用zmalloc函数分配listNode节点内存空间,之后判断新增之前链表是否为空,为空的话要设置新增节点的前后指针为NULL,让链表的头尾指针指向新增的节点。
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;//链表当前头节点的前指针设为node
list->head = node;//之后把node作为链表的头节点
}
list->len++;
return list;
}
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;
}
- 中间新增:
需要判断当前节点是否为头或尾节点。
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) {//在old_node后插入
node->prev = old_node;//前指针设为old_node
node->next = old_node->next;//后指针设为old_node的下一个节点
if (list->tail == old_node) {//边界条件,在最后一个元素后插入,设置list的尾指针为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;
}
4.删除节点
删除节点时要考虑节点是否为链表的头结点或者尾节点。如果是则要更新链表的信息,否则只要更新待删除的节点前后节点指向关系。
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--;
}
5.迭代器
- 迭代器数据结构:
typedef struct listIter {
listNode *next; //迭代器当前指向的节点
int direction; //迭代方向,可以取以下两个值:AL_START_HEAD和AL_START_TAIL
} listIter
- 创建一个迭代器:
listIter *listGetIterator(list *list, int direction) //为list创建一个迭代器iterator
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL; //为迭代器申请空间
if (direction == AL_START_HEAD) //设置迭代指针的起始位置(从头or从尾)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction; //设置迭代方向
return iter;
}
- 重置迭代器方向:
void listRewind(list *list, listIter *li) { //将迭代器li重置为list的头结点并且设置为正向迭代
li->next = list->head; //设置迭代指针的起始位置
li->direction = AL_START_HEAD; //设置迭代方向从头到尾
}
void listRewindTail(list *list, listIter *li) { //将迭代器li重置为list的尾结点并且设置为反向迭代
li->next = list->tail; //设置迭代指针的起始位置
li->direction = AL_START_TAIL; //设置迭代方向从尾到头
}
- 迭代器遍历: 返回迭代器iter指向的当前节点并更新iter
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;//返回备份的当前节点地址
}
- 链表复制:
链表的复制过程就是通过一个从头向尾访问的迭代器,将原链表中的数据复制到新建的链表中。
如果我们通过listSetDupMethod设置了数据的复制方法,则使用该方法进行数据的复制,然后将复制出来的新数据放到新的链表中。如果没有设置,则只是把老链表中元素的value字段赋值过去。
list *listDup(list *orig)
{
list *copy;
listIter *iter;
listNode *node;
if ((copy = listCreate()) == NULL) //创建一个表头
return NULL;
//设置新建表头的处理函数
copy->dup = orig->dup;
copy->free = orig->free;
copy->match = orig->match;
//迭代整个orig的链表
iter = listGetIterator(orig, AL_START_HEAD); //为orig定义一个迭代器并设置迭代方向
while((node = listNext(iter)) != NULL) { //迭代器根据迭代方向不停迭代
void *value;
//复制节点值到新节点
if (copy->dup) {
value = copy->dup(node->value); //如果定义了list结构中的dup指针,则使用该方法拷贝节点值。
if (value == NULL) {
listRelease(copy);
listReleaseIterator(iter);
return NULL;
}
} else
value = node->value; //获得当前node的value值
if (listAddNodeTail(copy, value) == NULL) { //将node节点尾插到copy表头的链表中
listRelease(copy);
listReleaseIterator(iter);
return NULL;
}
}
listReleaseIterator(iter); //自行释放迭代器
return copy; //返回拷贝副本
}
6.查找元素
查找元素同样是通过迭代器遍历整个链表,然后视用户是否通过listSetMatchMethod设置对比方法来决定是使用用户定义的方法去对比,还是直接使用value去对比。如果是使用value直接去对比,则是强对比,即要求对比的数据和链表的数据在内存中位置是一样的。
listNode *listSearchKey(list *list, void *key)
{
listIter *iter;
listNode *node;
iter = listGetIterator(list, AL_START_HEAD); //创建迭代器
while((node = listNext(iter)) != NULL) { //迭代整个链表
if (list->match) { //如果设置list结构中的match方法,则用该方法比较
if (list->match(node->value, key)) {
listReleaseIterator(iter); //如果找到,释放迭代器返回node地址
return node;
}
} else {
if (key == node->value) {
listReleaseIterator(iter);
return node;
}
}
}
listReleaseIterator(iter); //释放迭代器
return NULL;
}
7.通过下标访问链表
下标可以是负数,代表返回从后数第几个元素。
listNode *listIndex(list *list, long index) {
listNode *n;
if (index < 0) {//从后往前返回
index = (-index)-1;//指向最后一位
n = list->tail;
while(index-- && n) n = n->prev;//index为0,h
} else {
n = list->head;
while(index-- && n) n = n->next;
}
return n;
}
四、特性总结
- 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都 是O(1)。
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以 NULL为终点。
- 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点 和表尾节点的复杂度为O(1)。
- 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序 获取链表中节点数量的复杂度为O(1)。
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、 match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
五、参考资料
- 《Redis设计与实现》
六、扩展阅读
- C中void指针:www.zhihu.com/question/40…