450 likes | 542 Views
第十二讲 链表插入结点. 链表插入结点. 原则: 1 、插入操作不应破坏原链接关系 2 、插入的结点应该在它该在的位置。应该有一个插入位置的查找子过程。. 先看下面一个简单的例子: 已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为 10 。. head. 此结点已插入链表. 待插入结点. 参考程序. // 结构 7.c #include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量
E N D
链表插入结点 • 原则: • 1、插入操作不应破坏原链接关系 • 2、插入的结点应该在它该在的位置。应该有一个插入位置的查找子过程。
先看下面一个简单的例子:已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为10。先看下面一个简单的例子:已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为10。 head 此结点已插入链表 待插入结点
参考程序 // 结构7.c #include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量 #define LEN sizeof(struct numST) // 定义常量,表示结构长度 struct numST // 结构声明 { int num; // 整型数 struct numST *next; // numST结构指针 };
// 被调用函数insert(),两个形参分别表示链表和待插入的结点 void insert (struct numST **phead, struct numST *p) { // 函数体开始 struct numST *q,*r; // 定义结构指针q,r if ((*phead)==null) // 第一种情况,链表为空 { *phead = p; // 链表头指向p return; // 完成插入操作,返回 } else // 链表不为空 { // 第二种情况,p结点num值小于链表头结点的num值 if ((*phead)->num > p->num ) {// 将p结点插到链表头部 p->next = *phead;// 将p的next指针指向链表头(*phead) *phead = p;// 将链表头赋值为p return; // 返回 }
// 第三种情况,循环查找正确位置 r = *phead; // r赋值为链表头 q = (*phead)->next; // q赋值为链表的下一个结点 while (q!=null) // 利用循环查找正确位置 { // 判断当前结点num是否小于p结点的num if (q->num < p->num) { r = q; // r赋值为q,即指向q所指的结点 q = q->next;// q指向链表中相邻的下一个结点 } else // 找到了正确的位置 break; // 退出循环 } // 将p结点插入正确的位置 r->next = p; p->next = q; } }
// 被调用函数,形参为ST结构指针,用于输出链表内容 void print(struct numST *head) { int k=0; // 整型变量,用于计数 struct numST * r; // 声明r为ST结构指针 r=head; // r赋值为head,即指向链表头 while(r != null) // 当型循环,链表指针不为空则继续 { // 循环体开始 k=k+1; // 计数加1 printf("%d %d\n",k,r->num); r=r->next; // 取链表中相邻的下一个结点 } // 循环体结束 }
void main() // 主函数开始 { // 函数体开始 struct numST *head, *p;// ST型结构指针 head = null; // 初始化head为null // 分配3个ST结构的内存空间,用于构造链表 head = (struct numST *) malloc(LEN); head->next = (struct numST *) malloc(LEN); head->next->next = (struct numST *) malloc(LEN); // 为链表中的3个结点中的num赋值为5、10和15 head->num = 5; head->next->num = 10; head->next->next->num = 15; head->next->next->next = null; // 链表尾赋值为空 // 构造一个结点p,用于插入链表 p = (struct numST *) malloc(LEN); p->num = 12; p->next = null; insert(&head, p); // 调用insert函数将结点p插入链表 print(head); // 调用print函数,输出链表内容 } // 主函数结束
先看主函数 1、定义两个ST型结构指针*head,*p,并让head=null; 2、分配3个ST结构的内存空间,用于构造链表 (1)head=(struct numST*) malloc(LEN); (2)head->next=(struct numST*) malloc(LEN); (3)head->next->next=(struct numST*) malloc(LEN); head head->next head->next->next 这3个ST结构的内存空间如上图所示。
下面用赋值语句往这3个空间中放num数据。最后的一个结点为队尾,在其指针域存放null。下面用赋值语句往这3个空间中放num数据。最后的一个结点为队尾,在其指针域存放null。 (4)head->num=5; (5)head->next->num=10; (6)head->next->next->num=15; (7)head->next->next->next=null; 做了这4条之后形成了一条链表如下: head 该链表的头结点由head所指向。
3、构造一个结点p,在p结点的数据域放12,再插入链表3、构造一个结点p,在p结点的数据域放12,再插入链表 (1)p=(struct numST*) malloc(LEN); (2)p->num=12;; (3)p->next=null; 4、调用insert函数来插入p结点。 语句为 insert(&head,p); 意思是以将p插入到以head为队头的链表中。但这里在调用时,不是用head作为实参,而是用&head作为实参,属于传址调用,而非传值调用。
这里要讲传址调用和传值调用的区别 (1)如果是传值调用主程序中的调用语句为 insert(head,p);被调用函数为void insert(struct munST *phead, struct numST*p); 实际参数 head p p phead 形式参数
当着实际参数head赋给了形式参数phead之后,phead就指向了已经存在了的链表,见下图。当着实际参数head赋给了形式参数phead之后,phead就指向了已经存在了的链表,见下图。 head phead 这时原来的主程序中的头指针就不再起作用了。而是phead起作用。假如现在p中的结点数据为4小于5,应该将p插入到phead所指向的结点前,如下图
head phead phead 被调用函数无法改变head,这时head不再是头结点的指针了。如果被调用函数返回head,主函数只能表示head为头指针的三个结点,新插入的结点并没有包含进去。要想将新的插入到最前面的结点包含进去,就必须用传址调用。
(2)如果是传址调用主程序中的调用语句为 insert(&head,p); 被调用函数为 void insert(struct munST **phead, struct numST*p);先看struct numST **phead 是说定义(*phead)为指向numST结构的指针。*phead是指向numST结构的指针,phead又是指向*phead这个指针的指针。phead=&head
&head head phead 主程序中的实参为链表头指针head所在的地址值,传给被调用函数的phead的指针变量中,起到了让phead也指向head的目的。
在主函数中head为头指针,在被调用的子函数中phead为头指针的地址,head和*phead是同一个单元,只不过分别叫不同的名罢了。当然在子函数中无论插入什么结点都会让*phead指向链表的头。自然返回到主函数后,head也会是指向同一链表的头。 在主函数中head为头指针,在被调用的子函数中phead为头指针的地址,head和*phead是同一个单元,只不过分别叫不同的名罢了。当然在子函数中无论插入什么结点都会让*phead指向链表的头。自然返回到主函数后,head也会是指向同一链表的头。 从这个例子中读者可以领回到传值调用与传址调用的区别。 5、这样在子函数做插入结点的过程中,头指针的改变也能反映到主函数中来。调用print函数,从head开始输出整个链表的内容。
下面我们来研究insert函数 前提是主程序已将两个实参传给了insert函数的两个形参,这时*phead=head,p所指向的就是待插入的一个结点。事先定义两个结构指针q和r。 第一种情况: *phead==null,说明主程序传过来的头指针为空,即链表为空,一个结点都不存在。这时待插入的p结点就是链表中的第一个结点。只要执行如下两条语句即可 *phead=p; // 将表头指针指向p结点 return; // 返回主程序 在主程序中必然头指针head指向p结点。
第二种情况: p结点的num值小于链表头结点的num值,即(*phead)->num>p->num。这时要将p结点插入到头结点的前面,要执行如下三条语句 p->next=*phead; // 在p结点的指针域赋以头结点的地址值 *phead=p; // 将头结点指针phead指向p结点 return; // 返回主程序 这种情况如下图 *phead *phead null p
第三种情况: 前两种情况,无论遇到哪一种,都会返回主程序。只要不返回就是这第三种情况,即p结点的num大于头指针所指向的结点的num值。这时肯定地说p结点要插入到头结点之后,究竟要插到哪里需要找到应该插入的位置。我们设指针r和指针q,分别指向相邻的两个结点,r在前q在后,当着满足 r->num < p->num < q->num 时,p就插在r与q之间。我们看图 r q r q head p null
一开始让 r=*phead,让 q=*phead->next (1) 当指针q为空指针时,说明原链表中只有一个结点,即r指向的结点,这时只要将p结点接在r之后即可。执行 r->next=p; p->next=q; (2) 如果q!=null,说明起码有两个结点在链表中,接着要判p结点的num值是否大于q结点的num值。如果是大,则说明p应插在q之后而不是之前,这时让r和q指针同时后移一步,即 r=q; q=q->next;
执行(2) 在q!=null的情况下,如果p->num<=q->num了,说明这时找到了正确的插入位置,退出while循环,将p结点插入到r后,q前即可。使用的语句为 r->next=p; p->next=q; 在下面我们画出该算法的结构框图
作业 1、按下表顺序输入某班的一个学习小组的成员表 希望你将学习小组形成一个链表,每人一个结点。结点中有四个成员:姓名、出生年、出生月、指针。在链表中生日大者在前,小者在后。 建成链表后输出该链表。
2、一年后钱亮同学调至其它学习小组,希望你编程从原链表中删除钱亮所在结点,之后输出该链表。2、一年后钱亮同学调至其它学习小组,希望你编程从原链表中删除钱亮所在结点,之后输出该链表。 提示:原链表如下: head 查找待删除的结点的位置,要从链头找起。 (1)如果是链头结点,即有head->name==待删者name这时只要做head=head->next;即可 (2)如果不是链头结点,要设两个指针r和q,初始时让r=head; q=head->next;
(3)只要q!=null,就比较q->name是否为待删者的name?如果是则让 r->next=q->next; 如不是,就让r与q同时后移一步,即 r=q; q=q->next;然后转向(3) (4)如果发现q已是null,又未找到待删结点,则输出该人不在这个表中的信息。在原链表中一旦查到钱亮所在结点位置q,让 r->next = q->next; 意味着将孙参所在结点指向钱亮的指针,不再指向钱亮,而指向武陆
链表结构是利用结构中的指针域将每个结点链接起来,形似链条,属于线性结构数据。链表结构是利用结构中的指针域将每个结点链接起来,形似链条,属于线性结构数据。 下面介绍一种非线性结构的东西,二叉树。 先看下例: root 结点4 结点6 结点2 结点7 结点3 结点5 结点1
在图中 • (1)外形象一棵倒立的树 • (2)最上层有一个“根结点”,指针root指向根结点。 • (3)每个结点都是一个结构,一个成员是整型数据,两个成员是指针,分为左指针L和右指针R。 • (4)根结点的左指针指向左子树;右指针指向右子树。 • (5)左子树或右子树本身又是一棵二叉树,又有它们自己的左子树和右子树,…… 这是递归定义的,因此,在处理时常可用递归算法。
二叉树的遍历 树的遍历是指访遍树中的所有结点。 对比看遍历一个单链表,从表头开始按一个方向从头到尾就可遍历所有结点。对二叉树来说就没有这样简单了,因为对树或是对子树都存在根(或子树的根)和左子树、右子树,先遍历谁?由之产生了三种不同的方法:
1、前序法: 1.1 先访问根结点; 1.2 遍历左子树; 1.3 遍历右子树; 2、中序法: 2.1 遍历左子树; 2.2 访问根; 2.3 遍历右子树; 3、后序法 3.1 遍历左子树; 3.2 遍历右子树; 3.3 访问根;
我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针p指向二叉树的根结点我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针p指向二叉树的根结点 • 定义树的结构 • struct TREE • { • int data; • struct TREE *L, *R; • }; • 定义p为TREE结构的指针 • struct TREE *p; • 让LNR(P)为对以p为根的树作中序遍历的子函数。可得出如下图所示的递归算法与或结点图
该图说明如下: • 1、A结点表示中序遍历p结点为根的二叉树,函数为LNR(p)。该结点为“或”结点,有两个分支。当p为空时,A取B结点,什么都不做;当p不空时,说明树存在(起码有一个根),有结点C。 • 2、C结点为一个“与”结点,要依次做相关联的三件事情: • 2.1 D结点:中序遍历p的左子树,函数为LNR(p->L); • 2.2 E结点:直接可解结点,访问p结点(比如输出p结点数据域中的值)。 • 2.3 F结点:中序遍历p的右子树,函数为LNR(p->R)
3、比较LNR(p)与LNR(p->L)及LNR(p->R)可以看出,都是同一个函数形式,只不过代入了不同的参数,从层次和隶属关系看,p是父结点的指针,而p->L和p->R是子结点的指针,p->L是左子树的根,p->R是右子树的根。3、比较LNR(p)与LNR(p->L)及LNR(p->R)可以看出,都是同一个函数形式,只不过代入了不同的参数,从层次和隶属关系看,p是父结点的指针,而p->L和p->R是子结点的指针,p->L是左子树的根,p->R是右子树的根。 下面请大家做一个练习,依图2画“与或”图将图1所示的二叉树用中序遍历,将所访问到的结点数据输出。如图3
什么都不做 访问结点1:输出3
二叉树的建立 建立二叉树的过程是一个“插入”过程,下面我们用一个例子来讲解这一过程。 我们想建立这样一棵二叉树,树中的每一个结点有一个整数数据名为data,有两个指针:左指针L,右指针R,分别指向这个结点的左子树和右子树,显然可以用如下名为TREE的结构来描述这种结点: struct TREE { int data; struct TREE *L, *R; }
对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图1的中序遍历的方式。即如果待插入结点的数据比根结点的数据小,则将其插至左子树,否则插入右子树。对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图1的中序遍历的方式。即如果待插入结点的数据比根结点的数据小,则将其插至左子树,否则插入右子树。 定义一个递归函数 void insert(struct TREE **proot, struct TREE *p) 其中,指针p指向含有待插入数据的结点。 proot为树的根指针的地址。 insert函数棵理解为将p结点插入到*proot所指向的树中。
注意在上图中proot是被调用函数的形参。从前面对它的定义看,proot是指针的指针,实际上是指向二叉树根结点的指针的指针,或者说是指向二叉树根结点的指针的地址。如下图。因此,在主程序调用insert函数时,注意在上图中proot是被调用函数的形参。从前面对它的定义看,proot是指针的指针,实际上是指向二叉树根结点的指针的指针,或者说是指向二叉树根结点的指针的地址。如下图。因此,在主程序调用insert函数时, 实参 根结点 &root proot 实参为 &root,p 形参为 proot, p 下面是建立二叉树的参考程序。
#include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量 #define LEN sizeof(struct TREE) // 定义常量,表示结构长度 struct TREE // 结构声明 { int data; // 整型数 struct TREE *L,*R; // TREE结构指针 };
// 被调用函数insert,将结点插入二叉树 void insert (struct TREE **proot, struct TREE* p) { // 函数体开始 if (*proot==null) // 如果根结点为空 { *proot = p; // 将结点p插入根结点 return; // 返回 } else // 根结点不为空 { // 如果p结点数据小于等于根结点数据 if (p->data <= (*proot)->data) insert( &((*proot)->L), p); // 插入左子树 else // 如果p结点数据大于等于根结点数据 insert( &((*proot)->R), p); // 插入右子树 } } // 函数体结束
// 被调用函数,形参为TREE结构指针,输出二叉树内容 void print(struct TREE *root) { // 函数体开始 if (root == null) // 根或子树根结点为空 return; // 返回 print(root->L); // 输出左子树内容 printf("%d",root->data);// 输出根结点内容 print(root->R); // 输出右子树内容 } // 被调用函数结束 void main() // 主函数开始 { // 函数体开始 struct TREE *root, *p; // TREE型结构指针 int temp; // 临时变量,用于用户输入数据 root = null; // 初始化二叉树根结点为空 p = null; // 初始化待插入结点的指针为空 printf("请输入待插入结点的数据\n"); // 提示信息 printf("如果输入-1表示插入过程结束\n");// 提示信息 scanf("%d",&temp); // 输入待插入结点数据
while(temp != -1) // 当型循环,-1为结束标志 { // 循环体开始 // 为待插入结点分配内存单元 p = (struct TREE *) malloc(LEN); p->data = temp; // 将temp赋值给p结点的数据域 p->L = p->R = null; // 将p结点的左右指针域置为空 insert( &root, p ); // 将p结点插入到根为root的树中, // &root表示二叉树根结点的地址 printf("请输入待插入结点的数据\n"); // 提示信息 printf("如果输入-1表示插入过程结束\n");// 提示信息 scanf("%d",&temp); // 输入待插入结点数据 } // 循环体结束 if (root==null) // 如果根结点为空 printf("这是一棵空树。\n");// 输出空树信息 else // 根结点不为空 print(root); // 调用print函数,输出二叉树内容 } // 主函数结束