Redis源码(二)——链表

117 阅读7分钟

一、简介

  • 作为一种常用数据结构,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设计与实现》

六、扩展阅读