链表之LIST

Posted 车子 chezi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表之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

以上是关于链表之LIST的主要内容,如果未能解决你的问题,请参考以下文章

链表之LIST

链表之循环链表

算法习题---线性表之单链表的查找

数据结构线性表之实现单循环链表

数据结构(线性表之单链表)

线性链表之顺序表