Redis源码解析——双向链表

   相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分,我决定还是要讲一讲的。

基本结构

        首先我们看链表元素的结构。因为是双向链表,所以其基本元素应该有一个指向前一个节点的指针和一个指向后一个节点的指针,还有一个记录节点值的空间

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. typedef struct listNode {  
  2.     struct listNode *prev;  
  3.     struct listNode *next;  
  4.     void *value;  
  5. } listNode;  
        因为双向链表不仅可以从头开始遍历,还可以从尾开始遍历,所以链表结构应该至少有头和尾两个节点的指针。

        但是作为一个可以承载各种类型数据的链表,还需要链表使用者提供一些处理节点中数据的能力。因为这些数据可能是用户自定义的,所以像复制、删除、对比等操作都需要用户来告诉框架。在《Redis源码解析——字典结构》一文中,我们看到用户创建字典时需要传入的dictType结构体,就是一个承载数据的上述处理方法的载体。但是Redis在设计双向链表时则没有使用一个结构来承载这些方法,而是在链表结构中定义了

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. typedef struct list {  
  2.     listNode *head;  
  3.     listNode *tail;  
  4.     void *(*dup)(void *ptr);  
  5.     void (*free)(void *ptr);  
  6.     int (*match)(void *ptr, void *key);  
  7.     unsigned long len;  
  8. } list;  
        至于链表结构中为什么要存链表长度字段len,我觉得从必要性上来说是没有必要的。有len字段的一个优点是不用每次计算链表长度时都要做一次遍历操作,缺点便是导出需要维护这个变量。

创建和释放链表

        链表创建的过程比较简单。只是申请了一个list结构体空间,然后各个字段设置为NULL

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. list *listCreate(void)  
  2. {  
  3.     struct list *list;  
  4.   
  5.     if ((list = zmalloc(sizeof(*list))) == NULL)  
  6.         return NULL;  
  7.     list->head = list->tail = NULL;  
  8.     list->len = 0;  
  9.     list->dup = NULL;  
  10.     list->free = NULL;  
  11.     list->match = NULL;  
  12.     return list;  
  13. }  
        但是比较有意思的是,创建链表时没有设定链表类型——没有设置复制、释放、对比等方法的指针。作者单独提供了一些宏来设置,个人觉得这种割裂开的设计不是很好

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #define listSetDupMethod(l,m) ((l)->dup = (m))  
  2. #define listSetFreeMethod(l,m) ((l)->free = (m))  
  3. #define listSetMatchMethod(l,m) ((l)->match = (m))  
        释放链表的操作就是从头部向尾部一个个释放节点,迭代的方法是通过判断不断减小的链表长度字段len是否为0来进行。之前说过,len其实没有必要性,只要判断节点的next指针为空就知道到结尾了。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void listRelease(list *list)  
  2. {  
  3.     unsigned long len;  
  4.     listNode *current, *next;  
  5.   
  6.     current = list->head;  
  7.     len = list->len;  
  8.     while(len--) {  
  9.         next = current->next;  
  10.         if (list->free) list->free(current->value);  
  11.         zfree(current);  
  12.         current = next;  
  13.     }  
  14.     zfree(list);  
  15. }  

