草庐IT

朝花夕拾-链表(二)

程语有云 2023-03-28 原文

"Good code is its own best documentation." - Steve McConnell

“好代码本身就是最好的文档。” —— 史蒂夫·麦克康奈尔

0x00 大纲

0x01 前言

数据与结构的解耦

在上篇文章,我们通过将链表的节点放在具体数据类型的结构体内,这样,抽象(链表结构)不再依赖于细节(数据类型),细节(数据类型)依赖于抽象(链表结构),利用依赖倒置的思想,完成了数据与结构的解耦,进而实现了通用的链表。

offsetof

offsetof 是定义在C标准库头文件<stddef.h>中的一个宏,它会生成一个类型为size_t的无符号整型,代表一个结构成员相对于结构体起始的字节偏移量offset)。

container_of

cotainer_of返回的是结构体成员所在结构体的起始地址(指针),它的原理是用成员变量的起始地址减去成员变量在结构体内的偏移量(用offsetof求得)。

通用链表

有了上面三个理论基础,我们就具备了创建通用链表的条件。下面将通过具体的代码来演示如何构造和使用这样的链表结构,全程图解,包你学会。

0x02 链表节点

我们的通用链表是一个双向循环链表,它由一个链表头节点list_head和若干个位于结构体中的中间节点list_node(注意不包括数据域部分)构成。

我们定义了一个名为struct list_head的结构体类型作为我们的链表节点,它包含一个指向前驱节点的指针*prev和一个指向后继节点的指针*next。同时,为了方便后续的编码和增强代码的可读性,又定义了list_headlist_node两个结构体类型别名,它们是同一种数据类型的不同名称。

typedef struct list_head
{
    struct list_head *next, *prev;
} list_head, list_node;

下面的代码简单说明了这种方法给我们带来的语义上的便利,后面的代码示例将延续这样的风格。

list_head head;// 等价于 struct list_head head;
list_node node;// 等价于 struct list_head node;

0x03 创建链表

/**
 * 初始化一个链表(头)节点,它的前驱和后继指针都指向自己
 * @param list: 需要初始化的节点的指针
 * @return void
**/
static inline void list_init(list_head *list)
{
    list->next = list;
    list->prev = list;
}

0x04 插入节点

任意位置的插入

/**
 * 将节点entry插入到prev和next之间
 * @param entry: 新节点的指针
 * @param prev: 指向插入位置前驱节点的指针
 * @param next: 指向插入位置后继节点的指针
 * @return void
**/
static inline void __list_add(list_node *entry,
                              list_node *prev,
                              list_node *next)
{
    next->prev = entry;
    entry->next = next;
    entry->prev = prev;
    prev->next = entry;
}

插入到最前

/**
 * 将节点entry插入到头节点之后
 * 头插,新节点成为第一个节点
 * @param entry: 指向新节点的指针
 * @param head: 指向头节点的指针
 * @return void
**/
static inline void list_add_head(list_node *entry,
                                 list_head *head)
{
    __list_add(entry, head, head->next);
}

插入到最后

/**
 * 将节点entry插入到头节点之前
 * 尾插,新节点成为最后一个节点
 * @param entry: 指向新节点的指针
 * @param head: 指向头节点的指针
 * @return void
**/
static inline void list_add_tail(list_node *entry,
                                 list_head *head)
{
    __list_add(entry, head->prev, head);
}

0x05 删除节点

/**
 * 删除节点
 * @param prev: 被删除节点的前驱指针
 * @param head: 被删除节点的后继指针
 * @return void
**/
static inline void __list_del(list_node * prev,
                              list_node * next)
{
    next->prev = prev;
    prev->next = next;
}
/**
 * 删除节点,并将其前驱指针和后继指针指向NULL
 * @param prev: 指向被删除节点的指针
 * @return void
**/
static inline void list_del(list_node *entry)
{
    __list_del(entry->prev, entry->next);
    entry->prev = entry->next = NULL;
}

