链表之LIST
Posted 车子 chezi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表之LIST相关的知识,希望对你有一定的参考价值。
文章目录
上一篇博文 链表之SLIST_车子(chezi)-CSDN博客 说过,queue.h 里面有 5 种链表,分别是:
- SLIST
- LIST
- STAILQ
- TAILQ
- CIRCLEQ
这篇文章说 LIST
LIST 示意图
LIST 是双向无尾非循环链表。双向链表有前向的指针,因此可以执行一些前向操作。
仔细看图你会发现,le_next 这个指针指向后面的大结构体,这个不稀奇;稀奇的是,le_prev 这个指针,它指的不是前面的大结构体,而是前面的 le_next。为什么这样设计呢?卖个关子,答案在我以后的文章中。
接口和实现
注意:不同的版本,代码可能有差异。
/*
* List declarations.
*/
#define LIST_HEAD(name, type) \\
struct name \\
struct type *lh_first; /* first element */ \\
#define LIST_HEAD_INITIALIZER(head) \\
NULL
#define LIST_ENTRY(type) \\
struct \\
struct type *le_next; /* next element */ \\
struct type **le_prev; /* address of previous next element */ \\
/*
* List functions.
*/
#define LIST_EMPTY(head) ((head)->lh_first == NULL)
#define LIST_FIRST(head) ((head)->lh_first)
#define LIST_FOREACH(var, head, field) \\
for ((var) = LIST_FIRST((head)); \\
(var); \\
(var) = LIST_NEXT((var), field))
#define LIST_INIT(head) do \\
LIST_FIRST((head)) = NULL; \\
while (0)
#define LIST_INSERT_AFTER(listelm, elm, field) do \\
if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\\
LIST_NEXT((listelm), field)->field.le_prev = \\
&LIST_NEXT((elm), field); \\
LIST_NEXT((listelm), field) = (elm); \\
(elm)->field.le_prev = &LIST_NEXT((listelm), field); \\
while (0)
#define LIST_INSERT_BEFORE(listelm, elm, field) do \\
(elm)->field.le_prev = (listelm)->field.le_prev; \\
LIST_NEXT((elm), field) = (listelm); \\
*(listelm)->field.le_prev = (elm); \\
(listelm)->field.le_prev = &LIST_NEXT((elm), field); \\
while (0)
#define LIST_INSERT_HEAD(head, elm, field) do \\
if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \\
LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\\
LIST_FIRST((head)) = (elm); \\
(elm)->field.le_prev = &LIST_FIRST((head)); \\
while (0)
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
#define LIST_REMOVE(elm, field) do \\
if (LIST_NEXT((elm), field) != NULL) \\
LIST_NEXT((elm), field)->field.le_prev = \\
(elm)->field.le_prev; \\
*(elm)->field.le_prev = LIST_NEXT((elm), field); \\
while (0)
举例
我们举例说明,代码来自 https://manpages.courier-mta.org/htmlman3/list.3.html,我稍微改了一下。
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/queue.h>
struct entry
int data;
LIST_ENTRY(entry) entries; /* List */
;
LIST_HEAD(listhead, entry);
int
main(void)
struct entry *n1, *n2, *n3, *np;
struct listhead head; /* List head */
int i;
LIST_INIT(&head); /* Initialize the list */
n1 = malloc(sizeof(struct entry)); /* Insert at the head */
n1->data = 1; //我加的
LIST_INSERT_HEAD(&head, n1, entries);
n2 = malloc(sizeof(struct entry)); /* Insert after */
n2->data = 2; //我加的
LIST_INSERT_AFTER(n1, n2, entries);
n3 = malloc(sizeof(struct entry)); /* Insert before */
n3->data = 3; //我加的
LIST_INSERT_BEFORE(n2, n3, entries);
/* Forward traversal */
LIST_FOREACH(np, &head, entries)
printf("%i\\n", np->data);
LIST_REMOVE(n2, entries); /* Deletion */
free(n2);
/* Forward traversal */
LIST_FOREACH(np, &head, entries)
printf("%i\\n", np->data);
/* List deletion */
n1 = LIST_FIRST(&head);
while (n1 != NULL)
n2 = LIST_NEXT(n1, entries);
free(n1);
n1 = n2;
LIST_INIT(&head);
exit(EXIT_SUCCESS);
代码分析
代码呢,就是这么个代码。咱们一点一点看。
LIST_ENTRY 和 LIST_HEAD
struct entry
int data;
LIST_ENTRY(entry) entries; /* List */
;
LIST_HEAD(listhead, entry);
宏展开就是:
struct entry
int data;
struct // 没有标签的结构体
struct entry *le_next;
struct entry **le_prev;
entries;
;
struct listhead
struct entry *lh_first;
;
和 slist 很相似,唯一的区别是 entries 里面有 2 个指针,因为是双向,当然有 2 个指针了。注意,le_prev 的类型是指向指针的指针,即二级指针,这是为什么呢?
LIST_INIT 和 LIST_INSERT_HEAD
struct entry *n1, *n2, *n3, *np;
struct listhead head; /* List head */
int i;
LIST_INIT(&head); /* Initialize the list */
n1 = malloc(sizeof(struct entry)); /* Insert at the head */
n1->data = 1; //我加的
LIST_INSERT_HEAD(&head, n1, entries);
第 5 行就是 (&head)->lh_first = NULL;
初始化一个空的链表
第 9 行宏替换是:
if (((n1)->entries.le_next = (&head)->lh_first) != ((void *)0))
(&head)->lh_first->entries.le_prev = &(n1)->entries.le_next;
(&head)->lh_first = (n1);
(n1)->entries.le_prev = &(&head)->lh_first;
第 1 行的前半部分 ((n1)->entries.le_next = (&head)->lh_first)
和第 3 行完成头插
第一行的后半部分判断原先是不是空链表,如果是,if 条件不成立,否则会执行第 2 行。
第 4 行:让 n1 的 entries.le_prev 指向 (&head)->lh_first;lh_first 的类型是 struct entry *,而 le_prev 的类型是 struct entry **
第 2 行不好理解,让插入前的第一个节点的 entries.le_prev 指向 n1 的 entries.le_next。
所以,le_prev 被定义成 struct entry ** 就可以理解了,作者的意图是让它指向 struct entry * 类型,比如 lh_first 和 le_next
这时候再看本文开头的图,就理解了。
node 相当于是代码中的 struct entry
SLIST_INSERT_HEAD(&head, n1, entries) 的意思是:把 n1 节点插入到链表 head 的头部;第一个参数是表头的地址,第二参数是待插入的节点的地址,第三个参数是无标签结构体的成员名。
LIST_INSERT_AFTER
n2 = malloc(sizeof(struct entry)); /* Insert after */
n2->data = 2; //我加的
LIST_INSERT_AFTER(n1, n2, entries);
LIST_INSERT_AFTER(n1, n2, entries) 表示把节点 n2 插入到 n1 的后面。
第 3 行,宏替换后是
if (((n2)->entries.le_next = (n1)->entries.le_next) != ((void *)0))
(n1)->entries.le_next->entries.le_prev = &(n2)->entries.le_next;
(n1)->entries.le_next = (n2);
(n2)->entries.le_prev = &(n1)->entries.le_next;
把节点 n2 插入到 n1 的后面,我们看看哪些指针要改变
- n1 的 entries.le_next
- n2 的 entries.le_next
- n2 的 entries.le_prev
- 插入之前 n1 后面的那个节点(就叫它 n0 吧)的 entries.le_prev;如果 n0 等于 NULL,就不需要考虑了
第 1 行的 (n2)->entries.le_next = (n1)->entries.le_next
解决的是 2
第 2 行代码解决的是 4
第 3 行代码解决了 1
第 4 行代码解决了 3
LIST_INSERT_BEFORE
n3 = malloc(sizeof(struct entry)); /* Insert before */
n3->data = 3; //我加的
LIST_INSERT_BEFORE(n2, n3, entries);
LIST_INSERT_BEFORE(n2, n3, entries) 意思是把 n3 插入到 n2 的前面
第 3 行宏替换后是:
(n3)->entries.le_prev = (n2)->entries.le_prev;
(n3)->entries.le_next = (n2);
*(n2)->entries.le_prev = (n3);
(n2)->entries.le_prev = &(n3)->entries.le_next;
把 n3 插入到 n2 的前面,我们看看哪些指针要改变
- n2 的 entries.le_prev
- n3 的 entries.le_next
- n3 的 entries.le_prev
- 在插入 n3 之前, n2 前面的那个节点(就叫它 n1 吧)的 entries.le_next;
第 1 行解决了 3
第 2 行解决了 2
第 3 行解决了 4,这个有点复杂。(n2)->entries.le_prev 指向 n1 的 entries.le_next,对 (n2)->entries.le_prev 解引用,得到的就是 n1 的 entries.le_next,插入后,因为 n1 的后面是 n3,所以 n1 的 entries.le_next = n3;
第 4 行解决了 1
LIST_FOREACH
/* Forward traversal */
LIST_FOREACH(np, &head, entries)
printf("%i\\n", np->data);
宏展开后是:
for ((np) = ((&head)->lh_first); (np); (np) = ((np)->entries.le_next))
printf("%i\\n", np->data);
遍历每个节点,这个比较好懂。
注意,这种遍历是不能删除的,因为如果把 np 指向的节点删除了,
(np) = ((np)->entries.le_next)
这句就不对了。
LIST_FOREACH(np, &head, entries) 用来遍历链表的每个节点。第一个参数是临时变量,指向当前的节点,第二个参数是表头的地址,第三个 entries 是无标签结构体的成员名。
LIST_REMOVE
LIST_REMOVE(n2, entries); /* Deletion */
宏替换后是
if ((n2)->entries.le_next != ((void *)0))
(n2)->entries.le_next->entries.le_prev = (n2)->entries.le_prev;
*(n2)->entries.le_prev = (n2)->entries.le_next;
要删除 n2,我们看看哪些指针要改变
- n2 前面的那个节点(假设是 n1)的 entries.le_next
- n2 后面的那个节点(假设是 n3)的 entries.le_prev;如果 n3 是 NULL,那就不用了
第 3 行解决了 1
1-2 行解决了 2
LIST_FIRST 和 LIST_NEXT
n1 = LIST_FIRST(&head);
while (n1 != NULL)
n2 = LIST_NEXT(n1, entries);
free(n1);
n1 = n2;
1:展开后是 n1 = ((&head)->lh_first);
LIST_FIRST(&head) 表示取链表的第一个节点
3:展开后是 n2 = ((n1)->entries.le_next);
LIST_NEXT(n1, entries) 表示取节点 n1 的下一个节点
参考资料
https://manpages.courier-mta.org/htmlman3/list.3.html
以上是关于链表之LIST的主要内容,如果未能解决你的问题,请参考以下文章