0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

UtFs_Zlgmcu7890 来源:互联网 作者:佚名 2017-09-25 14:14 次阅读

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

>>>>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框架与接口的编程(上)》。

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

    关注

    117

    文章

    3785

    浏览量

    81002
  • 周立功
    +关注

    关注

    38

    文章

    130

    浏览量

    37614
  • 链表
    +关注

    关注

    0

    文章

    80

    浏览量

    10558

原文标题:周立功:高效的双向链表就应该这样用

文章出处:【微信号:Zlgmcu7890,微信公众号:周立功单片机】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    立功教授谈迭代器模式设计

    近日立功教授公开了数年的心血之作《程序设计与数据结构》,电子版已无偿性分享到电子工程师与高校群体下载,经立功教授授权,特对本书内容进行连载。
    的头像 发表于 09-26 13:51 6296次阅读
    <b class='flag-5'>周</b><b class='flag-5'>立功</b>教授谈迭代器模式设计

    【Linux高级编译】list.h的高效应用—双向链表的实现

    【Linux高级编译】Linux内核的list.h的高效应用——双向链表的实现
    的头像 发表于 09-15 10:00 2599次阅读
    【Linux高级编译】list.h的<b class='flag-5'>高效</b>应用—<b class='flag-5'>双向</b><b class='flag-5'>链表</b>的实现

    立功大师EASY FPGA原理图

    本帖最后由 eehome 于 2013-1-5 09:47 编辑 立功EASYFPGA原理图立功大师经典力作,FPGA原理图。欢迎大家下载学习
    发表于 03-16 11:02

    立功的NIOS视频

    立功的NIOS视频
    发表于 07-19 09:55

    立功CANTest软件

    立功CANTest软件
    发表于 01-15 16:52

    立功CANTest软件

    立功CANTest软件
    发表于 02-27 09:26

    【HarmonyOS】双向循环链表

    了一个个双向循环链表,把指针的高效能运用到了极致,这也许就是编程的艺术吧!致敬鸿蒙内核开发者贡献了如此优秀的源码,鸿蒙内核源码可作为大学C语言,操作系统,数据结构三门课的教学项目
    发表于 10-20 15:39

    立功ARM培训精华

    立功ARM培训精华 ppt模式,还需下载分段压缩才可以查看
    发表于 02-11 09:13 108次下载

    TinyM0配套教程大全 立功

    TinyM0配套教程大全 立功
    发表于 04-20 16:30 0次下载

    立功ARM培训精华

    立功ARM培训精华,有需要的下来看看。
    发表于 01-13 17:23 41次下载

    算法与数据结构——双向链表

    第三章为算法与数据结构,本文为3.3 双向链表
    的头像 发表于 09-19 17:56 7289次阅读
    算法与数据结构——<b class='flag-5'>双向</b><b class='flag-5'>链表</b>

    立功新著内容分享:双向链表是什么?

    单向链表的添加、删除操作,都必须找到当前结点的上一个结点,以便修改上一个结点的p_next指针完成相应的操作。
    的头像 发表于 09-22 18:24 5983次阅读

    了解Linux通用的双向循环链表

    在linux内核中,有一种通用的双向循环链表,构成了各种队列的基础。链表的结构定义和相关函数均在include/linux/list.h中,下面就来全面的介绍这一链表的各种API。
    发表于 05-07 10:44 672次阅读

    单片机教程 立功

    单片机教程 立功
    发表于 11-19 14:17 0次下载

    双向循环链表的创建

    需要注意的是,虽然双向循环链表成环状,但本质上还是双向链表,因此在双向循环链表中,依然能够找到头
    的头像 发表于 05-24 16:27 2098次阅读