可以看到,由于节点本身并不存储数据,所以,在删除链表节点的时候,也就不用考虑对数据域进行内存释放的操作。

0x06 替换节点

/**
 * 替换节点
 * @param old: 指向被替换节点的指针
 * @param entry: 指向新节点的指针
 * @return void
**/
static inline void list_replace(list_node *old,
                                list_node *entry)
{
    entry->next = old->next;
    entry->next->prev = entry;
    entry->prev = old->prev;
    entry->prev->next = entry;
}

0x07 遍历和获取节点数据

遍历链表

/**
 * 快速遍历链表(不可进行删除操作)
 * @param pos: 指向当前节点位置的指针
 * @param head: 指向头节点的指针
 * @return void
**/
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)
/**
 * 遍历链表(可进行删除操作)
 * @param pos: 指向当前节点位置的指针
 * @param n: 指向下一节点位置的指针
 * @param head: 指向头节点的指针
 * @return void
**/
#define list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)

获取节点数据

/**
 * 获得节点所在数据结构体的起始地址(指针)
 * @param ptr: 指向节点的指针
 * @param type: 数据结构体类型
 * @param member: 节点在数据结构体中被定义的变量名称
 * @return void
**/
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

0x08 小结

将上述的所有基本操作汇总后,得到我们的通用链表定义文件list.h(你可以在Linux内核的源码中找到它,这里的代码稍微作了一点修改):

#ifndef LIST_H
#define LIST_H
#include <stddef.h>
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr)-offsetof(type,member)))
typedef struct list_head
{
    struct list_head *next, *prev;
} list_head, list_node;
/**
 * 初始化一个链表(头)节点,它的前驱和后继指针都指向自己
 * @param list: 需要初始化的节点的指针
 * @return void
**/
static inline void list_init(list_head *list)
{
    list->next = list;
    list->prev = list;
}
/**
 * 将节点entry插入到prev和next之间
 * @param entry: 新节点的指针
 * @param prev: 指向插入位置前驱节点的指针
 * @param next: 指向插入位置后继节点的指针
 * @return void
**/
static inline void __list_add(list_node *entry,
                              list_node *prev,
                              list_node *next)
{
    next->prev = entry;
    entry->next = next;
    entry->prev = prev;
    prev->next = entry;
}
/**
 * 将节点entry插入到头节点之后
 * 头插,新节点成为第一个节点
 * @param entry: 指向新节点的指针
 * @param head: 指向头节点的指针
 * @return void
**/
static inline void list_add_head(list_node *entry,
                                 list_head *head)
{
    __list_add(entry, head, head->next);
}
/**
 * 将节点entry插入到头节点之前
 * 尾插,新节点成为最后一个节点
 * @param entry: 指向新节点的指针
 * @param head: 指向头节点的指针
 * @return void
**/
static inline void list_add_tail(list_node *entry,
                                 list_head *head)
{
    __list_add(entry, head->prev, head);
}
/**
 * 删除节点
 * @param prev: 被删除节点的前驱指针
 * @param head: 被删除节点的后继指针
 * @return void
**/
static inline void __list_del(list_node * prev,
                              list_node * next)
{
    next->prev = prev;
    prev->next = next;
}
/**
 * 删除节点,并将其前驱指针和后继指针指向NULL
 * @param prev: 指向被删除节点的指针
 * @return void
**/
static inline void list_del(list_node *entry)
{
    __list_del(entry->prev, entry->next);
    entry->prev = entry->next = NULL;
}
/**
 * 替换节点
 * @param old: 指向被替换节点的指针
 * @param entry: 指向新节点的指针
 * @return void
**/
static inline void list_replace(list_node *old,
                                list_node *entry)
{
    entry->next = old->next;
    entry->next->prev = entry;
    entry->prev = old->prev;
    entry->prev->next = entry;
}
/**
 * 判断循环双链表是否为空(只有头节点)
 * @param head: 指向头节点的指针
 * @return void
**/
static inline int list_empty(const list_head *head)
{
    return head->next == head;
}
/**
 * 快速遍历链表(不可进行删除操作)
 * @param pos: 指向当前节点位置的指针
 * @param head: 指向头节点的指针
 * @return void
**/
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)
/**
 * 遍历链表(可进行删除操作)
 * @param pos: 指向当前节点位置的指针
 * @param n: 指向下一节点位置的指针
 * @param head: 指向头节点的指针
 * @return void
**/
#define list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)
/**
 * 获得节点所在数据结构体的起始地址(指针)
 * @param ptr: 指向节点的指针
 * @param type: 数据结构体类型
 * @param member: 节点在数据结构体中被定义的变量名称
 * @return void
**/
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)
#endif // LIST_H

