周立功阐释高效的双向链表如何用

电子说

1.3w人已加入

描述

近日周立功教授公开了数年的心血之作《程序设计与数据结构》,电子版已无偿性分享到电子工程师与高校群体下载,经周立功教授授权,特对本书内容进行连载。

>>>> 1.1.1 添加结点

假定还是将结点添加到链表尾部,其函数原型为:

int dlist_add_tail(dlist_head_t *p_head, dlist_node_t *p_node);

其中,p_head为指向链表头结点的指针,p_node为指向待添加结点的指针,其使用范例详见程序清单3.38

程序清单3.38  dlist_add_tail()函数使用范例

1    int main(int argc, char *argv[])

2    {

3         dlist_head_t head;

4         dlist_node_t node;

5

6         dlist_init(&head);

7         dlist_add_tail(&head, &node);

8          // ……

9    } 

为了实现该函数,可以先查看添加结点前后链表的变化,详见图3.18

周立功

图3.18  添加结点示意图

由此可见,添加一个结点至链表尾部,需要4个指针(图中虚线箭头):

● 新结点的p_prev指向尾结点;

● 新结点的p_next指向头结点;

● 尾结点的p_next由指向头结点变为指

向新结点;

● 头结点的p_prev由指向尾结点修改为指向新结点。

通过这些操作后,当结点添加到链表尾部后,就成为了新的尾结点,详见程序清单3.39

程序清单3.39  dlist_add_tail()函数实现

1    int dlist_add_tail (dlist_head_t *p_head, dlist_node_t *p_node)

2    {

3         if (p_head == NULL){

4               return -1;

5         }

6         p_node -> p_prev         = p_head->p_prev;  // 新结点的p_prev指向尾结点

7         p_node -> p_next         = p_head;                // 新结点的p_next指向头结点

8         p_head -> p_prev->p_next    = p_node;             // 尾结点的p_next指向新结点

9         p_head -> p_prev         = p_node;                // 头结点的p_prev指向新结点

10        return 0;

11  }

实际上循环链表,无论是头结点、尾结点还是普通结点,其本质上都是一样的,均为p_next成员指向下一个结点,p_prev成员指向其上一个结点。因此,对于添加结点而言,无论将结点添加到链表头、链表尾还是其它任意位置,其操作方法完全相同。为此,需要提供一个更加通用的函数,可以将结点添加到任意结点之后,其函数原型为:

int dlist_add (dlist_head_t *p_head, dlist_node_t *p_pos, dlist_node_t *p_node); 

其中,p_head为指向链表头结点的指针,p_pos指定了添加的位置,新结点即添加在该指针指向的结点之后;p_node为指向待添加结点的指针。比如,同样将结点添加到链表尾部,其使用范例详见程序清单3.40

程序清单3.40  dlist_add()函数使用范例

1    int main(int argc, char *argv[])

2    {

3         dlist_head_t head;

4         dlist_node_t node;

5

6         dlist_init(&head);

7         dlist_add(&head, &(head.p_prev),  &node);

8         // ……

9    } 

由此可见,将尾结点作为结点添加的位置,同样可以将结点添加至尾结点之后,即添加到链表尾部。显然,也就没有必要再编写dlist_add_tail()实现代码了使用dlisd_add()即可,修改dlist_add_tail()函数的实现详见程序清单3.41

程序清单3.41  dlist_add_tail()函数实现

1    int dlist_add_tail (dlist_head_t *p_head, dlist_node_t *p_node)

2    {

3         return dlist_add(p_head, p_head->p_prev, p_node);

4    } 

为了实现dlist_add()函数,可以先查看添加一个结点到任意结点之后的情况,详见图3.19。图中展示的是一种通用的情况,由于结点的添加位置(头、尾或其它任意位置)与添加结点的方法没有关系,因此没有特别标明头结点和尾结点。

其实,对比图3.18图3.19可以发现,图3.18展示的只是图3.19一个特例,即恰好图3.19中的新结点之前的结点就是尾结点,添加结点的过程同样需要修改4个指针的值。为便于描述,将新结点前的结点称之为前结点,新结点之后的结点称之为后结点。显然,在添加新结点之前,前结点的下一个结点即为后结点。对设置4个指针值的描述如下:

● 新结点的p_prev指向前结点;

● 新结点的p_next指向后结点;

● 前结点的p_next由指向后结点变为指向新结点;

● 后结点的p_prev由指向前结点修改为指向新结点。

对比将结点添加到链表尾部的描述,只要将描述中的“前结点”换为“尾结点”,“后结点”换为“头结点”,它们的含义则完全一样,显然将结点添加到链表尾部只是这里的一个特例,添加结点的函数实现详见程序清单3.42

程序清单3.42  dlist_add()函数实现

