目录
11.7用指针处理链表
链表概述
链表是一种常见的数据结构。它是动态地进行存储分配的一种结构。可以根据需要开辟内存。
链表有一个“头指针”变量,它存放一个地址,该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应该包括两个部分:用户需要的实际数据和下一个结点的地址。头指针会指向第一个元素,第一个元素又会指向第二个元素... ...直到最后一个元素,它称为“表尾”,它的地址部分存放一个“NULL”(表示空地址),链表到此结束。
链表中各元素在内存中可以不是连续存放的。要找到某一元素,必须先找到上一个元素,根据它提供的下一个元素的地址才能找到下一个元素。如果不提供“头指针”(head),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。
所以,链表的这种数据结构,必须利用指针变量才能实现,即一个结点中应包含一个指针变量,用它放下一个结点的地址。
前面介绍了结构体变量,用它做链表中的结点是最合适的。一个结构体变量包含若干个成员,这些成员可以是数值类型、字符类型、数组类型。我们用这个指针类型存放下一个结点的地址。例如,可以设计这样一个结构体类型:
struct student{
int num;
float score;
struct student *next;
}
其中成员num和score用来存放结点中的有用的数据(用户需要用到的数据)。next是指针类型的成员,它指向struct、student类型数据(这就是next所在的结构体类型)。
一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据。
现在,next是struct student类型中的一个成员,它又指向struct student类型的数据。用这种方法可以建立链表。
上图中,每一个结点都属于struct student类型,它的成员next存放下一个结点的地址,程序设计人员可以不必知道具体各地址结点的地址,只要保证将下一个结点的地址放到前一个结点的成员next中即可。
注意,上面只是定义了一个struct student类型,但未实际分配存储空间。只有定义了变量才分配内存单元。
简单链表
这里通过一个例子来说明如何建立和输出一个简单的链表。
建立一个上图所示的简单链表,它由3个学生数据的结点组成。输出各结点中的数据。
#include<stdio.h>
#define NULL 0
struct student{
long num;
float score;
struct student *next;
};
int main()
{
struct student a,b,c,*head,*p;
a.num = 10101; //对结点num和score赋值
a.score = 89.5;
b.num = 10103;
b.score = 90;
c.num = 10107;
c.score = 85;
head = &a; //将结点a的起始地址赋给头指针head
a.next = &b; //将结点b的起始地址赋给a节点的next成员
b.next = &c; //将结点c的起始地址赋给b节点的next成员
c.next = NULL; //c结点的next成员不存放其他结点的地址
p = head; //使p也指向a结点
do{
printf("%ld %.2lf\n",p->num,p->score); //输出p指向结点的数据
p = p->next; //使p指向下一个结点
} while(p!=NULL); //输出完c结点后p的值为NULL,循环结束
return 0;
}
//输出:
10101 89.50
10103 90.00
10107 85.00
看完这个代码,我们可以思考一下这几个问题:
-
各个结点是怎样构成链表的?
-
没有头指针head行不行?
-
p起什么作用?没有它行不行?
开始时使head指向a结点,a.next指向b结点,b.next指向c结点,这就构成链表关系。“c.next=NULL”的作用是使c.next不指向任何有用的存储单元。在输出链表时要借助p,先使p指向a结点,然后输出a结点的数据,“p = p->next”是为输出下一个结点做准备。p->next的值是b结点的地址,因此执行“p = p->next”后p就指向了b结点,所以在下一次循环时输出的就是b结点中的数据。
本题是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。
处理动态链表所需的函数
前面说过,链表结构是动态分配的,即在需要的时候才开辟一个结点的存储单元。怎样动态地开辟和释放内存呢?C语言编译系统的库函数提供了以下有关函数。
malloc函数
其函数原型为:
void *malloc(unsigned int size);
其作用是在内存的动态存储区分配一个长度为size的连续空间。此函数的值(即“返回值”)是一个分配域的起始地址(类型为void)。如果此函数未能成功地执行(例如内存空间不足),则返回空指针(NULL)。
calloc函数
其函数原型为;
void *calloc(unsigned n,unsigned size);
其作用是在内存的动态存储区中分配n个长度为size的连续空间。函数返回一个指向分配域起始位置的指针;如果分配不成功,返回NULL。
用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。
free函数
其函数原型为:
void free(void *p);
其作用是释放由p指向的动态存储区,使这部分内存区能被其他变量使用。p是最近一次调用calloc或malloc函数时返回的值。free无返回值。
以前的C版本提供的malloc和calloc函数得到的是指向字符型数据的指针。ANSIC提供的malloc和calloc函数规定为void*类型。
建立动态链表
所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各节点数据,并建立起前后相连的关系。
写一函数建立一个有3名学生数据的单向动态链表。
#include<stdio.h>
#include <stdlib.h> // 包含 malloc 和 NULL 的标准库
//#include<malloc.h>
#define NULL 0
#define LEN sizeof(struct student)
struct student {
long num;
float score;
struct student *next;
};
int n; //n为全局变量,本文件模块中各函数均可调用它
//creat函数,是指针类型,即函数带回一个指针值,她指向一个 struct student 类型数据。
//实际上 creat函数带回一个链表的起始地址
struct student *creat(void) { //括号内写void 表示本函数没有形参,不需要进行数据传递
struct student *head;
struct student *p1,*p2;
n = 0;
p2 = (struct student*)malloc(LEN); //开辟一个新的单元
p1 = p2;
scanf("%ld %f",&p1->num,&p1->score);
head = NULL;
while(p1->num!=0) {
n = n+1;
if(n==1) head=p1;
else
{
p2->next=p1;
}
p2 = p1;
p1 = (struct student*)malloc(LEN);
scanf("%ld %f",&p1->num,&p1->score);
}
p2->next=NULL;
return(head); //head是已经定义的指针变量,指向struct student类型数据。
//因此 函数返回的是head的值,也就是链表中第一个结点的起始地址
}
部分代码解释:
p1 = p2 = (struct student*)malloc(LEN);
开辟一个长度为LEN的内存区,malloc带回的是不指向任何数据类型的指针(void *类型) 。而p1、p2是指向struct student类型数据的指针变量,因此必须使用强制转换的方法使指针的基类型变为struct student类型,在malloc(LEN)之前加了“( *struct student)”它的作用是使malloc返回的指针转换为指向struct student类型数据的指针。注意“ * ”不可以省略,否则就转换成struct student类型了,而不是指针类型了。
这个算法的思路是,让p1指向新开辟的结点,p2指向链表中的最后一个结点,把p1所指的结点连接在p2所指的结点后面 用“p2->next=p1"来实现。
输出链表
将链表中各结点的数据依次输出,这个问题比较容易处理。
编写一个输出链表的函数print。
void print(struct student *head)
{
struct student *p;
printf("\nNow,These %d records are :\n",n);
p = head;
if(head != NULL)
{
do{
printf("%ld %.2lf\n",p->num,p->score); //输出p指向结点的数据
p = p->next; //使p指向下一个结点
} while(p!=NULL); //输出完c结点后p的值为NULL,循环结束
}
}
head的值由实参传过来,也就是将已有的链表的头指针传给被调用的函数,在print函数中从head所指的第一个结点出发顺序输出各个结点。
对链表的删除操作
已有一个链表,希望删除其中某个结点。怎样考虑此问题的算法呢?
如图,从动态链表中删去一个结点,并不是真正从内存中抹掉,而是把它从链表里分离出来,只要撤销原来的链接操作即可。
写出一函数以删除动态链表中指定的结点。
以指定的学号作为删除结点的标志。例如:输入10103表示要求删除学号为10103的结点。
解题思路:从p指向第一个结点开始,检查该节点中的num值是否等于输入的要求删除的那个学号。如果相等就将该节点删除,如不相等,就将p后移一个结点,再如此下去,直到遇到表尾为止。
注意考虑,链表是空表(无结点)和链表中找不到要删除的结点的情况。
struct student *del (struct student *head,long num)
{
struct student *p1,*p2;
if(head==NULL)
{
printf("\nList null!\n");
return head;
}
p1 = head;
while(num!=p1->num&&p1->next!=NULL) //p1指向的不是所要找的结点,并且后面还有结点
{
p2 = p1;
p1 = p1->next; //p1后移了一个结点
}
if(num==p1->num) //找到了
{
if(p1==head) head = p1->next; //若p1指向的是首结点,把第二个结点地址赋予head
else
{
p2->next=p1->next; //否则 将下一结点地址赋值给前一结点地址
printf("delete:%ld\n",num);
n = n-1;
}
}
else
{
printf("%ld not been found!\n",num); //找不到该结点
}
return head;
}
函数的类型是指向struct student类型数据的指针,它的值是链表的头指针。函数参数为head和要删除的学号num。head的值可能在函数执行过程中被改变(当删除第一个结点时)。
对链表的插入操作
对链表的插入是指将一个结点插入到一个已有的链表中。
需要考虑的两个问题:
-
怎样找到插入的位置?
-
怎样实现插入?
如果有一群小学生,按身高排序(由低到高)手拉手排好队。现在来了一名新学生,要求按身高顺序插入到队伍中。首先要确定插到什么位置。可以将新同学与队中第1名小学生比身高,若新学生比第1名学生高,就使新同学后移一个位置,与第2名学生比,如果仍比第2名学生高,再往后移,与第3名学生比... ...直到出现比第i名学生高,比i+1名学生低的情况为止。显然,新同学的位置应该在第i名学生之后,在第i+1名学生之前。在确定了位置之后,让第i名学生与第i+1名学生的手脱开,然后让第i名学生去拉新同学的手 ,让新同学另外一只手去拉第i+1名学生的手。这样就完成了插入,形成了新的队列。
struct student *insert(struct student *head,struct student *stud)
{
struct student *p0,*p1,*p2;
p1 = head; //使p1指向第一个结点
p0 = stud; //p0指向要插入的结点
if(head == NULL) //如果原来的链表是空表
{
head = p0; //使p0指向的结点作为头结点
p0->next=NULL;
}
else
{
while((p0->num>p1->num)&&(p1->next!=NULL))
{
p2 = p1; //使p2指向刚才p1指向的结点
p1 = p1->next; //p1后移一个结点
}
if(p0->num<=p1->num)
{
if(head==p1) head = p0; //插入到原来第一个结点之前
else p2->next = p0; //插入到p2指向的结点之后
p0->next = p1;
}
else //插入到最后的结点之后
{
p1->next=p0;
p0->next=NULL;
}
n = n+1; //结点数加1
return head;
}
}
函数参数时head和stud。stud也是一个指针变量,从实参传来待插入结点的地址给stud。语句“head = p0;”的作用是使p0指向待插入的结点。函数类型是指针类型,函数值是链表起始地址head。
对链表的综合操作
将以上建立、输出、删除和插入的函数组织在以后C程序中,用main函数作为主调函数。则有:
int main()
{
struct student *head,stu;
long del_num;
printf("input records:\n");
head = creat(); //建立链表 返回头指针
print(head); //输出全部结点
printf("\ninput the delete number:");
scanf("%ld",&del_num); //输入要删除的学号
head = del(head,del_num); //删除后链表的头地址
print(head); //输出全部结点
printf("\ninput the inserted record:");
scanf("%ld %f",&stu.num,&stu.score);
head = insert(head,&stu); //插入一个结点 返回头结点地址
print(head); //输出全部结点
return 0;
}
上述代码只删除一个结点,插入一个结点。但如果想再插入一个结点,重复写上程序最后4行,共插入2个结点,结果确是错误的。
int main()
{
struct student *head,stu;
long del_num;
printf("input records:\n");
head = creat(); //建立链表 返回头指针
print(head); //输出全部结点
printf("\ninput the delete number:");
scanf("%ld",&del_num); //输入要删除的学号
head = del(head,del_num); //删除后链表的头地址
print(head); //输出全部结点
printf("\ninput the inserted record1:");
scanf("%ld %f",&stu.num,&stu.score);
head = insert(head,&stu); //插入一个结点 返回头结点地址
print(head); //输出全部结点
printf("\ninput the inserted record2:");
scanf("%ld %f",&stu.num,&stu.score);
head = insert(head,&stu); //插入一个结点 返回头结点地址
print(head); //输出全部结点
return 0;
}
(输出时,插入的数据会无限输出)
出现这个结果的原因是:stu是一个固定地址的结构体变量。第一次把stu结点插入到链表中,第二次若再用它来插入第二个结点,就把第一次结点的数据冲掉了,实际上并没有开辟两个结点。我们可以根据insert函数画出此时链表的情况。为了解决这个问题,必须在每插入一个结点时新开辟一个内存区。我们修改main函数,使之能删除多个结点(直到输入要删的学号为0),能插入多个结点(直到输入要删的学号为0)。
int main()
{
struct student *head,*stu;
long del_num;
printf("input records:\n");
head = creat(); //建立链表 返回头指针
print(head); //输出全部结点
// printf("\ninput the delete number:");
// scanf("%ld",&del_num);
// while(del_num!=0)
// {
// head = del(head,del_num); //删除后链表的头地址
// print(head); //输出全部结点
// printf("\ninput the delete number:");
// scanf("%ld",&del_num); //输入要删除的学号
// }
printf("\ninput the inserted record:");
stu = (struct student *)malloc(LEN);
scanf("%ld %f",&stu->num,&stu->score);
while(stu->num!=0)
{
head = insert(head,stu); //插入一个结点 返回头结点地址
print(head); //输出全部结点
printf("\ninput the inserted record:");
stu = (struct student *)malloc(LEN);
scanf("%ld %f",&stu->num,&stu->score);
}
return 0;
}
stu定义为指针变量,在需要插入时先用malloc函数开辟一个内存区,将其起始地址经强制类型转换后赋给stu,然后输入此结构体变量中各成员的值。对不同的插入对象,stu的值是不同的,每次指向一个新的struct student变量。在调用insert函数时,实参为head和stu,将已建立的链表起始地址传给insert函数的形参,将stu(即新开辟的单元地址)传给形参stud,返回的函数值是经过插入之后的链表的头指针(地址)。
在这里,最后一个多次删除插入的代码,我这边同时运行,多次删除操作可以实现,但是到了多次插入就出了点问题,大家可以自行研究一下。
声明:本文章为个人学习笔记,资料整理参考谭浩强《C程序设计(第三版)》如有错误,欢迎大家指正