电子说
近日周立功教授公开了数年的心血之作《程序设计与数据结构》,电子版已无偿性分享到电子工程师与高校群体下载,经周立功教授授权,特对本书内容进行连载。
>>>> 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
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); // 链表初始化
1617 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); // 删除一个结点
2122 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));
2829 dlist_del(&head, &(node2.node)); // 删除node2结点
30 dlist_foreach(&head, list_node_process, NULL); // 遍历链表,用户参数为NULL
31 return 0;
32 }与单向链表的综合范例程序比较可以发现,程序主体是完全一样的,仅仅是各个结点的类型发生了改变。对于实际的应用,如果由使用单向链表升级为双向链表,虽然程序主体没有发生改变,但由于类型的变化,则不得不修改所有程序代码。这是由于应用与具体数据结构没有分离造成的,因此可以进一步将实际应用与具体的数据结构分离,将链表等数据结构抽象为“容器”的概念。
在公众号后台回复关键字“程序设计”,即可在线阅读《程序设计与数据结构》;回复关键字“编程”,即可在线阅读《面向AMetal框架与接口的编程(上)》。
全部0条评论
快来发表一下你的评论吧 !