1 / 45

第十二讲 链表插入结点

第十二讲 链表插入结点. 链表插入结点. 原则: 1 、插入操作不应破坏原链接关系 2 、插入的结点应该在它该在的位置。应该有一个插入位置的查找子过程。. 先看下面一个简单的例子: 已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为 10 。. head. 此结点已插入链表. 待插入结点. 参考程序. // 结构 7.c #include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量

Télécharger la présentation

第十二讲 链表插入结点

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第十二讲 链表插入结点

  2. 链表插入结点 • 原则: • 1、插入操作不应破坏原链接关系 • 2、插入的结点应该在它该在的位置。应该有一个插入位置的查找子过程。

  3. 先看下面一个简单的例子:已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为10。先看下面一个简单的例子:已有一个如图所示的链表。它是按结点中的整数域从小到大排序的。现在要插入一个结点,该节点中的数为10。 head 此结点已插入链表 待插入结点

  4. 参考程序 // 结构7.c #include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量 #define LEN sizeof(struct numST) // 定义常量,表示结构长度 struct numST // 结构声明 { int num; // 整型数 struct numST *next; // numST结构指针 };

  5. // 被调用函数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; // 返回 }

  6. // 第三种情况,循环查找正确位置 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; } }

  7. // 被调用函数,形参为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; // 取链表中相邻的下一个结点 } // 循环体结束 }

  8. 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函数,输出链表内容 } // 主函数结束

  9. 先看主函数 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结构的内存空间如上图所示。

  10. 下面用赋值语句往这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所指向。

  11. 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作为实参,属于传址调用,而非传值调用。

  12. 这里要讲传址调用和传值调用的区别 (1)如果是传值调用主程序中的调用语句为 insert(head,p);被调用函数为void insert(struct munST *phead, struct numST*p); 实际参数 head p p phead 形式参数

  13. 当着实际参数head赋给了形式参数phead之后,phead就指向了已经存在了的链表,见下图。当着实际参数head赋给了形式参数phead之后,phead就指向了已经存在了的链表,见下图。 head phead 这时原来的主程序中的头指针就不再起作用了。而是phead起作用。假如现在p中的结点数据为4小于5,应该将p插入到phead所指向的结点前,如下图

  14. head phead phead 被调用函数无法改变head,这时head不再是头结点的指针了。如果被调用函数返回head,主函数只能表示head为头指针的三个结点,新插入的结点并没有包含进去。要想将新的插入到最前面的结点包含进去,就必须用传址调用。

  15. (2)如果是传址调用主程序中的调用语句为 insert(&head,p); 被调用函数为 void insert(struct munST **phead, struct numST*p);先看struct numST **phead 是说定义(*phead)为指向numST结构的指针。*phead是指向numST结构的指针,phead又是指向*phead这个指针的指针。phead=&head

  16. &head head phead 主程序中的实参为链表头指针head所在的地址值,传给被调用函数的phead的指针变量中,起到了让phead也指向head的目的。

  17. 在主函数中head为头指针,在被调用的子函数中phead为头指针的地址,head和*phead是同一个单元,只不过分别叫不同的名罢了。当然在子函数中无论插入什么结点都会让*phead指向链表的头。自然返回到主函数后,head也会是指向同一链表的头。 在主函数中head为头指针,在被调用的子函数中phead为头指针的地址,head和*phead是同一个单元,只不过分别叫不同的名罢了。当然在子函数中无论插入什么结点都会让*phead指向链表的头。自然返回到主函数后,head也会是指向同一链表的头。 从这个例子中读者可以领回到传值调用与传址调用的区别。 5、这样在子函数做插入结点的过程中,头指针的改变也能反映到主函数中来。调用print函数,从head开始输出整个链表的内容。

  18. 下面我们来研究insert函数 前提是主程序已将两个实参传给了insert函数的两个形参,这时*phead=head,p所指向的就是待插入的一个结点。事先定义两个结构指针q和r。 第一种情况: *phead==null,说明主程序传过来的头指针为空,即链表为空,一个结点都不存在。这时待插入的p结点就是链表中的第一个结点。只要执行如下两条语句即可 *phead=p; // 将表头指针指向p结点 return; // 返回主程序 在主程序中必然头指针head指向p结点。

  19. 第二种情况: p结点的num值小于链表头结点的num值,即(*phead)->num>p->num。这时要将p结点插入到头结点的前面,要执行如下三条语句 p->next=*phead; // 在p结点的指针域赋以头结点的地址值 *phead=p; // 将头结点指针phead指向p结点 return; // 返回主程序 这种情况如下图 *phead *phead null p

  20. 第三种情况: 前两种情况,无论遇到哪一种,都会返回主程序。只要不返回就是这第三种情况,即p结点的num大于头指针所指向的结点的num值。这时肯定地说p结点要插入到头结点之后,究竟要插到哪里需要找到应该插入的位置。我们设指针r和指针q,分别指向相邻的两个结点,r在前q在后,当着满足 r->num < p->num < q->num 时,p就插在r与q之间。我们看图 r q r q head p null

  21. 一开始让 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;

  22. 执行(2) 在q!=null的情况下,如果p->num<=q->num了,说明这时找到了正确的插入位置,退出while循环,将p结点插入到r后,q前即可。使用的语句为 r->next=p; p->next=q; 在下面我们画出该算法的结构框图

  23. 作业 1、按下表顺序输入某班的一个学习小组的成员表 希望你将学习小组形成一个链表,每人一个结点。结点中有四个成员:姓名、出生年、出生月、指针。在链表中生日大者在前,小者在后。 建成链表后输出该链表。

  24. 2、一年后钱亮同学调至其它学习小组,希望你编程从原链表中删除钱亮所在结点,之后输出该链表。2、一年后钱亮同学调至其它学习小组,希望你编程从原链表中删除钱亮所在结点,之后输出该链表。 提示:原链表如下: head 查找待删除的结点的位置,要从链头找起。 (1)如果是链头结点,即有head->name==待删者name这时只要做head=head->next;即可 (2)如果不是链头结点,要设两个指针r和q,初始时让r=head; q=head->next;

  25. (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; 意味着将孙参所在结点指向钱亮的指针,不再指向钱亮,而指向武陆

  26. 二 叉 树

  27. 链表结构是利用结构中的指针域将每个结点链接起来,形似链条,属于线性结构数据。链表结构是利用结构中的指针域将每个结点链接起来,形似链条,属于线性结构数据。 下面介绍一种非线性结构的东西,二叉树。 先看下例: root 结点4 结点6 结点2 结点7 结点3 结点5 结点1

  28. 在图中 • (1)外形象一棵倒立的树 • (2)最上层有一个“根结点”,指针root指向根结点。 • (3)每个结点都是一个结构,一个成员是整型数据,两个成员是指针,分为左指针L和右指针R。 • (4)根结点的左指针指向左子树;右指针指向右子树。 • (5)左子树或右子树本身又是一棵二叉树,又有它们自己的左子树和右子树,…… 这是递归定义的,因此,在处理时常可用递归算法。

  29. 二叉树的遍历 树的遍历是指访遍树中的所有结点。 对比看遍历一个单链表,从表头开始按一个方向从头到尾就可遍历所有结点。对二叉树来说就没有这样简单了,因为对树或是对子树都存在根(或子树的根)和左子树、右子树,先遍历谁?由之产生了三种不同的方法:

  30. 1、前序法: 1.1 先访问根结点; 1.2 遍历左子树; 1.3 遍历右子树; 2、中序法: 2.1 遍历左子树; 2.2 访问根; 2.3 遍历右子树; 3、后序法 3.1 遍历左子树; 3.2 遍历右子树; 3.3 访问根;

  31. 我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针p指向二叉树的根结点我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针p指向二叉树的根结点 • 定义树的结构 • struct TREE • { • int data; • struct TREE *L, *R; • }; • 定义p为TREE结构的指针 • struct TREE *p; • 让LNR(P)为对以p为根的树作中序遍历的子函数。可得出如下图所示的递归算法与或结点图

  32. 该图说明如下: • 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)

  33. 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

  34. 什么都不做 访问结点1:输出3

  35. 二叉树的建立 建立二叉树的过程是一个“插入”过程,下面我们用一个例子来讲解这一过程。 我们想建立这样一棵二叉树,树中的每一个结点有一个整数数据名为data,有两个指针:左指针L,右指针R,分别指向这个结点的左子树和右子树,显然可以用如下名为TREE的结构来描述这种结点: struct TREE { int data; struct TREE *L, *R; }

  36. 对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图1的中序遍历的方式。即如果待插入结点的数据比根结点的数据小,则将其插至左子树,否则插入右子树。对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图1的中序遍历的方式。即如果待插入结点的数据比根结点的数据小,则将其插至左子树,否则插入右子树。 定义一个递归函数 void insert(struct TREE **proot, struct TREE *p) 其中,指针p指向含有待插入数据的结点。 proot为树的根指针的地址。 insert函数棵理解为将p结点插入到*proot所指向的树中。

  37. insert(proot, p)可用下列与或结点图来描述

  38. 注意在上图中proot是被调用函数的形参。从前面对它的定义看,proot是指针的指针,实际上是指向二叉树根结点的指针的指针,或者说是指向二叉树根结点的指针的地址。如下图。因此,在主程序调用insert函数时,注意在上图中proot是被调用函数的形参。从前面对它的定义看,proot是指针的指针,实际上是指向二叉树根结点的指针的指针,或者说是指向二叉树根结点的指针的地址。如下图。因此,在主程序调用insert函数时, 实参 根结点 &root proot 实参为 &root,p 形参为 proot, p 下面是建立二叉树的参考程序。

  39. #include <stdio.h> // 预编译命令 #include <malloc.h> // 内存空间分配 #define null 0 // 定义空指针常量 #define LEN sizeof(struct TREE) // 定义常量,表示结构长度 struct TREE // 结构声明 { int data; // 整型数 struct TREE *L,*R; // TREE结构指针 };

  40. // 被调用函数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); // 插入右子树 } } // 函数体结束

  41. // 被调用函数,形参为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); // 输入待插入结点数据

  42. 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函数,输出二叉树内容 } // 主函数结束

  43. 结 束

More Related