双端链表

双端链表

链表作为数组之外的一种常用序列抽象, 是大多数高级语言的基本数据类型, 因为 C 语言本身不支持链表类型, 大部分 C 程序都会自己实现一种链表类型, Redis 也不例外 —— 实现了一个双端链表结构。

双端链表作为一种常见的数据结构, 在大部分的数据结构或者算法书里都有讲解, 因此, 这一章关注的是 Redis 双端链表的具体实现, 以及该实现的 API , 而对于双端链表本身, 以及双端链表所对应的算法, 则不做任何解释。

读者如果有需要的话,可以参考维基百科的双端链表词条,里面提供了关于双端链表的一些基本信息。

另外,一些书籍,比如《算法:C 语言实现》《数据结构与算法分析》则提供了关于双端链表的更详细的信息。

双端链表的应用

双端链表作为一种通用的数据结构, 在 Redis 内部使用得非常多: 既是 Redis 列表结构的底层实现之一, 同时为大量 Redis 模块所用, 用于构建 Redis 的其他功能。

实现 Redis 的列表类型

双端链表还是 Redis 列表类型的底层实现之一, 当对列表类型的键进行操作 —— 比如执行 RPUSH 、 LPOP 或 LLEN 等命令时, 程序在底层操作的可能就是双端链表。

redis> RPUSH brands Apple Microsoft Google
(integer) 3

redis> LPOP brands
"Apple"

redis> LLEN brands
(integer) 2

redis> LRANGE brands 0 -1
1) "Microsoft"
2) "Google"

Redis 列表使用两种数据结构作为底层实现:

  1. 双端链表
  2. 压缩列表

因为双端链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表作为底层实现, 并且在有需要的时候, 才从压缩列表实现转换到双端链表实现。

后续章节会对压缩链表和 Redis 类型做更进一步的介绍。

Redis 自身功能的构建

除了实现列表类型以外, 双端链表还被很多 Redis 内部模块所应用:

  • 事务模块使用双端链表依序保存输入的命令;
  • 服务器模块使用双端链表来保存多个客户端;
  • 订阅/发送模块使用双端链表来保存订阅模式的多个客户端;
  • 事件模块使用双端链表来保存时间事件(time event);

类似的应用还有很多, 在后续的章节中我们将看到, 双端链表在 Redis 中发挥着重要的作用。

双端链表的实现

双端链表的实现由 listNode 和 list 两个数据结构构成, 下图展示了由这两个结构组成的一个双端链表实例:

其中, listNode 是双端链表的节点:

typedef struct listNode {

    // 前驱节点
    struct listNode *prev;

    // 后继节点
    struct listNode *next;

    // 值
    void *value;

} listNode;

而 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;

注意, listNode 的 value 属性的类型是 void * ,说明这个双端链表对节点所保存的值的类型不做限制。

对于不同类型的值,有时候需要不同的函数来处理这些值,因此, list 类型保留了三个函数指针 —— dup 、 free 和 match ,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处理时,如果有给定这些函数,就会调用这些函数。

举个例子:当删除一个 listNode 时,如果包含这个节点的 list 的 list->free 函数不为空,就会先调用删除函数 list->free(listNode->value) 来清空节点的值,再执行余下的删除操作(比如说,释放节点)。

另外,从这两个数据结构的定义上,也可以了解到一些行为和性能特征:

  • listNode 带有 prev 和 next 两个指针,因此,遍历可以双向进行:从表头到表尾,表尾到表头。
  • list 保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 θ(1) —— 这是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。
  • list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 θ(1) ,这也保证了 LLEN 命令不会成为性能瓶颈。

以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:

函数 作用 算法复杂度
listCreate 创建新链表 O(1)
listRelease 释放链表,以及该链表所包含的节点 O(N)
listDup 创建给定链表的副本 O(N)
listRotate 取出链表的表尾节点,并插入到表头 O(1)
listAddNodeHead 将包含给定值的节点添加到链表的表头 O(1)
listAddNodeTail 将包含给定值的节点添加到链表的表尾 O(1)
listInsertNode 将包含给定值的节点添加到某个节点的之前或之后 O(1)
listDelNode 删除给定节点 O(1)
listSearchKey 在链表中查找和给定 key 匹配的节点 O(N)
listIndex 给据给定索引,返回列表中相应的节点 O(N)
listLength 返回给定链表的节点数量 O(1)
listFirst 返回链表的表头节点 O(1)
listLast 返回链表的表尾节点 O(1)
listPrevNode 返回给定节点的前一个节点 O(1)
listNextNode 返回给定节点的后一个节点 O(1)
listNodeValue 返回给定节点的值 O(1)

迭代器

Redis 为双端链表实现了一个迭代器 , 这个迭代器可以从两个方向对双端链表进行迭代:

  • 沿着节点的 next 指针前进,从表头向表尾迭代;
  • 沿着节点的 prev 指针前进,从表尾向表头迭代;

以下是迭代器的数据结构定义:

typedef struct listIter {

    // 下一节点
    listNode *next;

    // 迭代方向
    int direction;

} listIter;

direction 记录迭代应该从那里开始:

  • 如果值为 adlist.h/AL_START_HEAD ,那么迭代器执行从表头到表尾的迭代;
  • 如果值为 adlist.h/AL_START_TAIL ,那么迭代器执行从表尾到表头的迭代;

以下是迭代器的操作 API ,API 的作用以及算法复杂度:

函数 作用 算法复杂度
listGetIterator 创建一个列表迭代器 O(1)
listReleaseIterator 释放迭代器 O(1)
listRewind 将迭代器的指针指向表头 O(1)
listRewindTail 将迭代器的指针指向表尾 O(1)
listNext 取出迭代器当前指向的节点 O(1)

小结

  • Redis 实现了自己的双端链表结构。
  • 双端链表主要有两个作用:
    • 作为 Redis 列表类型的底层实现之一;
    • 作为通用数据结构,被其他功能模块所使用;
  • 双端链表及其节点的性能特性如下:
    • 节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,并且对链表的迭代可以在从表头到表尾和从表尾到表头两个方向进行;
    • 链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;
    • 链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长度);
时间: 2025-01-13 21:08:14

双端链表的相关文章

研磨数据结构与算法-02双端链表与双向链表

Node节点: /* * 链结点,相当于是车厢 */ public class Node { //数据域 public long data; //指针域 public Node next; public Node previous; public Node(long value) { this.data = value; } /** * 显示方法 */ public void display() { System.out.print(data + " "); } } 双端链表: /*

Java数据结构——用双端链表实现队列

//================================================= // File Name : LinkQueue_demo //------------------------------------------------------------------------------ // Author : Common //类名:FirstLastList //属性: //方法: class FirstLastList_long{ private Lin

Redis源码-数据结构之Adlist双端链表

Redis的Adlist实现了数据结构中的双端链表,整个结构如下: 链表节点定义: typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; 链表定义: typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); i

《Java数据结构与算法》笔记-CH5-链表-5用双端链表实现队列

1 //用双端链表实现队列 2 /** 3 * 节点类 4 */ 5 class LinkQ { 6 private long data; 7 public LinkQ next; 8 9 public LinkQ(long d) { 10 this.data = d; 11 } 12 13 public String toString() { 14 return String.valueOf(this.data); 15 } 16 } 17 /** 18 * 双端链表类 19 */ 20 cl

双端链表再次实现

package ch05; /** * 1:什么是双端链表. * 链表中保存着对最后一个链结点引用的链表 * 2:从头部进行插入 * 要对链表进行判断,如果为空则设置尾结点为新添加的结点 * 3:从尾部进行插入 * 如果链表为空,则直接设置头结点为新添加的结点 * 否则设置尾结点的后一个结点为新添加的结点 * 4:从头部进行删除 * 判断头结点是否有下一个结点,如果没有 * 则直接设置结点为null. * * */ public class FirstLastLinkList { //头结点 p

Data Handler 大模拟 + 双端链表 hdu 4268

E - Data Handler Time Limit:10000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Submit Status Description You are in charge of data in a company, so you are called "Data Handler". Different from the data in computer, the data

Java 模拟双端链表

双端链表: 双端链表与传统链表非常相似.只是新增了一个属性-即对最后一个链结点的引用rear 这样在链尾插入会变得非常容易,只需改变rear的next为新增的结点即可,而不需要循环搜索到最后一个节点 所以有insertFirst.insertLast 删除链头时,只需要改变引用指向即可:删除链尾时,需要将倒数第二个结点的next置空, 而没有一个引用是指向它的,所以还是需要循环来读取操作 /** * 双端链表 * @author stone */ public class TwoEndpoint

JAVA基础——链表结构之双端链表

双端链表:双端链表与传统链表非常相似.只是新增了一个属性-即对最后一个链结点的引用 如上图所示:由于有着对最后一个链结点的直接引用.所以双端链表比传统链表在某些方面要方便.比如在尾部插入一个链结点.双端链表可以进行直接操作 但传统链表只能通过next节点循环找到最后链结点操作.所以双端链表适合制造队列. 下面的双端链表类.有几个重要方法. insertFirst(插入首链结点) 这个方法与上篇博文的单链表是基本一样的.唯一区别就是,多了个last引用的操作.正常由于last是指向尾链结点的引用,

Java单链表、双端链表、有序链表实现

Java单链表.双端链表.有序链表实现 原创 2014年03月31日 23:45:35 标签: Java / 单链表 / 双端链表 / 有序链表 65040 单链表: insertFirst:在表头插入一个新的链接点,时间复杂度为O(1) deleteFirst:删除表头的链接点,时间复杂度为O(1) 有了这两个方法,就可以用单链表来实现一个栈了,见http://blog.csdn.net/a19881029/article/details/22579759 find:查找包含指定关键字的链接点