第十二讲 链表插入结点

45
1 第第第第 第第第第第第 第第第第 第第第第第第

Upload: quintessa-haley

Post on 31-Dec-2015

47 views

Category:

Documents


0 download

DESCRIPTION

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

TRANSCRIPT

Page 1: 第十二讲  链表插入结点

1

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

Page 2: 第十二讲  链表插入结点

2

链表插入结点

原则:

1 、插入操作不应破坏原链接关系

2 、插入的结点应该在它该在的位置。应该有一个插入位置的查找子过程。

Page 3: 第十二讲  链表插入结点

3

55

head

66

1010

1515

nullnull

121288

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

待插入结点此结点已插入链表

Page 4: 第十二讲  链表插入结点

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结构指针

};

参考程序

Page 5: 第十二讲  链表插入结点

5

// 被调用函数 insert() ,两个形参分别表示链表和待插入的结点void insert (struct numST **phead, struct numST *p){ // 函数体开始

struct numST *q,*r; // 定义结构指针 q,rif ((*phead)==null) // 第一种情况,链表为空{

*phead = p; // 链表头指向 preturn; // 完成插入操作,返回

}else // 链表不为空{

// 第二种情况, p结点 num值小于链表头结点的 num值

if ( (*phead)->num > p->num ){ // 将 p结点插到链表头部 p->next = *phead;// 将 p的 next指针指向链表头

(*phead) *phead = p; // 将链表头赋值为 p return; // 返回}

Page 6: 第十二讲  链表插入结点

6

// 第三种情况,循环查找正确位置r = *phead; // r赋值为链表头q = (*phead)->next; // q赋值为链表的下一个结点while (q!=null) // 利用循环查找正确位置{

// 判断当前结点 num是否小于 p结点的 numif (q->num < p->num){

r = q; // r赋值为 q,即指向 q所指的结点

q = q->next;// q指向链表中相邻的下一个结点

}else // 找到了正确的位置

break; // 退出循环}

// 将 p结点插入正确的位置r->next = p;p->next = q;

}}

Page 7: 第十二讲  链表插入结点

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; // 计数加 1printf("%d %d\n",k,r->num);

r=r->next; // 取链表中相邻的下一个结点} // 循环体结束

}

Page 8: 第十二讲  链表插入结点

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和 15head->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函数,输出链表内容

} // 主函数结束

Page 9: 第十二讲  链表插入结点

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

这 3个 ST 结构的内存空间如上图所示。

head->next

head->next->next

Page 10: 第十二讲  链表插入结点

10

下面用赋值语句往这 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 条之后形成了一条链表如下:

55head

1010

1515

nullnull该链表的头结点由 head 所指向。

Page 11: 第十二讲  链表插入结点

11

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

Page 12: 第十二讲  链表插入结点

12

这里要讲传址调用和传值调用的区别

( 1 )如果是传值调用主程序中的调用语句为 insert(head,p);被调用函数为void insert(struct munST *phead, struct numST*p);

head p实际参数

形式参数pphead

Page 13: 第十二讲  链表插入结点

13

当着实际参数 head 赋给了形式参数 phead 之后, phead 就指向了已经存在了的链表,见下图。

55 1010 1515

nullnullphead

这时原来的主程序中的头指针就不再起作用了。而是phead 起作用。假如现在 p 中的结点数据为 4 小于 5 ,应该将 p 插入到 phead 所指向的结点前,如下图

head

Page 14: 第十二讲  链表插入结点

14

55

head

1010 1515

nullnullphead

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

44

phead

Page 15: 第十二讲  链表插入结点

15

( 2 )如果是传址调用主程序中的调用语句为 insert(&head,p);

被调用函数为void insert(struct munST **phead, struct numST*p);

先看 struct numST **phead

是说定义( *phead )为指向 numST 结构的指针。

*phead 是指向 numST 结构的指针 ,phead 又是指向*phead 这个指针的指针。 phead=&head

Page 16: 第十二讲  链表插入结点

16

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

&head

phead

head

Page 17: 第十二讲  链表插入结点

17

在主函数中 head 为头指针,在被调用的子函数中 phead 为头指针的地址, head 和 *phead 是同一个单元,只不过分别叫不同的名罢了。当然在子函数中无论插入什么结点都会让 *phead 指向链表的头。自然返回到主函数后, head 也会是指向同一链表的头。

从这个例子中读者可以领回到传值调用与传址调用的区别。

5 、这样在子函数做插入结点的过程中,头指针的改变也能反映到主函数中来。调用 print 函数,从 head开始输出整个链表的内容。

Page 18: 第十二讲  链表插入结点

18