新增节点

        新增节点分为三种:头部新增、尾部新增和中间新增。头部和尾部新增都很简单,只是需要考虑一下新增之前链表是不是空的。如果是空的,要设置新增节点的指向前后指针为NULL,还要让链表的头尾指针都指向新增的节点

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. list *listAddNodeHead(list *list, void *value)  
  2. {  
  3.     listNode *node;  
  4.   
  5.     if ((node = zmalloc(sizeof(*node))) == NULL)  
  6.         return NULL;  
  7.     node->value = value;  
  8.     if (list->len == 0) {  
  9.         list->head = list->tail = node;  
  10.         node->prev = node->next = NULL;  
  11.     } else {  
  12.         node->prev = NULL;  
  13.         node->next = list->head;  
  14.         list->head->prev = node;  
  15.         list->head = node;  
  16.     }  
  17.     list->len++;  
  18.     return list;  
  19. }  
  20.   
  21. list *listAddNodeTail(list *list, void *value)  
  22. {  
  23.     listNode *node;  
  24.   
  25.     if ((node = zmalloc(sizeof(*node))) == NULL)  
  26.         return NULL;  
  27.     node->value = value;  
  28.     if (list->len == 0) {  
  29.         list->head = list->tail = node;  
  30.         node->prev = node->next = NULL;  
  31.     } else {  
  32.         node->prev = list->tail;  
  33.         node->next = NULL;  
  34.         list->tail->next = node;  
  35.         list->tail = node;  
  36.     }  
  37.     list->len++;  
  38.     return list;  
  39. }  
        上述代码还说明一个问题,新建节点的数据指针指向传入的value内容,而没有使用复制操作将value所指向的数据复制下来。于是插入链表中的数据,要保证在链表生存周期之内都要有效。

        在链表中间插入节点时,可以指定插入到哪个节点前还是后。这个场景下则需要考虑作为坐标的节点是否为链表的头结点或者尾节点;如果是,可能还要视新插入的节点的位置更新链表的头尾节点指向。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. list *listInsertNode(list *list, listNode *old_node, void *value, int after) {  
  2.     listNode *node;  
  3.   
  4.     if ((node = zmalloc(sizeof(*node))) == NULL)  
  5.         return NULL;  
  6.     node->value = value;  
  7.     if (after) {  
  8.         node->prev = old_node;  
  9.         node->next = old_node->next;  
  10.         if (list->tail == old_node) {  
  11.             list->tail = node;  
  12.         }  
  13.     } else {  
  14.         node->next = old_node;  
  15.         node->prev = old_node->prev;  
  16.         if (list->head == old_node) {  
  17.             list->head = node;  
  18.         }  
  19.     }  
  20.     if (node->prev != NULL) {  
  21.         node->prev->next = node;  
  22.     }  
  23.     if (node->next != NULL) {  
  24.         node->next->prev = node;  
  25.     }  
  26.     list->len++;  
  27.     return list;  
  28. }  

删除节点

        删除节点时要考虑节点是否为链表的头结点或者尾节点。如果是则要更新链表的信息,否则只要更新待删除的节点前后节点指向关系。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void listDelNode(list *list, listNode *node)  
  2. {  
  3.     if (node->prev)  
  4.         node->prev->next = node->next;  
  5.     else  
  6.         list->head = node->next;  
  7.     if (node->next)  
  8.         node->next->prev = node->prev;  
  9.     else  
  10.         list->tail = node->prev;  
  11.     if (list->free) list->free(node->value);  
  12.     zfree(node);  
  13.     list->len--;  
  14. }  

创建和释放迭代器

        迭代器是一种辅助遍历链表的结构,它分为向前迭代器和向后迭代器。我们在迭代器结构中可以发现方向类型变量

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. typedef struct listIter {  
  2.     listNode *next;  
  3.     int direction;  
  4. } listIter;  
        创建一个迭代器,需要指定方向,从而可以让迭代器的next指针指向链表的头结点或者尾节点

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. listIter *listGetIterator(list *list, int direction)  
  2. {  
  3.     listIter *iter;  
  4.   
  5.     if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;  
  6.     if (direction == AL_START_HEAD)  
  7.         iter->next = list->head;  
  8.     else  
  9.         iter->next = list->tail;  
  10.     iter->direction = direction;  
  11.     return iter;  
  12. }  
        还可以通过下面两个方法,让迭代器类型发生转变,比如可以让一个向前的迭代器变成一个向后的迭代器。还可以让这个迭代器指向另外一个链表,而非创建它时指向的链表。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void listRewind(list *list, listIter *li) {  
  2.     li->next = list->head;  
  3.     li->direction = AL_START_HEAD;  
  4. }  
  5.   
  6. void listRewindTail(list *list, listIter *li) {  
  7.     li->next = list->tail;  
  8.     li->direction = AL_START_TAIL;  
  9. }  
        因为通过listGetIterator创建的迭代器是在堆上动态分配的,所以不使用时要释放

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void listReleaseIterator(listIter *iter) {  
  2.     zfree(iter);  
  3. }  

