发布时间:2023-10-03 09:00
第10章 结构、联合、枚举
计算机科学中的任何问题都可以通过引入另一个间接层来解决。
Any problem in computer science can be solved withanother level of indirection.
——大卫·韦勒(David Wheeler),图灵奖得主
学习目标:
• 掌握结构体类型的定义方法,结构体变量的定义、访问和使用
• 理解联合类型的定义方法、联合变量的定义和访问方式
• 理解枚举类型的定义方法、枚举变量的定义和访问方式
• 了解单链表的递归定义及基本操作,如建立、遍历、插入、删除等
除了基本的数据类型外,C语言还允许用户根据实际需要自定义数据类型,以表示复杂的数据对象。本章将介绍如何利用已有的基本数据类型构造新的复合或构造数据类型,包括结构体、联合体、共同体和链表。重点阐述构造数据类型的定义、变量定义和使用,以及与指针、数组的组合使用。
10.1 结构体
本节要点:
• 结构体类型的定义
• 结构体类型变量的定义、赋值和访问
• 结构体指针和数组的使用
编程时,可利用基本数据类型描述单个数据对象,利用数组类型描述多个同一类型的数据。但当所描述的对象同时包含多个属性和特征,可能同时涉及多种数据类型时,应如何描述呢?如一个学生对象,可能同时包含姓名、学号、性别和成绩等多方面的信息。此时,可能会考虑利用多个基本数据类型表示该数据对象,如可用字符数组表示姓名,整型变量表示学号,字符型变量表示性别(\'F’为女,\'M’为男),实型变量表示成绩。那如何同时表示多个学生对象呢?基于已学知识,一种容易想到的方式是使用数组。假设一共有10个学生,则使用如下数组进行刻划:
int ID[10]; / * 学号数组 * /char Name[10][20]; / * 姓名数组,每行对应一位学生姓名 * /char Sex[10]; / * 性别数组 * /double Score[10]; / * 成绩数组 * /但是这种方式的问题在于:将每个学生对象的信息分开表示和存储,类似于将机器的每个零件拆开后分开保存,缺乏信息描述的完整性。为此,C语言提供了结构体(Structure)类型,将每个对象作为一个整体进行描述。
/*li10_01.c:结构体变量示例*/
#include
#include
struct Date
{
int year;
int month;
int day;
};
typedef struct Date Date;
struct Student
{
int ID;
char name[20];
Date birthday;
char sex;
double score;
};
typedef struct Student Student;
int main()
{
Student s1={1001,\"Zhu\",{1991,3,12),\'F\',78};
Student s2,s3,s4;
scnaf(\"%d%s%d%d%d%c%lf\",&s2.ID,s2.name,&s2.birthday.year,&s2.birthday.month,&s2.birthday.day,&s2.sex,&s2.score);
s3=s1;
s4.ID=1004;
strcpy(s4.name,\"Liu\");
s4.birthday.year=1992;
s4.birthday.month=7;
s4.birthday.day=5;
s4.sex=\'F\';
s4.score=80;
printf(\"%d %s %d.%d.%d %c %lf\\n\",s1.ID,s1.name,s1.birthday.year,s1.birthday.month,s1.birthday.day,s1.sex,s1.score);
printf(\"%d %s %d.%d.%d %c %lf\\n\",s2.ID,s2.name,s2.birthday.year,s2.birthday.month,s2.birthday.day,s2.sex,s2.score);
printf(\"%d %s %d.%d.%d %c %lf\\n\",s3.ID,s3.name,s3.birthday.year,s3.birthday.month,s3.birthday.day,s3.sex,s3.score);
printf(\"%d %s %d.%d.%d %c %lf\\n\",s4.ID,s4.name,s4.birthday.year,s4.birthday.month,s4.birthday.day,s4.sex,s4.score);
return 0;
}
10.1.3 结构体指针
结构体指针是指基类型为结构体的指针,即可指向结构体类型变量的指针,
其定义方式如下。
语法:结构体类型名 * 指针变量名;
示例:
struct Student *p;
Data *q;
定义结构体指针后,可用已存在的结构体变量的地址对其赋值,例如:
q=&day1;
【例10.2】结构体指针定义及其使用。
/*li10_02.c:结构体指针示例*/
#include
#include
struct Date
{
int year;
int month;
int day;
};
typedef struct Date Date;
struct Student
{
int ID;
char name[20];
Date birthday;
char sex;
};
typedef struct Student Student;
int main()
{
Student s1,*p;
p=&s1;
s1.ID=2001;
strcpy(p->name,\"Liang\");
p->birthday.year=1978;
p->birthday.month=4;
p->birthday.day=20;
p->sex=\'M\';
p->score=100;
printf(\"%d %s %d.%d.%d %c %.2f\\n\",p->ID,p->name,p->birthday.year,p->birthday.month,p->birthday.day,(*p).score);
return 0;
}
说明运算符“->”前面只能是结构体指针,运算符“.”前面只能是结构体变量。
本例中,p是结构体指针,s1是结构体变量,birthday成员是结构体变量。
因此,访问year成员时,可以用 p->birthday.year 或 s1.birthday.year 或(*p).birthday.year,
但不可以用p->birthday->year或s1->birthday.year的形式。
10.1.4 结构体数组结构体数组是指数组元素的类型为结构体类型的数组。一维结构体数组的定义方式如下。语法:结构体类型名 数组名[常量表达式];示例:struct Student st[10];/ * 定义10个元素的学生结构体数组 * /Data da[10]; / * 定义10个元素的日期结构体数组 * /
【例10.3】结构体数组定义及其使用。
/*li10_03.c:结构体数组示例*/
#include
#include
struct Date
{
int year;
int month;
int day;
};
typedef struct Date Date;
struct Student
{
int ID;
char name[20];
Date birthday;
char sex;
double score;
};
typedef struct Student Student;
int main()
{
Student st[3]={{1001,\"Zhang\",{19992,5,21},\'F\',83},{1002,\"Wang\",{1993,6,18},\'M\',66}};
int i;
st[2].ID=1003;
strcpy(st[2].name,\"Li\");
st[2].birthday.year=1993;
st[2].birthday.month=7;
st[2].birthday.day=22;
st[2].sex=\'M\',
st[2].score=65;
for(i=0;i<3;i++)
{
printf(\"%d %s %d.%d.%d %c %f\\n\",(st+i)->ID,(st+i)->name,(st+i)->birthday.year,(st+i)->birthday.month,(st+i)->birthday.day,(st+i)->sex,(st+i)->score);
}
return 0;
}
说明
本例中定义了一个含有3个元素的结构体数组st,但只初始化了前2个元素的值。因此,第3个元素的值会被全部置为0。用第1种访问结构体数组成员的方式,对元素的成员一一赋值使其有意义。输出时采用的是第2种访问方式,注意理解结构体变量与结构体指针的区别。
10.1.5 向函数传递结构体
除了定义结构体类型的指针和数组外,还可以将结构体作为函数的参数,
向函数传递结构体信息。传递方式主要分为以下3种。
(1)传递结构体成员。在函数调用过程中,如果只需传递结构体中的部分成员,
则可以直接将该成员作为函数实参。
此时,函数的形参与实参中结构体成员类型相同。
传递方式与普通变量的传递并无区别,属于简单的传值调用。
(2)传递结构体变量。
在函数调用过程中,可将结构体变量作为函数的实参,向函数传递完整的结构体内容。
此时,函数形参是同类型的结构体变量,用于接收实参中各成员的值。
在函数内,可利用“.”运算符访问各结构体成员。传递方式同样属于传值调用。
(3)传递结构体地址。
可将结构体指针或结构体数组的首地址作为函数实参,向函数传递结构体地址。
此时函数形参为同类型的结构体指针,传递方式属于传地址调用。在函数内可利用指针修改实参中结构体成员的值。
【例10.4】向函数传递结构体示例。
/*li10_04.c:函数传递结构体示例*/
#include
#include
struct Student
{
int ID;
char name[20];
double score;
};
typedef struct Student Student;
/ * 函数功能: 更新结构体成员值,传值调用
函数参数: 结构体类型的变量
函数返回值:无返回值
* /
void renew_value1(Student st) / * 传值调用方式 */
{
st.ID=1003;
strcpy(st.name,\"Jean\");
st.score=98;
}
/ * 函数功能:更新结构体成员值,传地址调用
函数参数:结构体类型的指针
函数返回值:无返回值
* /
void renew_value2(Student *pt)
{
pt->ID=1004;
strcpy(pt->name,\"Dell\");
pt->score=98;
}
int main()
{
Student st1={1001,\"Tom\",95},st2={1002,\"Jack\",93};
printf(\"%d %s %f\\n\",st1.ID,st1.name,st1.score);
printf(\"%d %s %f\\n\",st2.ID,st2.name,st2.score);
renew_value1(st1);
renew_value2(&st2);
printf(\"%d %s %f\\n\",st1.ID,st1.name,st1.score);
printf(\"%d %s %f\\n\",st2.ID,st2.name,st2.score);
return 0;
}
运行改程序,输出结果为:
1001 Tom 95.000000
1002 Jack 93.000000
1001 Tom 95.000000
1004 Dell 98.000000
说明
函数renew_value1的形参为结构体类型的变量,因此实参为结构体类型的变量st1。传递方式为传值调用,因此,在函数内修改结构体成员的值,st1中成员值并没有发生改变;函数renew_value2的形参为结构体类型的指针,接收实参中结构体变量的地址&st2。传递方式为传地址调用,因此,在函数内可利用指针修改st2中成员的值。
10.1.6 结构体应用——学生成绩排名
【例10.5】从键盘读入不超过10个学生的信息,每个学生的信息包括学号、姓名、一门课程的成绩,
并按成绩由高到低的顺序输出这些学生的完整信息。
分析:
根据题目描述,每个学生包含3个属性,因此需要定义结构体类型。
同时,由于有不止一个学生的信息,应当用数组来处理。
因此,本程序中需要用结构体数组来表示所有学生的信息。
根据题意,需要提供输入、输出以及按成绩排序这3大主要功能,因此定义3个函数,函数的形式参数是结构体指针,而实参是main函数中定义的结构体数组名。
排序的方法可以利用第6、7章介绍过的冒泡法或选择法,只是在排序过程中具体比较的是学生记录的成绩成员值。
/*li10_05.c:结构体应用示例*/
#include
struct Student
{
int ID;
char name[20];
double score;
};
typedef struct Student Student;
int Input(Student[]);
void Sort(student[],int len);
void Output(const Student[],int len);
int main()
{
Student st[10];
int num;
num=Input(st);
Output(st,num);
Sort(st,num);
Output(st,num);
return 0;
}
int Input(Student s[])
{
int i,n;
do
{
printf(\"Enter the sum of students:\\n\");
scanf(\"%d\",&n);
}while(n<=0||n>10);
for(i=0;ist[index].score)
{
index=i;
}
}
if(index!=k)
{
temp=st[index];
st[index]=st[k];
st[k]=temp;
}
}
}
void Output(const Student s[],int len)
{
int i;
printf(\"学号 姓名 成绩\\n\");
for(i=0;i
说明
① 除main函数外,本例中还定义了输入函数Input、排序函数Sort、输出函数Output,在main中调用相应的函数来完成所需的功能,代码较为清晰。
② 3个自定义函数都有一个结构体“数组”作为形参,由第7章可知,该形参本质上是一个结构体指针,因此“Student s[ ]”也可写为“Student*s”。函数调用时,由主调函数向其传递数组名(实际上是实参数组的首地址),在自定义函数中可以对整个结构体数组进行操作。
③ 注意const在形式参数表中的使用。为了保护对应的实参数组不被修改,输出函数Output中的指针形式参数前加了const限定,而输入函数Input和排序函数Sort本来的任务就是要改变结构体数组中元素的内容,因此不能加const进行限定。
④ 如果每个学生不止1个成绩,假如有5个成绩,则需要将score成员定义为含5个元素的一维数组来存储5个分数。关于这样的结构体类型及变量的使用,请读者参考第12章的综合示例。
*10.2 联合本节要点:
• 联合类型的定义
• 联合类型变量的定义、赋值和访问编程时可能会碰到这样一种情况,需要将多种数据组合在一起,成为一个整体。但在使用时,每次只会使用其中一种数据,即同一时刻只有一种数据起作用。例如,描述学生成绩时,成绩可能是整形、浮点型或字符型(等级制),但一门课程通常只使用一种类型;描述一个人的婚姻状况时,可能未婚、已婚或离异,但同一时刻只存在一种状态。在描述这种情形时,可以将这个数据综合体定义成一个联合(Union),有的文献中也称为共用体。
1.联合类型的定义
联合类型定义的方式与结构体类型定义类似,只是关键字不一样,形式如下。
语法:union 联合名{ 类型1 成员1; 类型2 成员2; …… 类型n 成员n;};
其中,“union”是定义联合的关键字,“联合名”是用户自定义的联合类型的名称。“union联合名”是该联合类型的完整类型名。
可以用 typedef 为联合类型定义别名。各成员的类型可以相同,也可以不同。与结构体定义类似,联合定义语句结尾的分号不能省略。例如定义一种类型,用于课程成绩的存放。各种课程的成绩可能是整型、实型或者字符型,但对一门具体的课程而言,其分数类型是确定的,只有一种。因此,我们可以把成绩定义为联合类型。
union Score / *定义一个联合类型* /
{ int i; double d; char c;}; / *必须以分号结束* /
typedef union Score Score; / *用typedef进行重新命名* /
2.联合类型变量的定义
定义一个联合变量的方法如下。
语法:联合类型名 变量名;示例:Score sc;其中,“Score”为联合类型名,sc为新定义的联合变量。
定义之后,编译器为联合变量sc分配内存空间。sc在内存中的存储示意如图10.2所示。
注意
在内存存储方面,联合变量与结构体变量具有本质的不同。结构体变量中每一个成员都有各自独立的内存空间,其占有的内存至少是所有成员占据内存的总和。而联合变量所有成员共享同一段内存空间,其占有内存是所需内存最大的那个成员的空间。联合变量这种占有内存的方式,决定了它每次只能有一个成员起作用,也就是最后赋值的那个成员。联合类型的这些特征可以概括为“空间共享,后者有效”。
在访问联合变量中的成员时,也使用“.”运算符:
语法:
联合变量名.成员名
由于各成员空间共享,联合变量的使用有一些特殊。联合不能整体赋值和输出,在初始化时也只能初始化第一个成员,例如:
sc.i = 88; / * 合法 * /
sc.d = 78.5; / * 合法 * /
sc = { 88,78.5,\'A\' }; / * 不合法 * /
Score sc = {88} ; / * 合法 * /
Score sc = { 88,78.5,\'A\' }; / * 不合法 * /
此外,联合不能作为函数的参数。
【例10.6】联合类型使用示例。
运行此程序,输出结果为:
sizeof(Score) is 8
sizeof(sc) is 8
sizeof(sc.i) = 4, sizeof(sc.d) = 8, sizeof(sc.c) = 1sc.i = 88, sc.d =-92559592117432998000000000000000000000000000000000000000000000.000000, sc.c = X
sc.i = 0, sc.d = 78.500000, sc.c =
sc.i = 67, sc.d = 78.500000, sc.c = C
说明
① 从本例可以看出,sc的3个成员分别占4 字节、8字节和1字节,但是sc总共的字节数为8字节,等于占用空间最大的double型成员d的字节数。
② 当对其中一个成员赋值后,起作用的成员为当前被赋值的成员。此时,其他成员也可以访问,只是将内存中的0、1序列按当前访问成员的数据类型来解释,有时会出现意想不到的输出结果。此外,联合还可以与结构体联合使用。例如在描述学生信息时,学生分为普通学生和转专业学生,每个学生只能处于一种状态。除姓名、成绩等信息外,如果是普通学生,只需了解学号信息,而转专业学生则需要原学号和原专业信息。可将学生信息结构体定义如下:
struct Transfer{ / *转专业学生信息结构体* /
int ID_transfer; / *学号* /
char *major_orginal; / *原专业* /
};
union Transfer_state{ / *学生状态信息联合* /
int ID_normal; / *普通学生学号* /
struct Transfer tansfer_inf; / *转专业学生学号和原专业*/
}
struct Student{
union Transfer_state ID;
char name[10];
int score;
int if_transfer; / *是否为转专业标记* /
};
结构体struct Student中第1个成员是一个联合类型unionTransfer_state,其中包含两个成员,分别表示普通学生的学号信息和转专业学生的相关信息,每个时刻只有一个成员起作用。而转专业学生的相关信息又由结构体struct Transfer描述,包括原学号和原专业信息。信息结构可由图10.3描述:
与结构体类型类似,联合类型也可以定义其指针、数组,因为实际编程中该类型用得并不多,故在此不做详细介绍,具体的定义与使用方法与结构体类似。
*10.3 枚举
本节要点:
• 枚举类型的定义
• 枚举类型变量的定义、赋值和访问
枚举(Enumeration)即一一列举之意,允许用户自定义一种构造数据类型,该类型具有有限的取值范围,可以逐一列举出来。该类型的使用目的是提高程序的可读性。
1.枚举类型的定义
语法:enum 枚举类型名 {枚举常量1,枚举常量2,…,枚举常量n};
示例:
enum Seasons { Spring, Summer, Autumn, Winter };
其中,“enum”是定义枚举的关键字,“枚举类型名”是用户自定义的枚举类型的名称,“enum枚举类型名”作为完整的枚举类型标识,也可以用typedef来定义类型别名。枚举常量1、枚举常量2……枚举常量n是n个常量,称为枚举元素或枚举常量,表示该类型可取值的范围。定义语句后分号不能省略。
示例中,“enum Seasons”是一个新创建的枚举类型,可能的取值包括Spring、Summer、Autumn和Winter。
C语言中,系统会为每个枚举元素对应一个默认整型值,通常从“0”开始,并顺次加1。因此,Spring、Summer、Autumn和Winter分别对应0、1、2和3。
也可以在枚举类型定义时对元素的值进行指定,方式为:enum Season { Spring=4, Summer=1, Autumn, Winter };这样,枚举元素对应的整数就变为4、1、2、3。对于没有赋值的元素,值依据前面已赋值的枚举常量元素值依次加1。
2.枚举类型变量的定义
语法:
枚举类型名 变量名;
示例:
enum Season day;t
ypedef enum Season Season;/ * 定义别名后再定义变量 * /
Season day;
对于枚举类型变量,赋值时可以赋以枚举类型数据和整型数据,输出时只能以整型方式输出,无法直接输出枚举常量。也可以通过输出与枚举常量写法一样的字符串间接输出枚举值。
枚举类型也可以定义数组、指针。下面的例子演示枚举数组的使用。
【例10.7】枚举类型使用示例。
/*li10_07.c:枚举类型使用示例*/
#include
enum Seasons
{
Spring=4,
Summer=1,
Autumn,
Winter
};
typedef enum Seasons Seasons;
int main()
{
Seasons day[4]={Summer};
int i;
day[1]=2;
day[2]=6;
for(i=0;i<4;i++)
{
printf(\"day[%d]:%d\",i,day[i]);
switch(day[i])
{
case Spring:
printf(\"Spring\\n\");
break;
case Summer:
printf(\"Summer\\n\");
break;
case Autumn:
printf(\"Autumn\\n\");
break;
case Winter:
printf(\"Winter\\n\");
break;
default:
printf(\"Wrong!\\n\");
}
}
return 0;
}
说明① 本例中定义了一个枚举类型数组day,只对第0个元素进行了初始化,后面3个元素都被初始化为0。
② 对枚举变量赋值时,可以赋以枚举类型数据和整型数据。另外,从本例也可以看出,虽然枚举类型取值范围对应的整数只有1~4,但赋值超出该范围时(“day[2] =6;”),系统并不会进行错误检查。
③ 枚举类型变量可以以%d形式输出,其他方式需要编写代码进行转换。
*10.4 链表
本节要点:
• 链表的递归定义
• 链表的基本操作
在编程中,我们常利用数组存储和处理大批量同类型的数据。数组使用起来直观、方便,但也存在以下几点问题。
① 在定义数组时,需要明确数组长度,即数组所占用的内存大小。但在程序运行前,可能无法预知数组的实际长度,因其通常由用户的输入所决定。一种常用的解决方案是使数组的定义长度超过实际需求,但这种做法容易造成空间浪费。
② 数组必须占有一块连续的内存空间。例如“int a[220];”,需占用880字节(220*4Byte)的连续内存空间。如果内存中没有这么大的连续空闲区域,则代码无法运行。如图10.4所示,内存中有3块空闲的区间,分别是600字节、800字节和400字节,虽然总和超过了880字节,但是单块均不超过880字节,所以此时程序无法运行。
③ 数组的插入和删除等操作需要移动大量数组元素,程序运行效率低。针对上述问题,设计人员提出了一种解决方案——链表(LinkedTable)。本节接下来的内容就将对链表进行简单介绍。
10.4.1 链表的概念利用数组处理批量数据时,当程序处理完一个数据,只需将下标加1就可以处理下一个数据,当下标达到数组长度−1时,程序就会知道所有数据都已处理完了。现在如果使用链表来存储这批数据,这些数据有可能散布在内存的不同地方。那么程序在处理时,如何找到下一个数据?又如何判断所有数据是否都已处理完?要回答这两个问题,需要从链表的组织方式入手来寻找答案。链表的基本构成单位是结点。一个结点又由两部分组成:数据部分和指针部分,分别称为数据域和指针域。如图10.5所示,数据域用于存放待处理的数据,指针域则存放下一个数据的地址。一个链表就是由这样一系列的结点所“链接”而成。图10.6给出了一个链表的例子,这个链表包含1个指针head和3个结点。head里面存放了链表第1个结点的地址,也称为首地址,其实质是告诉程序这批数据的第1个数据在哪里。head指针因此被称为头指针,这是链表中最重要的指针,是整个链表的入口,据此才能找到第1个结点,从而才能依次找到后面那些结点。3个结点中分别存放了3、4、6这3个数据。第1个结点中存放了数据3,并在
指针部分存放了第2个结点的地址。同理,第2个结点除了存放数据4外,也在指针部分存放了下一个结点的地址。第3个结点存放了数据6,因为它是最后一个结点,因此指针部分为空(NULL),表明它是最后一个数据。
下面讨论链表的代码实现。首先来看结点的实现。从图10.5可以看出,一个结点至少包含两方面的内容:数据信息和指针信息,且数据类型不同。因此我们可以将其定义为一个结构体:
struct Node / * 结点的结构体类型定义 * /
{ int data; / * 结点的数据部分 * /
struct Node *next; / * 结点的指针部分,这里的*一定不能丢失 * /
};
在上述定义中,data成员用于存放数据(此处假定存放的是int型数据,当然也可以根据实际需要修改),next成员用于存放下一个结点的起始地址,其类型是struct Node *。
【例10.8】完成图10.6所示的链表的定义,并且输出链表中各结点的数据域的值。
/*li10_08.c:链表定义示例*/
#include
struct Node
{
int data;
struct Node *next;
};
int main()
{
struct Node n1,n2,n3,*head,*p;
head=&n1;
n1.data=3;
n1.next=&n2;
n2.data=4;
n2.next=&3;
n3.data=6;
n3.next=0;
p=head;
while(p!=\'\\0\')
{
printf(\"%d \",p->data);
p=p->next;
}
printf(\"\\n\");
return 0;
}
运行此程序,输出结果为:
3 4 6
说明
① 本例主要展示了两个功能:建立链表和打印链表。
② 本例是链表建立的一个简单演示,只包含3个结点。在实际中,有可能要处理大批量的数据,并且数据的数量也可能在运行时动态地变化,因此无法采取本例中的方式,即事先为每个数据定义一个结构体变量作为结点,再将它们链接起来。而是在需要存储数据的时候向系统动态申请内存。每增加一个数据,程序就申请一个结点大小的内存空间,将数据存放进去,并将其添至链表中。下一节中将有详细说明。
③ 依次访问链表的各个结点称为链表的遍历。链表的构成方法决定了不可能随机访问任意一个结点,因此,无论是在链表中查找某一个元素值或是输出一个或多个元素值,都需要对链表进行遍历。其基本方法是:定义一个工作指针,假设指针名为 p,首先令其初值等于head,然后用p指针控制循环,当其非空时访问p所指向结点的数据域信息,然后使p指针顺着链后移一位,如此下去,直到p指针为空的时候停止。本例中的while循环展示了这一方法,这在后面每一个示例中都需要用到,不再赘述。
④ 存储链表首地址的head指针极为重要,它是整个链表的入口,我们对链表进行的绝大部分操作都是从 head 指针开始入手的,并且在操作的过程中也要时刻注意 head指针的维护。
10.4.2 链表的基本操作
链表的基本操作包括建立(批量存入数据)、打印(输出所有数据)、删除(在批量数据中删除指定数据)、插入(在批量数据中添加一个数据)等。本节将介绍这些基本操作的实现。
1.链表的建立
1.链表的建立链表的建立就是指从一个空链表开始,一个一个地添加新结点,直至所有的数据都加入该链表。上一小节讲过,链表真正使用时,结点是动态产生的。当出现一个需要处理的数据时,首先动态申请一个结点空间,然后对结点的数据域进行赋值,再对结点的指针域进行处理,使该结点链入到整个单链表中。按照新结点加入位置的不同,链表的建立大致有以下几种方法。
① 前插法:新结点每次都插入到链表的最开头,作为新链表的第一个结点;
② 尾插法:新结点每次都插入到链表的最后面,作为新链表的最后一个结点;
③ 序插法:新结点插入后保证结点域数据的有序性,该法需经搜索确定插入位置;④ 定位法:新结点插入到链表中指定的位置。本节主要介绍尾插法,其基本思想如下所示。
① 定义两个指针分别指向链表的第一个和最后一个结点,假设这两个指针名字为head 和tail。
② 初始化链表,即“head = tail = NULL;”,让两个工作指针均不指向任何地方。
③ 通过工作指针p申请一个动态结点空间,然后将数据赋值给动态结点的数据域,指针域及时赋值为空。
④ 生成一个新结点p之后, head指针要根据原来链的情况进行不同的处理:如果原来是空链,则heal等于p;如果原来非空,则head不需要做任何处理。然后将p所指向的结点链到tail所指向的结点后面,体现“尾插”思想。最后将p赋值给tail,保证tail始终指向当前链表的最后一个结点处。
⑤ 重复进行③、④两个步骤,直至结束。下面的示例给出了用尾插法建立链表,再对该链表进行遍历输出所有的元素,最后利用遍历思想释放所有结点动态空间的完整过程。
【例10.9】单链表的建立、打印与释放。
/*li10_09.c:单链表shili*/
#include
#include
struct Node
{
int data;
struct Node *next;
};
typedef struct Node Node;
Node *Create();
void Print(Node *head);
void Release(Node *head);
int main()
{
Node *head;
head=Create();
Print(head);
Release(head);
return 0;
}
Node *Create()
{
Node *head,*tail,*p;
int num;
head=tail=NULL;
printf(\"请输入一批数据,以-9999结尾:\\n\");
scanf(\"%d\",&num);
while(num!=-9999)
{
p=(Node*)malloc(sizeof(Node));
p->data=num;
p->next=NULL;
if(NULL=head)
{
head=p;
}
else
{
tail->next=p;
}
tail=p;
scanf(\"%d\",&num);
}
return head;
}
void Print(Node *head)
{
Node *p;
p=head;
if(NULL==head)
{
printf(\"链表为空!\\n\");
}
else
{
printf(\"链表如下\\n\");
while(p!=NULL)
{
printf(\"%d \",p->data);
p=p->next;
}
}
printf(\"\\n\");
}
void Release(Node *head)
{
Node *p1,*p2;
p1=head;
while(p1!=NULL)
{
p2=p1;
p1=p1->next;
free(p2);
}
printf(\"链表释放内存成功!\\n\");
}
运行此程序,屏幕上显示为:请输入一批数据,以-9999结尾:用户从键盘输入为:2 4 8 11 19 -9999 <回车>程序输出结果为:2 4 8 11 19链表释放内存成功!
说明
① 本例除main函数外,还包括Create、Print、Release这3个函数,功能分别是创建链表、打印链表和释放链表。
② 图10.7(a)到图10.7(c)展示了尾插法的完整过程。初始化链表,执行“head = tail = NULL;”,如图10.7(a)所示。往链表中添加第1个结点时,执行“head = p; tail = p;”,如图10.7(b)所示。往链表中添加后续结点时,将当前尾结点的next指向该结点,即“tail->next = p;”,然后再将新结点的地址存入tail指针,“tail = p;”,如图10.7(c)所示。
③ 链表打印(Print函数)的基本思想与例10.8相同,主要是增加了链表是否为空的判断。
④ 链表释放(Release 函数)的作用是,当程序运行结束时,释放建立链表时所申请的内存空间。它的基本思想是:从第一个结点开始,首先保存该结点下一个结点的地址,再将该结点的内存空间释放。重复上述过程,直至链表结尾,仍然是通过遍历思想进行的。
2.从链表中删除数据从链表中删除一个数据时,除了头指针 head 之外,还需有2个工作指针,假设为 p2和p1,分别指向待删除结点的前一个位置以及待删除结点的位置。删除的完整过程有以下几个步骤。① 定位:如果链表不是空链表,则用遍历的思想,在链表中查找待删除数据所在的结点位置。有两种可能性:如果该数据存在,则将存有该数据的结点的位置保存到某一个指针中;如果该数据不存在,则提示用户,并返回。② 脱链:将待删除结点从链中“解”下来,如果该结点是链表中的第一个结点,则让head指针指向其下一个结点即可,即“head = p1->next;”;如果不是第一个结点,则让p2的next域指向待删除结点的下一个结点即可,即“p2->next = p1->next;”,这样就将p1所指向的结点从链表中脱离开来。③ 释放:无论是哪种情况,如果待删除的数据是存在的,最后一定要将p1所指向的结点空间释放掉,完成删除工作。下面通过例10.10来演示一下删除结点的完整过程。
【例10.10】在例10.9的基础上,增加一个函数Delete,实现数据的删除。
Node *Delete(Node *head,int num)
{
Node *p1,*p2;
if(NULL==head)
{
printf(\"链表为空!\\n\");
return head;
}
p1=head;
while(p1->next&&p1->data!=num)
{
p2=p1;
p1=p1->next;
}
if(p1->data=num)
{
if(head==p1)
{
head=p1->next;
}
else
{
p2->next=p1->next;
}
free(p1);
printf(\"删除成功!\\n\");
}
else
{
printf(\"链表中无此数据!\\n\");
}
return head;
}
说明① 以上程序实际运行时还测试了删除1和2的情况,以此检验删除最前面的结点以及中间位置的结点得到的结果是否正确,再加上上面两种测试用例,表明该删除算法是完备和健壮的。② 注意脱链的不同方法:若删除第1个结点时一定要修改head指针,如图10.8(a)所示,执行head=p1->next; ;如果删除的不是第1个结点,如图10.8(b)所示,则p2指针就保存了该结点前一个结点的地址,让p2->next指向待删除结点的下一个结点即可,即“p2->next = p1->next;”。③ 无论删除的是哪一个结点,最后一定要释放指针所指向的结点空间。
3.向链表中插入数据
向链表中插入一个数据,有多种要求。有时是在指定位置进行插入;更多情况下,是要求在有序链表的基础上插入一个元素以保持原来的顺序性。按序插入的方法一般有如下步骤。
① 定位:需要通过遍历的方法逐个扫描结点,将待插入的值与链表中结点的值进行比较,从而确定新结点应该在什么位置进行插入。用指针变量记下这两个位置以方便插入。如果原来是空链表,则不必扫描比较。
② 生成:用一个指针生成一个新结点,将待插入的数据放入结点的数据域中。
③ 插入:将新结点插入在定位获得的两个指针位置之间,如果插入的结点是新的第1个结点,则一定要修改头指针head。插入时需要修改两个指针的值,一是新结点的next域,另一个是head指针(如果插入的结点成为了新的第1个结点)或新结点在链表中前趋结点的next域。
【例10.11】假定链表中的数据是按从小到大的顺序存放的,
现要求在例10.9的基础上,增加一个函数 Insert,实现向链表中插入某一数据的功能,并保持插入后链表的数据依然按照从小到大的次序存放。
Node* Insert(Node *head,int num)
{
Node *p,*p1,*p2;
p={Node*}malloc(size(Node));
p->data=num;
p->next=NULL;
p1=head;
while(p1&&p->data>p1->data)
{
p2=p1;
p1=p1->next;
}
if(p1==head)
{
head=p;
}
else
{
p2->next=p;
}
p->next=p1;
printf(\"数据插入成功!\\n\");
return head;
}
说明① 上例运行只给出了一个示例,读者可以自己再运行一下,观察在链表的中间或是最前面插入一个结点时的输出情况。② 插入时,分两种情况处理:如果原链表为空,或者待插入数据小于第1个结点的值,新结点应为链表的第1个结点,如图10.9(a)所示,执行“head = p;”;否则,插入位置应在p2所指结点与p1所指结点之间,如图10.9(b)所示,执行“p2->next = p;”,这里也包含了p1为空、p2指向链表最后一个结点的特殊情况,如图10.9(c)所示。不管是哪种情况,最后统一执行“p->next = p1;”来修改新结点的指针域。在链表中,有一个元素需要加入时就生成一个结点,不存在空间浪费的问题,也不存在数组中的下标越界的问题,链表的结点个数取决于可以使用的内存空间的多少。链表的建立、遍历、插入、删除、查找(在插入和删除中已有体现)是链表中最基本的操作,无论哪种操作,链表中的头指针都是最重要的信息,是访问整个链表的起点。
10.5 本章小结本章主要讲述了如何定义新的数据类型,包括结构体、联合、枚举以及链表,以描述复杂的数据对象。读者首先需要理解各数据类型的应用场合。在结构体类型中,掌握结构体类型的定义、结构体变量的定义,以及结合指针、数组和函数的相关操作。以学生信息管理为例,介绍结构体类型的具体使用。了解联合和枚举类型,以及变量的定义与使用。掌握利用typedef为类型定义别名。最后,了解链表的递归定义,以及一些相关的基本操作,如链表的建立、插入、删除、查找、遍历等操作。