下面我们来研究 insert 函数前提是主程序已将两个实参传给了 insert 函数的两个形

参,这时 *phead=head, p 所指向的就是待插入的一个结点。事先定义两个结构指针 q和 r 。

第一种情况:*phead==null ,说明主程序传过来的头指针为空,即链表为空,一个结点都不存在。这时待插入的 p 结点就是链表中的第一个结点。只要执行如下两条语句即可

*phead=p; // 将表头指针指向 p 结点 return; // 返回主程序

在主程序中必然头指针 head 指向 p 结点。

Page 19: 第十二讲  链表插入结点

19

第二种情况:p 结点的 num 值小于链表头结点的 num 值,即 (*phead)->num>p->num 。这时要将 p 结点插入到头结点的前面,要执行如下三条语句

p->next=*phead; // 在 p 结点的指针域赋以头结点的地址值*phead=p; // 将头结点指针 phead 指向 p 结点return; // 返回主程序

这种情况如下图55*phead 1010

44

1515

nullnull

*phead

p null

Page 20: 第十二讲  链表插入结点

20

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

r->num < p->num < q->num时, p 就插在 r与 q 之间。我们看图

55head 1010

1212p

1515

nullnull

qr

null

qr

Page 21: 第十二讲  链表插入结点

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;

Page 22: 第十二讲  链表插入结点

22

执行 (2)在 q!=null 的情况下,如果 p->num<=q->num 了,说明

这时找到了正确的插入位置,退出 while 循环,将 p结点插入到 r 后, q 前即可。使用的语句为

在下面我们画出该算法的结构框图

r->next=p;p->next=q;

Page 23: 第十二讲  链表插入结点

23

(*phead)==null T

F

T F (*phead)->num>p->num

第 2种情况: p结点中的数据小于头结点中的数据。将 p结点插入到表头前,然后返回主函数。即

p->next=*phead; *phead=p; return;

第 3种情况: p 结点中的数据大于等于头结点中的数据。定义两个指针r和 q,让 r指向表头,让 q指向相邻的下一个结点,即

r=*phead; q= (*phead)->next;

进入循环查找该插入的位置。

第 1 种情况:链表为空。让链表 头 *phead指向 p 结点,之后返回主函数。即 *phead=p; return;

T F q->num<p->num

while(q!=null) 当 q指向不空

p 结点中的数据大于 q结点中的数据。让 r与 q同时后移一步 r=q; q=q->next;

p结点中的数据小于等于 q 结点中的数据。找到了正确的插入位置,退出循环。

找到了 p结点正确的插入位置。包括两种情况(1)q指向空,r指向队尾结点;(2)r->num<p->num<=q->num。这两种情况都需将 p插入到 r后 q前,即 r->next=p; p->next=q;

Page 24: 第十二讲  链表插入结点

24

作业1 、按下表顺序输入某班的一个学习小组的成员表

希望你将学习小组形成一个链表,每人一个结点。结点中有四个成员:姓名、出生年、出生月、指针。在链表中生日大者在前,小者在后。建成链表后输出该链表。

姓名姓名 赵达赵达 钱亮钱亮 孙参孙参 李思李思 周芜周芜 武陆武陆 郑琪郑琪

出 年出 年生 月生 月

19831983 19831983 19831983 19821982 19831983 19831983 19821982

11 33 22 99 55 44 66

Page 25: 第十二讲  链表插入结点

25

2 、一年后钱亮同学调至其它学习小组,希望你编程从原链表中删除钱亮所在结点,之后输出该链表。

提示:原链表如下:

李思李思19821982

99head

武陆武陆19831983

44

赵达赵达19831983

11

孙参孙参19831983

22

钱亮钱亮19831983

33

郑琪郑琪19821982

66

周芜周芜19831983

55

nullnull

查找待删除的结点的位置,要从链头找起。(1) 如果是链头结点,即有 head->name== 待删者 name

这时只要做 head=head->next; 即可(2) 如果不是链头结点,要设两个指针 r和 q ,初始时

让r=head; q=head->next;

Page 26: 第十二讲  链表插入结点

26

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

Page 27: 第十二讲  链表插入结点

27

二 叉 树

Page 28: 第十二讲  链表插入结点

28

链表结构是利用结构中的指针域将每个结点链接起来,形似链条,属于线性结构数据。

下面介绍一种非线性结构的东西,二叉树。先看下例:

66

LL RR

99

nullnull nullnull

55

nullnull nullnull

77

nullnull nullnull

33

nullnull nullnull

88

LL RR44

LL RR

结点1

结点2

结点4

结点6

结点7结点

5结点

3

root

Page 29: 第十二讲  链表插入结点

29

在图中