迭代器遍历

        迭代器的遍历其实就是简单的通过节点向前向后指针访问到下一个节点的过程
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. listNode *listNext(listIter *iter)  
  2. {  
  3.     listNode *current = iter->next;  
  4.   
  5.     if (current != NULL) {  
  6.         if (iter->direction == AL_START_HEAD)  
  7.             iter->next = current->next;  
  8.         else  
  9.             iter->next = current->prev;  
  10.     }  
  11.     return current;  
  12. }  

链表复制

        链表的复制过程就是通过一个从头向尾访问的迭代器,将原链表中的数据复制到新建的链表中。但是这儿有个地方需要注意下,就是复制操作可能分为深拷贝和浅拷贝。如果我们通过listSetDupMethod设置了数据的复制方法,则使用该方法进行数据的复制,然后将复制出来的新数据放到新的链表中。如果没有设置,则只是把老链表中元素的value字段赋值过去。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. list *listDup(list *orig)  
  2. {  
  3.     list *copy;  
  4.     listIter iter;  
  5.     listNode *node;  
  6.   
  7.     if ((copy = listCreate()) == NULL)  
  8.         return NULL;  
  9.     copy->dup = orig->dup;  
  10.     copy->free = orig->free;  
  11.     copy->match = orig->match;  
  12.     listRewind(orig, &iter);  
  13.     while((node = listNext(&iter)) != NULL) {  
  14.         void *value;  
  15.   
  16.         if (copy->dup) {  
  17.             value = copy->dup(node->value);  
  18.             if (value == NULL) {  
  19.                 listRelease(copy);  
  20.                 return NULL;  
  21.             }  
  22.         } else  
  23.             value = node->value;  
  24.         if (listAddNodeTail(copy, value) == NULL) {  
  25.             listRelease(copy);  
  26.             return NULL;  
  27.         }  
  28.     }  
  29.     return copy;  
  30. }  

查找元素

        查找元素同样是通过迭代器遍历整个链表,然后视用户是否通过listSetMatchMethod设置对比方法来决定是使用用户定义的方法去对比,还是直接使用value去对比。如果是使用value直接去对比,则是强对比,即要求对比的数据和链表的数据在内存中位置是一样的。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. listNode *listSearchKey(list *list, void *key)  
  2. {  
  3.     listIter iter;  
  4.     listNode *node;  
  5.   
  6.     listRewind(list, &iter);  
  7.     while((node = listNext(&iter)) != NULL) {  
  8.         if (list->match) {  
  9.             if (list->match(node->value, key)) {  
  10.                 return node;  
  11.             }  
  12.         } else {  
  13.             if (key == node->value) {  
  14.                 return node;  
  15.             }  
  16.         }  
  17.     }  
  18.     return NULL;  
  19. }  

通过下标访问链表

        下标可以是负数,代表返回从后数第几个元素。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. listNode *listIndex(list *list, long index) {  
  2.     listNode *n;  
  3.   
  4.     if (index < 0) {  
  5.         index = (-index)-1;  
  6.         n = list->tail;  
  7.         while(index-- && n) n = n->prev;  
  8.     } else {  
  9.         n = list->head;  
  10.         while(index-- && n) n = n->next;  
  11.     }  
  12.     return n;  
  13. }  

结尾节点前移为头结点

        这个方法在Redis代码中使用比较多。它将链表最后一个节点移动到链表头部。我想设计这么一个方法是为了让链表内容可以在无状态记录的情况下被均匀的轮询到。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void listRotate(list *list) {  
  2.     listNode *tail = list->tail;  
  3.   
  4.     if (listLength(list) <= 1) return;  
  5.   
  6.     /* Detach current tail */  
  7.     list->tail = tail->prev;  
  8.     list->tail->next = NULL;  
  9.     /* Move it as head */  
  10.     list->head->prev = tail;  
  11.     tail->prev = NULL;  
  12.     tail->next = list->head;  
  13.     list->head = tail;  

  1. }  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值