1    int dlist_add (dlist_head_t *p_head, dlist_node_t *p_pos, dlist_node_t *p_node)

2    {

3         if ((p_head == NULL) || (p_pos == NULL) || (p_node == NULL)){

4             return -1;

5         }

6         p_node->p_prev        = p_pos;                // 新结点的p_prev指向前结点

7         p_node->p_next        = p_pos->p_next;        // 新结点的p_next指向后结点

8         p_pos->p_next->p_prev = p_node;                    // 后结点的p_prev指向新结点

9         p_pos->p_next         = p_node;              // 前结点的p_next指向新结点

10        return 0;

11  }

尽管上面的函数在实现时并没有用到参数p_head但还是将p_head参数传进来了因为实现其它的功能时将会用到p_head参数比如判断p_pos是否在链表中。

有了该函数,添加结点到任意位置就非常灵活了,比如,提供一个添加结点到链表的头部,使其作为链表的第一个结点的函数,其函数原型为:

int dlist_add_head (dlist_head_t *p_head, dlist_node_t *p_node);

此时,头结点即为新添加结点的前结点,直接调用dlist_add()即可实现,其实现范例详见程序清单3.43

程序清单3.43  dlist_add_head()函数实现

1    int dlist_add_head (dlist_head_t *p_head, dlist_node_t *p_node)

2    {

3         return dlist_add(p_head, p_head, p_node);

4    }

>>>> 1.1.2 删除结点

基于添加结点到任意位置的思想,需要实现一个删除任意结点的函数。其函数原型为:

int dlist_del (dlist_head_t *p_head, dlist_node_t *p_node); 

其中,p_head为指向链表头结点的指针, p_node为指向待删除结点的指针,使用范例详见程序清单3.44

程序清单3.44  dlist_del()使用范例程序

1    int main(int argc, char *argv[])

2    {

3         dlist_head_t head;

4         dlist_node_t node;

5         dlist_init(&head);

6         dlist_add_tail(&head, &node);

7         dlist_del(&head, &node);

8         //......

9         return 0;

10  } 

为了实现dlisd_del()函数,可以先查看删除任意结点的示意图,图 3.20(1)为删除节点前的示意图,图 3.20(2)为删除节点后的示意图。

周立功

图 3.20添加结点示意图

由此可见,仅需要修改两个指针的值:

● 将“删除结点”的前结点的p_next修改为指向“删除结点”的后结点;

● 将“删除结点”的后结点的p_prev修改为指向“删除结点”的前结点。

删除结点函数的实现详见程序清单3.45

程序清单3.45  dlist_del()函数实现

1    int dlist_del (dlist_head_t *p_head, dlist_node_t *p_node)

2    {

3         if ((p_head == NULL) || (p_node == NULL) || (p_node == p_head)){

4             return -1;

5         }

6         p_node->p_prev->p_next = p_node->p_next;               // 前结点的p_next修改为指向后结点

7         p_node->p_next->p_prev = p_node->p_prev;               // 后结点的p_prev修改为指向前结点

8

9         p_node->p_next = NULL;

10        p_node->p_prev = NULL;

11       return 0;

12  } 

为了防止删除头结点,程序中对p_head与p_node进行了比较,当p_node为头结点时,则直接返回错误。

>>>> 1.1.3 遍历链表

与单向链表类似,需要一个遍历链表各个结点的函数,其函数原型(dlist.h)为:

int dlist_foreach (dlist_head_t            *p_head,

              dlist_node_process_t      pfn_node_process,

              void                  *p_arg);

其中,p_head指向链表头结点,pfn_node_process为结点处理回调函数,每遍历到一个结点时,均会调用该函数,便于用户处理结点。dlist_node_process_t类型定义如下

typedef int (*dlist_node_process_t) (void *p_arg, dlist_node_t *p_node);

dlist_node_process_t类型参数为一个p_arg指针和一个结点指针,返回值为int类型的函数指针。每遍历到一个结点均会调用pfn_node_process指向的函数,便于用户根据需要自行处理结点数据。当调用该回调函数时,传递给p_arg的值即为用户参数,其值与dlist_traverse()函数的第3个参数一样,即该参数的值完全是由用户决定的;传递给p_node 的值即为指向当前遍历到的结点的指针。当遍历到某个结点时,如果用户希望终止遍历,此时,只要在回调函数中返回负值即可终止继续遍历。一般地,若要继续遍历,则函数执行结束后返回0即可。dlist_foreach()函数的实现详见程序清单3.46

程序清单3.46  链表遍历函数的实现

1    int dlist_foreach (dlist_head_t         *p_head,

2                  dlist_node_process_t  pfn_node_process,

3                  void                *p_arg)