( 1 )外形象一棵倒立的树

( 2 )最上层有一个“根结点”,指针 root 指向根结点。

( 3 )每个结点都是一个结构,一个成员是整型数据,两个成员是指针,分为左指针 L 和右指针 R 。

( 4 )根结点的左指针指向左子树;右指针指向右子树。

( 5 )左子树或右子树本身又是一棵二叉树,又有它们自己的左子树和右子树,……

这是递归定义的,因此,在处理时常可用递归算法。

Page 30: 第十二讲  链表插入结点

30

二叉树的遍历

树的遍历是指访遍树中的所有结点。

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

Page 31: 第十二讲  链表插入结点

31

1 、前序法:1.1 先访问根结点;1.2 遍历左子树;1.3 遍历右子树;

2 、中序法:2.1 遍历左子树;2.2 访问根;2.3 遍历右子树;

3 、后序法3.1 遍历左子树;3.2 遍历右子树;3.3 访问根;

Page 32: 第十二讲  链表插入结点

32

我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针 p 指向二叉树的根结点

定义树的结构struct TREE{

int data;struct TREE *L, *R;

};定义 p为 TREE 结构的指针

struct TREE *p;

让 LNR(P) 为对以 p 为根的树作中序遍历的子函数。可得出如下图所示的递归算法与或结点图

Page 33: 第十二讲  链表插入结点

33

A LNR(p) p中序遍历 结点为根的二叉树

B什么都不做

C

p!=nullp==null

DLNR(p->L)

p中序遍历 的左子树

Ep访问 结点 F

LNR(p->R)p中序遍历 的右子树

2图

Page 34: 第十二讲  链表插入结点

34

该图说明如下:

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)

Page 35: 第十二讲  链表插入结点

35

3 、比较 LNR(p)与 LNR(p->L)及 LNR(p->R) 可以看出,都是同一个函数形式,只不过代入了不同的参数,从层次和隶属关系看, p 是父结点的指针,而p->L和 p->R 是子结点的指针, p->L 是左子树的根,p->R 是右子树的根。

下面请大家做一个练习,依图 2 画“与或”图将图 1所示的二叉树用中序遍历,将所访问到的结点数据输出。如图 3

Page 36: 第十二讲  链表插入结点

36

4访问结点6输出:

LNR(p->L)2指向结点 LNR(p->R)

6指向结点

LNR(p) p=root

p!=null

p->R!=null

nullnullnull

!=null !=null !=null !=null

null

p->L!=null

什么都不做访问结点 1 :

输出 3

Page 37: 第十二讲  链表插入结点

37

二叉树的建立建立二叉树的过程是一个“插入”过程,下面我们用

一个例子来讲解这一过程。我们想建立这样一棵二叉树,树中的每一个结点有一

个整数数据名为 data ,有两个指针:左指针 L ,右指针 R ,分别指向这个结点的左子树和右子树,显然可以用如下名为 TREE 的结构来描述这种结点:

struct TREE{

int data;struct TREE *L, *R;

}

Page 38: 第十二讲  链表插入结点

38

对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图 1 的中序遍历的方式。即如果待插入结点的数据比根结点的数据小,则将其插至左子树,否则插入右子树。

定义一个递归函数void insert(struct TREE **proot, struct TREE *p)

其中,指针 p 指向含有待插入数据的结点。proot 为树的根指针的地址。insert 函数棵理解为将 p 结点插入到 *proot 所

指向的树中。

Page 39: 第十二讲  链表插入结点

39

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

p将结点 插入根结点*proot=p; return;返回

C

根结点不空,树已存在

根结点为空*proot==null

p->data<=(*proot)->data

insert(&((*proot)->R),p);p将 结点插入右子树

3图

p->data> (*proot)->data

insert(&((*proot)->L),p);p将 结点插入左子树

Page 40: 第十二讲  链表插入结点

40

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

根结点

proot

实参

&root

实参为 &root, p形参为 proot, p下面是建立二叉树的参考程序。

Page 41: 第十二讲  链表插入结点

41

#include <stdio.h> // 预编译命令#include <malloc.h> // 内存空间分配#define null 0 // 定义空指针常量#define LEN sizeof(struct TREE) // 定义常量,表示结构长度

struct TREE // 结构声明{

int data; // 整型数struct TREE *L,*R; // TREE 结构指针

};

Page 42: 第十二讲  链表插入结点

42

// 被调用函数 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); // 插入右子树

}} // 函数体结束

Page 43: 第十二讲  链表插入结点

43

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

Page 44: 第十二讲  链表插入结点

44

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函数,输出二叉树内容

} // 主函数结束

Page 45: 第十二讲  链表插入结点

45

结 束