链表在Redis中的应用非常广泛,链表是列表键的底层实现之一,发布订阅,慢查询,监视器等功能也用到了链表。
以下给出Redis中链表相关的一些思考问题,通过源码分析,给出问题的答案,掌握链表的底层实现原理和设计思路。
源码版本:Redis 6.0.10
问题思考C语言中如何设计一个通用的泛型链表?
-
Redis中的链表和链表节点的实现
- 节点值的类型是void *,这样设计有什么好处?
- 仅使用多个listNode也能组成链表,为什么还要额外用一个list结构去持有链表?
- 为什么用双向链表,不用单链表?
-
双向链表API实现分析
- 链表创建
- 链表销毁
- 头部插入节点
- 尾部插入节点
- 指定位置前后插入节点
- 删除指定节点
- 链表迭代器的设计实现
- 查找指定节点
- 复制链表
- 表头节点移动到表尾
以REDIS 6.0.10源码为例,链表的实现参考adlist.h, adlist.c
链表节点通过listNode结构体实现,定义如下:
typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode;
可以看出这是一个双向链表,value存储节点值,其类型为void *
节点值的类型为void *,这样设计有什么好处?节点值设置void *类型,目的是实现一个通用的泛型链表,提高代码复用性。(类似C++的STL容器,多态思想)
考虑实际应用场景中,链表节点值的类型可以是int, float, double等基本类型, 或者是自定义结构体类型,如果简单将value定义为某个具体类型,就只能为每个节点类型定义一个listNodeXX的结构体,同时需额外为每个节点类型新增一套增、删、改、查的API,实现非常繁琐,如下所示:
typedef struct listNodeInt { struct listNode *prev; struct listNode *next; int value; } listNodeInt; typedef struct listNodeDouble { struct listNode *prev; struct listNode *next; double value; } listNodeDouble; // 增加一个节点到链表头部 list *listAddNodeHeadInt(list *list, int value); list *listAddNodeHeadDouble(list *list, double value);
可以看出,这种实现方式的问题在于代码重复度过高,难以维护,且扩展性差,每支持一个新的节点类型都要新增代码,这显然是不能接受的。所以节点值类型要设计为void *
仅使用多个listNode也能组成链表,为什么还要额外用一个list结构去持有链表, 这样设计有什么好处?Redis中额外使用list结构,用于持有链表,简化 *** 作。list结构体定义如下:
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用于记录表头指针和表尾指针,好处是获取表头节点或表尾节点的时间复杂度为O(1)
-
len类型用于记录链表长度, 好处是获取链表节点总数的时间复杂度为O(1)
-
dup, free, match成员用于实现多态链表,对于不同类型的节点,挂接不同的函数钩子,设置特定的复制、释放、比较 *** 作。
- dup函数用于复制节点值
- free函数用于释放节点值
- match函数用于比较两个节点值是否相等
举例:一个长度为2的双向链表示意图:
双向链表相较于单链表,有如下优点:
- 支持双向查找节点,且查找给定节点的前驱节点的时间复杂度为O(1)
- 尾部插入节点快,时间复杂度为O(1)
调用listCreate,创建一个空的双向链表,时间复杂度O(1)
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; }链表销毁
调用listRelease,释放链表,时间复杂度O(N),N为链表长度
void listRelease(list *list) { listEmpty(list); zfree(list); } 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); // 不同的节点类型,挂接不同的free函数钩子,实现多态链表的释放 *** 作。 zfree(current); current = next; } list->head = list->tail = NULL; list->len = 0; }头部插入节点
调用listAddNodeHead,在链表头部插入节点,时间复杂度O(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; }尾部插入节点
调用listAddNodeTail, 在链表尾部插入节点,时间复杂度O(1)
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,在指定位置之前或之后插入节点,时间复杂度O(1)
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; }
after等于1时,在给定old_node节点之后插入,否则在给定节点前插入。
删除指定节点调用listDelNode, 删除指定节点,时间复杂度为O(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--; }链表迭代器的设计实现
定义struct listIter结构体实现链表的迭代器,支持双向迭代。(类似C++容器的begin()和rbegin() *** 作)
#define AL_START_HEAD 0 // 正向迭代, head -> tail #define AL_START_TAIL 1 // 反向迭代,tail -> head typedef struct listIter { listNode *next; int direction; // 取值有两种,AL_START_HEAD或AL_START_TAIL,支持双向迭代。 } listIter;
通过listGetIterator,创建一个正向/反向迭代器:
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; }
重置迭代器, 通过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; }
通过listNext,访问迭代器中的下一个元素:
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; }查找指定节点
调用listSearchKey,查找指定节点,平均时间复杂度O(N),N为链表长度。
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; }复制链表
调用listDup,复制链表,时间复杂度O(N),N为链表长度。
实现技巧分析:通过迭代器隐藏链表内部实现,简化链表遍历 *** 作;通过dup函数指针,统一了不同类型链表节点的复制流程,实现简洁优雅,值得一学。
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; listRewind(orig, &iter); while((node = listNext(&iter)) != NULL) { void *value; if (copy->dup) { value = copy->dup(node->value); if (value == NULL) { listRelease(copy); return NULL; } } else value = node->value; if (listAddNodeTail(copy, value) == NULL) { listRelease(copy); return NULL; } } return copy; }表头节点移动到表尾
通过listRotateHeadToTail,实现表头节点移动到表尾,时间复杂度为O(1)
void listRotateHeadToTail(list *list) { if (listLength(list) <= 1) return; listNode *head = list->head; list->head = head->next; list->head->prev = NULL; list->tail->next = head; head->next = NULL; head->prev = list->tail; list->tail = head; }
通过listRotateTailToHead,实现表尾节点移动到表头,时间复杂度为O(1)
void listRotateTailToHead(list *list) { if (listLength(list) <= 1) return; listNode *tail = list->tail; list->tail = tail->prev; list->tail->next = NULL; list->head->prev = tail; tail->prev = NULL; tail->next = list->head; list->head = tail; }参考资料
【1】《Redis设计与实现》 第3章 链表
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)