有关朝花夕拾-链表(二)的更多相关文章

  1. 【数据结构和算法】实现带头双向循环链表(最复杂的链表) - 2

    前文,我们实现了认识了链表这一结构,并实现了无头单向非循环链表,接下来我们实现另一种常用的链表结构,带头双向循环链表。如有仍不了解单向链表的,请看这一篇文章(7条消息)【数据结构和算法】认识线性表中的链表,并实现单向链表_小王学代码的博客-CSDN博客目录前言一、带头双向循环链表是什么?二、实现带头双向循环链表1.结构体和要实现函数2.初始化和打印链表3.头插和尾插4.头删和尾删5.查找和返回结点个数6.在pos位置之前插入结点7.删除指定pos结点8.摧毁链表三、完整代码1.DSLinkList.h2.DSLinkList.c3.test.c总结前言带头双向循环链表,是链表中最为复杂的一种结

  2. ruby - Ruby 有像栈、队列、链表、映射或集合这样的容器吗? - 2

    我在网上查了几个Ruby教程,他们似乎什么都用数组。那么如何在Ruby中实现以下数据结构呢?堆栈队列链表map组 最佳答案 (从评论中移出)好吧,通过限制堆栈或队列方法(push、pop、shift、unshift),数组可以是堆栈或队列。使用push/pop提供LIFO(后进先出)行为(堆栈),而使用push/shift或unshift/pop提供FIFO行为(队列)。map是hashes,和一个Set类已经存在。您可以使用类实现链表,但数组将使用标准数组方法提供类似于链表的行为。 关

  3. javascript - JS将数组转换为json链表? - 2

    我是JS的新手,组织数据的概念让我有些困惑,我试图从特定的数组格式中获取数据(因为这是我必须使用的格式)并将其输出为另一种特定的JSON格式。这是给D3sankey模块传递数据https://github.com/d3/d3-plugins/blob/master/sankey/sankey.js我不知道如何将节点的索引添加到链接中,而不是名称。真的,我完全迷失了它!我在这里做了一个fiddle:https://jsfiddle.net/adamdavi3s/kw3jtzx4/下面是所需数据和输出的示例vardata=[{"source":"Agricultural'waste'","

  4. 合并两个有序链表 - 2

    文章目录1.题目描述2.解题思路方法1:方法2:1.题目描述题目链接:力扣21,合并两个有序链表将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。2.解题思路方法1:首先我们能够想到的就是遍历一遍数组,判断两个结点的大小,将数值小的结点放在前面,数值大的不断尾插在后面。是不是听着挺简单的?具体实现:我们可以创建两个空指针,head用来存放链表的头结点,tail用来遍历两条链表,将两条链表链接起来。当某个链表为空时,我们可以直接返回另一条链表当两个链表都不为空时,我们可以不断比较两条链表的大小,当head和tail为空时,我们将较小的结点同时赋给head

  5. javascript - Javascript 中的链表与数组 - 2

    所以我在JS中玩弄链表并提出了以下问题:比方说,我们有一个数组和一个链表,它们都有5000个元素。我们想在索引10处插入新元素。数组方式非常简单。我们在给定索引处插入新元素,并将其余元素向前移动一个索引。所以我尝试用链表来做这件事,并以下面的方式结束它:从NicholasZakas获取链表的实现并添加附加方法addOnPosition(data,index)。最后是代码:functionLinkedList(){this._head=null;this._length=0;}LinkedList.prototype={constructor:LinkedList,add:functio

  6. 【数据结构与算法】一套链表 OJ 带你轻松玩转链表 - 2

    ✨个人主页:bitme✨当前专栏:数据结构✨刷题专栏:基础算法链表OJ🏳️一.移除链表元素🏴二.反转链表🏁三.链表的中间结点🚩四.链表中倒数第k个结点🏳️‍🌈五.合并两个有序链表🏳️‍⚧️六.链表的回文结构🏴‍☠️七.链表分割🏴󠁧󠁢󠁷󠁬󠁳󠁿八.相交链表🏳️‍🌈九.环形链表🍹十.环形链表II 🏳️一.移除链表元素简介:给你一个链表的头节点head和一个整数val,请你删除链表中所有满足Node.val==val的节点,并返回新的头节点。示例1:输入:head=[1,2,6,3,4,5,6],val=6输出:[1,2,3,4,5]示例2:输入:head=[],val=1输出:[]示例3:输入:he

  7. go - 输出反向链表时出现无限循环 - 2

    我正在学习Go并编写了以下代码来反转链表。但是,代码没有按预期工作。这是一个节点结构以及用于打印和反转列表的函数。typeNodestruct{numberintprevious*Nodenext*Node}funcPrintList(node*Node){forn:=node;n!=nil;n=n.next{fmt.Println(n)}}funcReverseList(node*Node){varnextNodeRef*Nodeforn:=node;n!=nil;n=n.previous{ifn.next==nil{n.next=n.previousn.previous=nil*n

  8. C语言实现链表--数据结构 - 2

    魔王的介绍:😶‍🌫️一名双非本科大一小白。魔王的目标:🤯努力赶上周围卷王的脚步。魔王的主页:🔥🔥🔥大魔王.🔥🔥🔥❤️‍🔥大魔王与你分享:很喜欢宫崎骏说的一句话:“不要轻易去依赖一个人,它会成为你的习惯当分别来临,你失去的不是某个人而是你精神的支柱,无论何时何地,都要学会独立行走,它会让你走得更坦然些。”文章目录一、前言二、链表实现1、创建结构体类型2、创建结点3、打印单链表4、单链表尾插5、单链表头插6、单链表尾删7、单链表头删8、单链表查找9、单链表插入☃️该位置之后插入☃️该位置之前插入(插入正常理解)10、单链表删除11、单链表销毁三、总代码SeqListNode.hSeqListNod

  9. go - 一个结构体的单向链表的初始化 - 2

    对于我正在处理的一项任务,我们被指示创建两个实现Stack接口(interface)(包括push、pop等方法)的数据结构。当我完成第一个结构时,链表部分让我不知所措。作为正在编写他们的第一个Go项目的人,我不确定如何处理以下指令:1.创建一个名为StackLinked的新结构,它实现了Stacker,并使用单(或双)链表作为其内部表示。2.除了实现Stacker中的所有方法外,还编写一个makeStackLinked()函数(不是方法!),该函数使用链表表示返回一个新的空堆栈我曾尝试这样实现:typeStackLinkedstruct{top*StackLinkednext*Sta

  10. go - 链表在 Golang 中不会改变 - 2

    我的问题是,当我将head指向head.next时input.Val仍然是1而不是2(这是下一个值)。typeListNodestruct{ValintNext*ListNode}functest(head*ListNode)*ListNode{head=head.Nextreturnhead}funcmain(){varinput,input2ListNodeinput=ListNode{Val:1,Next:&input2}}input2=ListNode{Val:2}test(&input)fmt.Println(input.Val)} 最佳答案

随机推荐