4    {

5        dlist_node_t  *p_tmp, *p_end;

6        int            ret;

7

8        if ((p_head == NULL) || (pfn_node_process == NULL)) {

9            return -1;

10      }

11

12      p_tmp = dlist_begin_get(p_head);

13      p_end = dlist_end_get(p_head);

14

15      while (p_tmp != p_end) {

16          ret = pfn_node_process(p_arg, p_tmp);

17          if (ret < 0) {                            // 不再继续遍历

18              return ret;

19          }

20          p_tmp = dlist_next_get(p_head, p_tmp);    // 继续下一个结点

21      }

22      return 0;

23  }

为了便于查阅,如程序清单3.47所示展示了dlist.h文件的内容。

程序清单3.47  dlist.h文件内容

1    #ipragma once

2    #include

3    #include

4   

5    typedef struct _dlist_node{

6         struct _dlist_node  *p_next;                    // 指向下一个结点的指针

7         struct _dlist_node  *p_prev;                    // 指向下一个结点的指针

8    }dlist_node_t;

9

10  typedef  dlist_node_t  dlist_head_t;

11 

12  // 链表遍历时的回调函数类型

13  typedef int (*dlist_node_process_t) (void *p_arg, dlist_node_t *p_node);

14

15  int dlist_init (dlist_head_t *p_head);                                           // 链表初始化

16

17  int dlist_add (dlist_head_t *p_head, dlist_node_t *p_pos, dlist_node_t *p_node);     // 添加结点至指定位置

18  int dlist_add_tail(dlist_head_t *p_head, dlist_node_t *p_node);                    // 添加结点至链表尾部

19  int dlist_add_head (dlist_head_t *p_head, dlist_node_t *p_node);                // 添加结点至链表头部

20  int dlist_del (dlist_head_t *p_head, dlist_node_t *p_node);                     // 删除一个结点

21

22  dlist_node_t *dlist_prev_get (dlist_head_t *p_head, dlist_node_t *p_pos);     // 寻找某一结点的前一结点

23  dlist_node_t *dlist_next_get (dlist_head_t *p_head, dlist_node_t *p_pos);     // 寻找某一结点的后一结点

24  dlist_node_t *dlist_tail_get (dlist_head_t *p_head);                     // 获取尾结点

25  dlist_node_t *dlist_begin_get (dlist_head_t *p_head);    // 获取开始位置,第一个用户结点

26  dlist_node_t *dlist_end_get (dlist_head_t *p_head);   // 获取结束位置,尾结点下一个结点的位置

27

28  int dlist_foreach (dlist_head_t         *p_head,

29                dlist_node_process_t  pfn_node_process,

30                void               *p_arg);

同样以int类型数据为例,来展示这些接口的使用方法。为了使用链表,首先应该定义一个结构体,将链表结点作为其一个成员,此外,再添加一些应用相关的数据,如定义如下包含链表结点和int型数据的结构体:

typedef struct _dlist_int{

           dlist_node_t  node;                                // 包含链表结点

           int          data;                            // int类型数据

}dlist_int_t;

综合范例程序详见程序清单3.48

程序清单3.48  综合范例程序

1    #include

2    #include "dlist.h"

3   

4    typedef struct _dlist_int{

5         dlist_node_t  node;                                // 包含链表结点

7         int          data;                            // int类型数据

8    }dlist_int_t;

9   

10  int list_node_process (void *p_arg, dlist_node_t *p_node)

11  {

12        printf("%d  ", ((dlist_int_t *)p_node) -> data);

13        return 0;

14  }

15

16  int main(int argc, char *argv[])

17  {

18        dlist_head_t  head;                               // 定义链表头结点

19        dlist_int_t    node1, node2, node3;

20       dlist_init(&head);

21

22        node1.data = 1;

23        dlist_add_tail(&head, &(node1.node));

24        node2.data = 2;

25        dlist_add_tail(&head, &(node2.node));

26        node3.data = 3;

27        dlist_add_tail(&head, &(node3.node));

28

29       dlist_del(&head, &(node2.node));                       // 删除node2结点

30       dlist_foreach(&head, list_node_process, NULL);      // 遍历链表,用户参数为NULL

31        return 0;

32  } 

与单向链表的综合范例程序比较可以发现程序主体是完全一样的仅仅是各个结点的类型发生了改变。对于实际的应用如果由使用单向链表升级为双向链表虽然程序主体没有发生改变但由于类型的变化则不得不修改所有程序代码。这是由于应用与具体数据结构没有分离造成的,因此可以进一步将实际应用与具体的数据结构分离,将链表等数据结构抽象为“容器”的概念。

在公众号后台回复关键字“程序设计”,即可在线阅读《程序设计与数据结构》;回复关键字“编程”,即可在线阅读《面向AMetal框架与接口的编程(上)》。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分