第十二讲  链表插入结点
Download
1 / 45

第十二讲 链表插入结点 - PowerPoint PPT Presentation


  • 67 Views
  • Uploaded on

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

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about ' 第十二讲 链表插入结点' - quintessa-haley


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.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript

链表插入结点

  • 原则:

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

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


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

(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,再插入链表

(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起作用。假如现在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也会是指向同一链表的头。

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

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

提示:原链表如下:

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为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是右子树的根。

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


    什么都不做我们就以中序法为例研究如何遍历二叉树。仍然采用递归算法。令指针

    访问结点1:输出3


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

    建立二叉树的过程是一个“插入”过程,下面我们用一个例子来讲解这一过程。

    我们想建立这样一棵二叉树,树中的每一个结点有一个整数数据名为data,有两个指针:左指针L,右指针R,分别指向这个结点的左子树和右子树,显然可以用如下名为TREE的结构来描述这种结点:

    struct TREE

    {

    int data;

    struct TREE *L, *R;

    }


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

    定义一个递归函数

    void insert(struct TREE **proot, struct TREE *p)

    其中,指针p指向含有待插入数据的结点。

    proot为树的根指针的地址。

    insert函数棵理解为将p结点插入到*proot所指向的树中。


    insert(proot, p)对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图可用下列与或结点图来描述


    注意在上图中对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图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函数,输出二叉树内容

    } // 主函数结束


    结 束对二叉树最重要的是根,它起定位的作用,因此,首先建立的是根结点。也就是说,如果从键盘输入数据来建立二叉树,第一个数据就是这棵树的根的数据,之后再输入的数据,每一个都要与根中的数据作比较,以便确定该数据所在接点的插入位置。假定我们这里依然用图