链表之LIST


上一篇博文 链表之SLIST_车子(chezi)-CSDN博客 说过,queue.h 里面有 5 种链表,分别是:

  1. SLIST
  2. LIST
  3. STAILQ
  4. TAILQ
  5. 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 的后面,我们看看哪些指针要改变

  1. n1 的 entries.le_next
  2. n2 的 entries.le_next
  3. n2 的 entries.le_prev
  4. 插入之前 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 的前面,我们看看哪些指针要改变

  1. n2 的 entries.le_prev
  2. n3 的 entries.le_next
  3. n3 的 entries.le_prev
  4. 在插入 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,我们看看哪些指针要改变

  1. n2 前面的那个节点(假设是 n1)的 entries.le_next
  2. 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单向链表是一种常见的数据结构,可以动态地存储一系列节点。每个节点包含数据和指向下一个节点的指针。 在C语言中,可以通过定义一个表示节点的结构体来实现单向链表: ```c struct Node { int data; struct Node* next; }; ``` 其中 `data` 用于存储节点的数据,`next` 指向下一个节点。链表的头节点可以通过一个指针来表示: ```c struct Node* head = NULL; ``` 现在,我们来实现插入节点的功能。 首先,需要创建一个新的节点,并为其分配内存空间: ```c struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); ``` 然后,可以给节点的 `data` 赋值: ```c newNode->data = newData; ``` 接下来,将新节点插入到链表中。 如果链表是空的,即头节点为 `NULL`,那么将新节点作为头节点: ```c if (head == NULL) { head = newNode; newNode->next = NULL; } ``` 否则,需要找到插入位置的前一个节点,将其 `next` 指向新节点,同时将新节点的 `next` 指向原来的后续节点: ```c else { struct Node* temp = head; while (temp->next != NULL) { temp = temp->next; } temp->next = newNode; newNode->next = NULL; } ``` 最后,记得要释放新节点的内存空间,以防止内存泄漏: ```c free(newNode); ``` 这样,就完成了单向链表的节点插入操作。 需要注意的是,在实际中,还需要考虑边界条件、异常情况的处理等,以确保代码的健壮性和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值