前言
在C语言初期,在写一些具有应用意义的的程序时我们通常会定义一个数组来存放数据,例如统计学生的成绩,制作一个电话簿。数组为我们提供了存放行数据的便利,只需要一个定义就可以存放很多的元素。
可是它也有其不方便的点,就是我们需要在使用前就提前定义好它所分配的空间大小,这会给实际应用带来诸多不便。因为在实际情况下我们往往不能预知某个班有多少位学生,一个电话簿将存多少个人的号码…等等。如果定义的数组过大就会导致空间的浪费,如果小了又不能满足存放数据的需求,所以我们有没有更灵活的方式来开辟空间存储数据呢?
答案是肯定的,在之前我们学习了动态储存管理的函数,利用这个函数我们就可以根据实际情况的不同来动态的创建数组,这个数组可以根据实际情况的不同随时调整大小,而我们今天要学习的链表,就是我们想要的动态数组。
正文
一,链表的定义
链表是由若干个线性相连的结点构成的,结点实际上就是一个结构体,这个结点包含两部分,一个是我们要存放的数据,另一个就是结构体类型的指针,结点的指针总是指向下一个结点的地址,每一个结点就是通过指针联系的。为了找到链表的第一个结点,我们需要定义一个头指针headp指向第一个结点的地址,最后一个节点由于没有下一个结点,所以它的指针就是这一串指针的“尾”,我们定义为tailp,给它赋值为NULL
二,对链表的操作
由于链表的概念比较抽象,所以下面我们用一个存放不同单位的电话的电话簿的例子来展示对链表的操作。
struct unit_tele//以建立一个单位的电话簿为例子
{
char uname[30] = {};
char utele[12] = {};
struct unit_tele* next = NULL;
};
①创建链表
struct unit_tele* create_list()//创建链表
{
struct unit_tele* headp = NULL;//头指针
struct unit_tele* newp = NULL;//新建节点的指针
struct unit_tele* tailp = NULL;//尾指针。
char name[30] = { 0 };
while (1)
{
printf("请输入单位的名字(输入‘#’退出程序):\n");
scanf("%s", name);
if (strcmp(name, "#") == 0)//判断程序是否进行
break;
else
{
newp = (struct unit_tele*)malloc(sizeof(struct unit_tele));//建立结点
printf("请输入对应单位的电话:\n");//给新建结点中的元素赋值
scanf("%s", &(newp->utele));
strcpy(newp->uname, name);
if (headp == NULL)//判断创立的结点是不是第一个结点
{
headp = newp;//头指针指向的是首结点
tailp = newp;//只有一个节点时,这个既是首结点也是尾结点
}
else
{//建立列表时新建的链表总是添加在最后的,所以是尾结点
tailp->next = newp;//让上一个尾指针所在结构体的指针指向新的尾结点
tailp = newp;//让尾指针再次指向最后一个结构体
}
}
}
tailp->next = NULL;
return headp;
}
值得说明的是,在这个创建链表的函数中定义的三个指针,headp,newp,tailp,它们指向的就是各自对应的结构体,而他们指向的结构体中存放的指针才是指向下一个结构体的指针,定义它们是为了帮助我们添加我们正在创建的结点,所以我们在定义结构体时把内部的指针命名为next,意为指向下一个结点。
不知道读到这里你是否理解了各个指针的含义(笔者在学习时困惑了很久qvq),总而言之,为了建立有头有尾的完整的链表,就有了头,尾指针headp,tailp。为了定位到我们正在创建的结点,就有了newp,有了它我们才能知道新建结点的地址,让它的上一个结点指向它,并给新建结点内部的指针赋值为下一个结点的地址(上述例子是新建一整个链表,所以新建的结点总是尾结点)。
另外,在创建链表时候,我们要知道正在添加的结点是不是第一个结点,因为这会决定我们对头指针的赋值,在定义指针时,我们对所有指针的赋值都是空,事实也是如此:在我们创建链表之前每一个指针都是没有所指的。所以在建立链表之前头指针的指向也是空,只有创建了第一个结点(即首结点)之后头指针才会存放一个地址而不为空,所以我们可以通过头指针是否为空来判断我们正在创建的节点是不是第一个的节点,第一个结点既是首结点也是尾结点,所以要让首尾指针都指向这个结点,之后创建的结点都是新的尾结点,所以只需要让尾指针指向这里,并让旧的尾结点中的指针指向这里,我们就完成了一个结点的添加。
最后,在添加好所有的结点之后,我们再为尾结点的指针赋值为NULL并返回头指针就完成了链表的建立(这一步也可以在添加结点的循环中执行)。不知道你对上述过程是否理解,这里再分享一个笔者的观点,在我看来,headp,tailp,newp,可以理解为三个标志,它们指向的地方就是头,尾,新建的结点,而创建链表的过程,就是对这三个特定的结点不断修改的过程。
②输出链表
void print_unit_tele(struct unit_tele* phead)//输出链表体中的元素
{
struct unit_tele* p = phead;
while (p != NULL)
{
printf("单位%s的电话号码是:%s\n", p->uname, p->utele);
p = p->next;
}
学了创建链表的过程,那么输出链表的操作就很容易理解了。p指针接收了链表的头指针,利用p我们可以输出对应结点的内容,在此之后我们再把这个结点中的指针为p赋值,这样p就指向了下一个结点,依次我们就可以遍历整个链表。
③删除指定结点
struct unit_tele* delete_tele(struct unit_tele* headp, char uname[])//删除结点
{
struct unit_tele* lastp = NULL;
struct unit_tele* deletep = NULL;
for (deletep = headp; deletep != NULL; lastp = deletep, deletep = deletep->next)
//遍历链表寻找要删除的结点,deletep从指向第一个节点开始,当它指向NULL时还没有退出就说明遍历到尾也没有对应的结点
{
if (strcmp(deletep->uname, uname) == 0)
{
break;
}
}
if (deletep != NULL)//根据deletep是否为空指针判断有没有要删除的结点
{
if (lastp == NULL)//要删除的结点是第一个结点
{
headp = deletep->next;
return headp;
}
else
{
lastp->next = deletep->next;
free(deletep);
}
}
else
{
printf("要删除的单位本来就没有。\n");
}
return headp;
相信到这里这个程序对你来说已经是很好理解了,只是在这里我们增加了一个新的指针lastp,顾名思义这是指向我们要删除的结点的上一个结点,只有记录下它我们才能在删除一个结点之后再次将链表连接起来。通过相同的思想,我们也可以在指定的位置添加一个结点。
三,参考案例
借助一些应用了链表的实例会更好的理解这个概念,这里笔者再给出一个例子
要求:
学生成绩计算,假设学生成绩存放于score.csv文件中,由平时(五分制)、期中(百分制)、实验(百分制)和期末成绩(百分制)构成,编写一个程序,计算学生的总评成绩(百分制),并写入文件中。注:
a) 总评成绩=平时x0.1+期中x0.2+实验x0.2+期末x0.5;
b) 五分制转换为百分制的规则:A-95,B-85,C-75,D-65,E-55
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
typedef struct student stu;
struct student
{
char behave;
int mid;
int test;
int end;
stu* next;
};
//通过建立一个链表存储从文件读到的成绩
stu* creat_list(const char* file_name)
{
stu* headp = NULL;
stu* newp = NULL;
stu* tailp = NULL;
FILE* fp = NULL;
fp = fopen(file_name, "r");
//建立链表
do {
newp = (stu*)malloc(sizeof(stu));
if (headp == NULL)
{
headp = newp;
tailp = newp;
}
else
{
tailp->next = newp;
tailp = newp;
}
} while (fscanf(fp, "%c,%d,%d,%d\n", &(newp->behave), &(newp->mid), &(newp->test), &(newp->end)) != EOF);
tailp->next = NULL;
fclose(fp);
return headp;
}
//输出链表中的元素到文件
void in_file(const char* file_name,stu* headp)
{
FILE* fp = fopen(file_name, "w");
stu* nodep = NULL;
nodep = headp;
//计算后的总成绩
double grade = 0;
//转换成百分制的表现分
int bescore = 0;
while ((nodep->next) != NULL)
{
switch (nodep->behave)
{
case 'A':
bescore = 95;
break;
case 'B':
bescore = 85;
break;
case 'C':
bescore = 75;
break;
case 'D':
bescore = 65;
break;
case 'E':
bescore = 55;
break;
}
grade = bescore * 0.1 + (nodep->mid) * 0.2 + (nodep->test) * 0.2 + (nodep->end) * 0.5;
fprintf(fp, "%c,%d,%d,%d,%lf\n", nodep->behave, nodep->mid, nodep->test, nodep->end, grade);
nodep = nodep->next;
}
fclose(fp);
}
int main()
{
const char* file_name = "C:\\Users\\beaaaaar\\OneDrive\\Desktop\\score.csv";
stu* headp = NULL;
headp = creat_list(file_name);
in_file(file_name, headp);
printf("成绩已填写完成。\n");
return 0;
}
结语
以上就是笔者对于链表的定义和操作的理解,对链表还可以有其他的许多操作,但基本思想都与上述的三个操作相差不大,如果理解了上述的三个操作,那么其他的操作对你来说一定不在话下!感谢阅读,希望可以再学习c语言的路上帮到你,如果你发现了笔者的错误,欢迎你为笔者指正(笔者也是小白ovo)!希望在学习路上,大家都可以实现自己的目标!