先听我说:
emm...这是本人在上的第一篇文章,我呢,是一名在校的大一学生(2019级),真的,我总感觉我们这一届大一好像和其他学长学姐的大一不一样。高考放了三个月假,上了三四个月的课,又放了四个多月的假,这不,再等一会儿,又得放暑假了。
所以呢,我总得干些啥子事情吧,于是乎,我来了。(文章中若有错误地方,还请指出,谢谢。)
0. 结构体:
首先呢,我们在认识链表之前,应首先回顾结构体,这里便通过对结构体的简单使用来回顾:
#include
#define OUTPUT1 "%-10s\t%-5s\t%-5c\t%-5d\t%-5f\n",
#define OUTPUT2 OUTPUT1 p->Name,p->ID_Number,p->Sex,p->Age,p->Height
struct Information {
char Name[20];
char ID_Number[16];
char Sex;
int Age;
double Height;
}lourina={"倚阑","2019120888",'M',19,170.5},student[2]={
{"GoodStudent", "2019000000", 'M', 18, 188.8},
{"LuckyDog", "2020111111", 'M', 18, 178.8}};
int main()
{
struct Information Alina = { "Alina", "2020120688", 'W', 20, 167.8 };
struct Information* p;
p = &lourina;
printf(OUTPUT2);
p = &Alina;
printf(OUTPUT2);
for (int i = 0; i < 2; i++, p++)
{
if (0 == i)
p = student;
printf(OUTPUT2);
}
return 0;
}
输出结果:
倚阑 2019120888 M 19 170.500000
Alina 2019120688 W 20 167.800000
GoodStudent 2019000000 M 18188.800000
LuckyDog 2019111111 M 18 178.800000
关于使用到的宏定义以及结构体指针,还需读者细细消化。
结构体是和数组一样同属构造类型,但结构体与数组不同的地方在于结构体的构造,其可以是由基本数据类型构造,也可以是由构造类型构造,也可以是基本数据类型和构造类型混合构造一个结构体。所以结构体相比于数组具有更大的灵活性。
为了更加深入认识结构体的构造,我们借助代码直观地感受。
struct Student {
char Name[20];
char ID_Number[16];
char Sex;
int Age;
double Height;
};
struct Grade {
char subject[16];
double Test_Scores;
double Usual_performance;
};
struct Information {
char Class[20];
struct Student student;
struct Grade grade[8];
};
1. 什么是链表:
很简单,链表可以简单理解为一个一个的表单被链接起来。
链表其实是一种非常基础的数据结构,在项目实践中会经常用到,区别于数组,数组在创建时候需要规定数组的大小,而链表则是根据个人的需要,使用多少便创建多长的链表。
那为什么,既然链表这么方便,还须定义数组这样的数据类型,这个问题留到最后,我再进行回答。
2. 链表的创建:
我们说链表,数据我们是知道如何创建的,毕竟学了int/float/char/double等数据类型,那么如何链接起来呢?
请看下图:
结构体在这个时候派上用场,从简单的示意图中知道,这个结构体主要分为两部分,一用来存放数据,另一用来链接下一个结构体。很显然,完成了链表创建的基本思想。那么如何链接,指针就上场了,结构体中创建一个指针,且使这个指针指向下一个结构体,就完成链表的创建操作了。
但是,会有人问,我该怎么指向下一个结构体,且即使指向了下一个结构体,也是在已经创建并初始化了的结构体变量的情况下才能链接,无法实现想要多少就创建多长的目的。
所以说,我们需要借助两个函数:malloc和free函数。
malloc函数作用:在内存中动态地分配一块size大小的内存空间。
free函数作用:释放由指针指向的内存区,使部分内存区能被其他变量使用。
多说无益,上操作!
#include
#include
struct Student {
char Name[20];
char ID_Number[16];
char Sex;
int Age;
double Height;
struct Student* Next;
};
int main()
{
struct Student* p, * q;
p = (struct Student*)malloc(sizeof(struct Student));
gets(p->Name);
gets(p->ID_Number);
p->Sex = getchar();
scanf("%d", &p->Age);
scanf("%lf", &p->Height);
p->Next = NULL;
q = p;
p = (struct Student*)malloc(sizeof(struct Student));
q->Next = p;
p->Next = NULL;
return 0;
}
一直到p->Next=NULL之前我们都能理解,就是简单为指针p所指向的结构体变量进行赋值。
(噢对,先对p指针的这个使用到malloc函数的语句进行解释,在malloc前面的括号中的struct Student*其实是一个类型转化操作,只不过他并不是自动类型转换,而是强制类型转换,即我们强制将分配得到的大小为sizeof(struct Student)的内存空间类型转换为指向这个结构体的指针类型,即这个p指针指向了一个没有命名的属于结构体Student的结构体变量)
p->Next=NULL;其实就是使p所指向的结构体变量中的Next指针指向空(NULL)。
q=p;就是简单的指针赋值操作,使q指向p所指向的内存空间(结构体变量)。
p=(struct Student*)malloc(sizeof(struct Student));重新分配一个新的内存空间,且使指针p指向该内存空间。
q->Next=p;使指针q指向的结构体变量中的Next指针指向p指针。
为便于直观理解,再上图:
相信对链表的创建有一定的感觉,我们在该链表创建的过程中,并没有创建结构体变量,而是通过结构体指针间接地创建结构体变量,所以就可以避免必须在程序运行之前就规定好链表长度的问题。
为了更好实现链表的强大功能,我们引入循环结构:
#include
#include
struct Student {
char Name[20];
char ID_Number[16];
char Sex;
int Age;
double Height;
struct Student* Next;
};
int main()
{
struct Student* pHead, * pEnd, * pNew;
int n;
printf("请输入学生人数:");
scanf("%d", &n);
pNew = (struct Student*)malloc(sizeof(struct Student));
for (int i = 0; i < n; i++)
{
getchar();
printf("请输入第%d位学生的相关信息:\n", i + 1);
printf("请输入学生姓名:"); gets(pNew->Name);
printf("请输入学生学号:"); gets(pNew->ID_Number);
printf("请输入学生性别:"); pNew->Sex=getchar();
printf("请输入学生年龄:"); scanf("%d",&pNew->Age);
printf("请输入学生身高:"); scanf("%lf",&pNew->Height);
pNew->Next = NULL;
if(0 == i)
pHead = pNew;
else
pEnd->Next = pNew;
pEnd = pNew;
pNew = (struct Student*)malloc(sizeof(struct Student));
}
free(pHead);
return 0;
}
充分实现了我们想要的功能,即根据个人的需求创建相应长度的链表,且不多不少。
3. 链表的遍历:
相信读者阅读到这,应该认为,我们只是创建了链表,输入了数据,但是我们的目的是想要在合适时候输出这些数据。
所以我们就有链表的遍历操作。
我对上面的程序进行了补充,请看:
#include
#include
struct Student {
char Name[20];
char ID_Number[16];
char Sex;
int Age;
double Height;
struct Student* Next;
};
int main()
{
struct Student* pHead, * pEnd, * pNew;
int n;
printf("请输入学生人数:");
scanf("%d", &n);
pNew = (struct Student*)malloc(sizeof(struct Student));
for (int i = 0; i < n; i++)
{
getchar();
printf("请输入第%d位学生的相关信息:\n", i + 1);
printf("请输入学生姓名:"); gets(pNew->Name);
printf("请输入学生学号:"); gets(pNew->ID_Number);
printf("请输入学生性别:"); pNew->Sex = getchar();
printf("请输入学生年龄:"); scanf("%d", &pNew->Age);
printf("请输入学生身高:"); scanf("%lf", &pNew->Height);
pNew->Next = NULL;
if(0 == i)
pHead = pNew;
else
pEnd->Next = pNew;
pEnd = pNew;
pNew = (struct Student*)malloc(sizeof(struct Student));
}
pNew = pHead;
for (int i = 0; i < n; i++)
{
getchar();
printf("请输入第%d位学生的相关信息:\n", i + 1);
printf("请输入学生姓名:"); gets(pNew->Name);
printf("第%d位学生学号:%s\n", i + 1, pNew->ID_Number);
printf("第%d位学生性别:%c\n", i + 1, pNew->Sex);
printf("第%d位学生年龄:%d\n", i + 1, pNew->Age);
printf("第%d位学生身高:%f\n", i + 1, pNew->Height);
pNew = pNew->Next;
}
free(pHead);
return 0;
}
细心的读者已经发现了我们之前创建结构体指针时候,是创建了是三个结构体指针变量*pHead、*pEnd、*pNew分别指向链表的首结点、尾结点、新结点。目的是保存链表的重要结点信息,以便后续使用该链表时候能找到首结点进行遍历。
当然,这样也能实现我们想要的遍历。但是,我们的遍历操作一般是如下:
int i = 0;
for( pNew = pHead; pNew != NULL; pNew = pNew->Next, i++)
{
printf("第%d位学生姓名:%s\n", i + 1, pNew->Name);
printf("第%d位学生学号:%s\n", i + 1, pNew->ID_Number);
printf("第%d位学生性别:%c\n", i + 1, pNew->Sex);
printf("第%d位学生年龄:%d\n", i + 1, pNew->Age);
printf("第%d位学生身高:%f\n", i + 1, pNew->Height);
}
free(pHead);
所以这就能明白为什么我们在最后一个结点(结构体变量)时候,还使其Next指针指向NULL,就是为后续链表的其他操作做一定的准备工作(亦能放防止程序运行时候发生错误)。
那么,我们对链表已经有初步的掌握了,如下是我第一次写链表的代码:
#include
#include
#include
struct Information {
char Names[20];
int ID_Number;
struct Information* Next;
};
void fengexian();
int main()
{
int i = 2;
char Action[10] = "\0";
char Exit[10] = "exit";
char Again[10] = "again";
printf("正在启动创建链表功能···\n\t\t···创建链表功能已启动!\n");
fengexian();
struct Information* pHead, * pNew, * pEnd;
pNew = pEnd = (struct Information*)malloc(sizeof(struct Information));
printf("请输入第1个链表的相关数据:\n");
printf("请输入学生姓名:"); gets_s(pNew->Names,20);
printf("请输入学生学号:"); scanf("%d", &pNew->ID_Number);
pNew->Next = NULL;
pEnd = pNew;
pHead = pNew;
pNew = (struct Information*)malloc(sizeof(struct Information));
fengexian();
printf("继续创建链表输入again;结束创建链表输入exit\n"); getchar();
gets_s(Action,10);
fengexian();
Again_or_Exit:
if (Action[0] != '\0')
{
int Action_Length = strlen(Action);
Again_or_Exit_Result:
if (Action_Length == 5)
{
for (int i = 0; i < 5; i++)
{
if (Action[i] != Again[i])
goto Error1;
}
goto Again_New_List;
}
else if (Action_Length == 4)
{
for (int i = 0; i < 4; i++)
{
if (Action[i] != Exit[i])
goto Error2;
}
goto GetNum;
}
else
{
Error1:
Error2:
printf("你输入了一个错误字符串,请重新输入:\n");
gotoAgain_or_Exit_Result;
}
}
for(; ; )
{
Again_New_List:
printf("请输入第%d个链表的相关数据:\n", i);
printf("请输入学生姓名:"); getchar(); gets_s(pNew->Names,20);
printf("请输入学生学号:"); scanf("%d", &pNew->ID_Number);
pNew->Next = NULL;
pEnd->Next = pNew;
pEnd = pNew;
pNew = (struct Information*)malloc(sizeof(struct Information));
fengexian();
printf("继续创建链表输入again;结束创建链表输入exit\n"); getchar(); gets_s(Action,10);
fengexian();
i++;
goto Again_or_Exit;
}
GetNum:
fengexian();
printf("已结束对链表的数据输入\n\t现启动对链表数据的输出(确认请输入0,;取消则输入1)\n");
int Num;
scanf("%d", &Num);
if(Num == 0)
{
int i = 1;
fengexian();
fengexian();
printf("\t序号\t\t学生姓名\t\t学生学号\n");
for(pNew = pHead; pNew != NULL; pNew = pNew->Next, i++)
{
printf("\t%d\t\t%s\t\t\t%d\n", i, pNew->Names, pNew->ID_Number);
}
printf("已完成对链表数据的输出");
}
else if (Num == 1)
return 0;
else
{
printf("你输入了一个错误数字\n请重新输入:\n");
goto GetNum;
}
return 0;
}
void fengexian()
{
for (int i = 0; i < 10; i++)
{
printf("----------");
}
printf("\n");
return;
}
【ps:这是我第一次写的链表程序,在判断again还是exit时候若是输错字符串,会进入无限循环,这是代码的一个逻辑错误,因为我用了比较多的goto语句,所以容易犯死循环的错误】
4. 链表的插入:
完成了对链表的创建操作,假如我们需要在创建好的链表中插入新的数据,这时候,就需要对链表进行插入操作。
我们先从简单例子看起:
在尾结点插入新结点:
在尾结点插入新结点,和链表的创建操作类似,大体做法一致,做法如下:(p为结构体指针)
p->Next = NULL;
pEnd->Next = p;
pEnd = p;
在链表中某一结点处添加新结点:
对于在链表中某一结点处添加新结点,只需要找到其要添加的位置,然后改变对应结点的结构体变量的指针指向即可完成插入操作。如下:
p->Next = pNew->Next;
pNew->Next = p;
在首结点前插入新结点:
在多数的教材中,其首结点是不保存数据的,所以,若插入的新结点要放到链表开头,则步骤与第二点一致。但我所阐述的首结点是保存数据的。故这里对在首结点前插入新结点再做解释。
其代码操作如下:
p->Next = pHead;
pHead = p;
以上三段代码仅仅是介绍结点插入时候应该对结构体指针进行怎样的处理,但在具体程序应用中,应注意查找的条件,寻找对应的结点,确定结点插入步骤是正确的。
5. 链表的删除:
学习了对链表的插入操作后,则也应该有相应的链表的删除操作。这里做简单介绍。
删除尾结点:
pEnd = pNew;
free(pNew->Next);
pEnd = NULL;
这里必须free掉pEnd所指向的内存空间。虽然编译器不会报错,但作为合格的程序员,使用完的内存空间,必须释放!!!
删除链表中的某一结点:
pNew->Next = p->Next;
p->Next = NULL;
free(p);
删除首结点:
p = pHead;
pHead = p->Next;
p->Next = NULL;
free(p);
对于删除首结点,不可直接使用pHead=pHead->Next操作;这样会忽略了原来pHead所指向的结构体变量,其内存空间得不到释放,会浪费内存空间,甚至造成内存泄漏。
删除的结点,其内存空间必须释放!!!
6. 双向链表(简略描述,此次不做重点):
双向链表,其实就是在构造结构体时候,再添加一指针,而这指针指向上一结构体变量,结合具体的链表创建操作,即可完成双向链表的创建。
双向链表相比于单向链表,在某些问题研究上,其执行效率要比单向链表的执行效率高。
7. 链表和数组:
前面提到:有人会问,“既生链表,何生数组”?现对该问题进行解答:
在内存存储方式上,链表为链式存储结构,数组为顺序存储结构。所以在访问数据中,数组的效率更高,而链表只能通过遍历查找。
在对元素的操作上,链表的操作更为简便,数组则较为繁琐。如需在数组元素之间插入以新的元素,需要移动大量的数据。
在占用内存空间上,数组占用内存空间较小,且其内存空间的释放由编译器自行操作,不由程序员控制。
在功能上,链表可实现动态存储和动态删除,数组则不可。
PS:另外,更多文章来自本人微信公众号“倚阑终泪干”。感谢支持。