c语言学习之链表2

目录

前言

PS,

例题

定义结构体

typedef类型定义符

创建单向动态链表

疑惑1

小优化

删除结点

插入结点

打印

错误,

int main(){}

完工 

尾声


前言

在对c语言的学习中,目前我学到了链表,对于自学者的我来说,实在搞不清到底这么搞,那些指针如何指,以及各种错综复杂的判断条件。于是乎,在折腾了这么久总算有些初窥,写下这些东西只是我一点心得,可能不完善,可能也不太浅显易懂,但这是我的初步尝试。

PS,

  1. 每块代码块右面写有注释,但好像离得远,得划过去ヽ(・_・;)ノ
  2. 利用vs2022编写,输入字符的函数为scanf_s();
  3. 前面讲的比较详细,后面相似便可不必赘述。

例题

题目如下:e71e8f390c554dd399eddca58fe9086e.jpeg

 该道题目涉及创建一个动态单项链表,同时还有删除结点以及插入结点这三个方面。b0f1690ea89a4b14a481e9590fd372d1.jpeg

 我在书上找到的图,大概是这样,也很直观。一个结点包括存储的数据以及一个指针来与其他结点相连,大多视频也会画出或给出这样一幅图。

定义结构体

链表的具体如下,,,,,

#include<stdio.h>
#include<malloc.h>//提供malloc函数原型
#include<string.h>//sizeof
#define NULLL 0

typedef struct Student {       //利用typedef定义新类型名,用法 typedef int counter;
	             //就可用 counter代替int,表明计数器,该结构体中,用str代替struct Student
	int num;
	int scoure;
	struct Student* next;
}str;

凭借我浅薄的学识,就有了以下代码,首先搞出头文件,再定义一个单项动态链表的结构体,即struct Student.就结构体而言,其中包含了num表示学生的学号,而scoure表示分数。还有一枚定义为struct Student类型名为next的指针。

typedef类型定义符

其中,我用了typedef类型定义符,其作用是使struct Student等价于str,如此,申明一个struct Student变量便可这样写

str 变量名;

6c6b915539ed41e5ac6dc60e185f4df9.jpg

创建单向动态链表

之后,将创建结构体的函数搞好。这是我看书学的,书中也有例,如下,这是它给出的解析5e6a6d11cd8149cdb06a3a9846ee1680.jpeg2af6838eb4994ebaba5b0fd9f5494d14.jpeg

 学习过程中,我是看着书将代码块打出来,但实际上有些不知所云,后来经过好多天的折腾也算大概窥见了一些东西。如下是创建链表的函数,加上我自己的见解写上的注释。

struct Student* stu1() {                               //创建链表
	struct Student* head, * p1, * p2;         //定义一个头指针,两个工作指针,节点数n初始化为0
	int n = 0;              //将头指针指向空,为p1,p2申请内存
	head = NULL;
	p2 = p1 = (struct Student*)malloc(sizeof(struct Student));
	printf("Please input students' numer and scoure.\n");
	scanf_s("%d", &p1->num); scanf_s("%d", &p1->scoure);
	while (p1->num!=0)          //开始循环,以num为0作为结束条件
	{
		n++;                             //结点数加一
		if (n == 1)head = p1;                 //将p1给头指针
		else p2->next = p1;         //将p1链到p2尾部
		p2 = p1;                         //将p1赋给p2
		p1 = (struct Student*)malloc(sizeof(struct Student));     //再次为p1申请内存
		scanf_s("%d", &p1->num); scanf_s("%d", &p1->scoure);
	}
	p2->next = NULL;          //循环结束后,将p2的尾部链到空,避免成为野指针
	free(p1);                                            //将p1的空间释放
	return head;   
}

 以下是我自己的见解。

  1. 前面应有对这个函数的申明,这只是包含函数体部分。
  2. 首先,申明了三个指针,分别是头指针head和两个工作指针p1,p2。将int类型的n作为计数点,并初始化为0。将头指针指向空(NULL在前面用#define定义为0
  3. 定义了一个int类型的n来判断,对于这个结点的计数,一开始我不知其用处。后来我发现每进行一次while循环,结点就增加一次。我认为其作用就是对下面是否为头指针的判断而作为判断条件。
  4. 为p1,p2分配内存,利用malloc,其定义在头文件<malloc.h>中。同时,利用sizeof()来统计struct Student的空间大小。
  5. 从键盘中读入p1的num和scoure中去。然后开始循环,持续读入数据来链接链表。循环的条件为读入的num不为0,即跳出循环的条件是键盘输入0,但是读入num后还需输入scoure的值才能跳出循环,(因为我写的函数中,scanf输入num后才输出scoure)否则计算机将会等待用户输入。后面还会针对这个条件有一些对我产生的问题的看法。
  6. 在while循环中,我们假设n=1先,则就满足了第一个判断,所以将p1的地址给到了head头指针,随后else部分内容不执行,则执行下一行 p2=p1;这样,就将p2和head都指向了p1。head的功能定义便可完成了,让它永远指向链表的第一个地址,但是p2就开始配合p1来连接链表。
  7. 随后,再重新为p1申请一份空间(malloc),再次将数据读入,当num不为0时进入第二次循环,此时n=2,则head=p1将永远不会触发,将执行else部分,p2->next = p1;在while的第一次循环中,我们已经将原先的p1给到了p2,此时的p1是重新输入的数据,我们把它链到p2的尾部。然后再次将p1赋给p2,实际上就是重复这样一个过程:将p1保存到p2,p1重新申请空间存入数据,链到原来保存的数据尾部。

    27bf8cd9ff314f0bac0ec69487c0b5a0.jpeg


    ​​​​我的想象

  8. 跳出循环后,将p1申请的空间释放掉,再将p2的尾部指向空。然后该函数将head头指针返回。头指针指向第一次循环结点数为1的数据。

  9. 疑惑1

  10. 疑惑1,在输入了0,终止循环后,0的这组数据是否会被接到链表尾部?(这个问题看起来有些蠢,但当时学习时满脸疑惑,的确有这个疑问,所以我就记录了下来。)答案实际上是不会的(现在我知道了,但也是我写了好久这个链表才悟到的)因为输入的语句scanf_s("%d", &p1->num); scanf_s("%d", &p1->scoure);放在最后面,之后将进行判断,为假跳出循环。所以这组数据存到了p1当中,但没有链到链表尾部,随后跳出循环将p1申请的空间释放了。


小优化

对于第5点,输入了num的值,实际上已经满足了跳出循环的条件。但是计算机还得等待用户键入scoure的值才能继续执行。于是,将scoure的键入放在开头,有了这样。c5ed388b73384986b2b935c0ee86fe3f.png

 在循环外输入num的值,进入循环后再键入scoure。如此,只需输入一次0回车,便可结束循环。4f49b530119641579b7d7f6b2f2b8479.png

删除结点

在这里的删除的结点,是给出一个结构体中的变量值,然后通过遍历查找存在该值的结点来进行删除。删除的位置有三种

  1. 如果删掉的结点是表头
  2. 如果是链表中任意一结点
  3. 如果是表尾

af6536b767c7418b8150706c6fc86af2.jpeg

dcebd28a30f04792b58f671a62b54749.jpeg

以下是代码和注释,创建链表时已讲得详细,此时相似内容便不必赘述。


struct Student* stu2(struct Student* head, int n) {     //删除结点
	str* p1, * p2;	
	if (!head)                           //判断链表是否存在
			{
				printf("the list null]\n");
				return head;                    //不存在就直接返回

			}
	p1 = head; p2 = NULL;      //若存在则进行下一步,将传入该函数的指针赋给p1,p2初始化为空
	while ((n != p1->num)&&(p1->next!=NULL)) {   //开始遍历,结束条件是在链表中找到了n,亦或p1的下一个指针为空
		p2 = p1; p1 = p1->next;  //将指针保存下来给p2,再将next赋给p1
	}
	if (n == p1->num) {                        //如果找到了,开始判断是否为头指针
		if (head == p1) head = p1->next;    //如果是,将p1的下一个指针给头指针,将原来的头指针从链表中断掉
		else p2->next = p1->next;    //如果不是,就将之前保存到p2的指针尾部链到p1的尾部,这样,就将p1从链表中删去
		free(p1);                         //去掉p1后将p1释放掉                              
		printf("%d delete.\n",n);
	}
	else printf("the num was unfound.\n");          //else对应前面的if (n == p1->num)
	return head;
}
  1. 首先用if进行判断,判断传进来的链表头指针是否为空,!head可以理解为head==0。如果为空,直接将原链表返回即可,再进行下面已经没有意义了。
  2. 再用while进行遍历。目的是找出需删除的结点的数据是否在该链表中,即条件为(n != p1->num)&&(p1->next!=NULL)) 结束条件是在链表中找到了n,亦或p1的下一个指针为空,表示彻底找不到。
  3. 跳出循环后可能有两种情况,一种是找到了n,第二种是p1的next已经指向了空。用if先来假设第一种情况就有p1==n,则else为第二种情况。
  4. 在查找到数值n中,也有两种情况,第一种是p1就是head,即链表中第一个有数据的地址,第二种就是p1不是head。我们再次用if进行判断。因为要删除的结点就是p1,所以第一种情况我们把head指向p1的下一个地址即可,即(*p1).next。
  5. 针对p1不是head的这种情况,在我们可以回到while循环中看到,已经用p2保存到了前一个p1的地址。所以我们在这里只需要将p2的next指向p1的next,就可以把p1断掉。最后,再将p1释放掉。
  6. 回到前面,我们说while有两个条件,只需满足其一便可跳出循环 而对于第二个循环,则是最后面else部分,因为已经遍历到了最后,后面没有数据可以遍历,即是在该链表中找不到数据与n符合。
  7. 疑惑,如果最后一个数据的next为空跳出了循环,那万一该数据便是与n符合,会被漏掉嘛?书上也是这样写,那就一般来说没有问题。后来我想到了,跳出循环后是用if判断,如果符合将会跳到if里面,所以不必担心。

插入结点

原链表的数据分数scoure为从大到小降序排列。然后通过对比插入链表的数据,该插入到原链表何处保持降序的排列。

a6795695ea90488db9e589e3a78f094d.jpeg

77966620f4ef4bf7ad1a2d07aec34d2d.jpeg

以下,传入两个链表,其中一个为插入链表。

操作与删除链表相似。


str* str3(str* head, str* add) {        //插入结点
	str* p0, * p1, * p2=NULL;      //定义三个指针,其中p0存放要插入的指针
	p1 = head; p0 = add; 
	if (!head) {               //判断,如果原链表不存在,则要插入的链表就形成新链表
		head = p0;
		p0->next = NULL;       //再在主函数中创建的add没有尾部没有指向空?!
	}
	else {
		while ((p0->scoure <= p1->scoure) && (p1->next != NULL)) { //如果原链表存在,则开始遍历,如果要插入的链表中scoure小于原链表中的scoure,
			p2 = p1;                        //就搜索next的数据,将p1赋给p2,p1指向next
			p1 = p1->next;
		}

		if (p0->scoure >= p1->scoure) {         //如果要插入的链表scoure大于等于原链表数据
			if (p1 == head)p0 = head;    //判断这个数据是不是头指针,如果是,头指针直接赋给要插入的指针
			else p2->next = p0;    //如果不是,就将p0赋给p2的next,前面有将p1的数据保存到p2
			p0->next = p1;          //最后,再将p1接上
		}
		else {
			p1->next = p0;   //else对应if (p0->scoure >= p1->scoure),如果找不到,则表明p1->next == NULL
			p0->next = NULL;         //就直接链到p1尾部,将p0尾部指向空
		}
	}
	return head;
}
  1. 定义了三个指针,其中p1,p2依旧为工作指针,p0则来存放要加进去了函数。
  2. 此时,也有两种情况,如果原链表不存在,那么插入一个链表就相当于创建一个新链表。
  3. 否则,即为原链表存在,开始while循环,同上。
  4. 如果找到了合适的位置插入,就得进行判断是否为头指针。

最后,如果找不到,则插入到表尾即可。


打印


void pr(str* a) {               //打印
	str* p1;                //定义一个结构体指针,将传进来的指针赋给它
	p1 = a;
	while (p1!=NULL)                                         
	{
		printf("%4d %4d\n", p1->num, p1->scoure);                           
		p1 = p1->next;       // 打印完成,指向下一个指针,直到NULL,结束循环
	} 
}

 利用循环打印链表各值。

错误,

在这里之前我犯了一个错误。我之前是直接在main()中利用创建好的链表来进行打印,而没有将指针给到其他工作指针进行打印。而后,打印完后我再尝试在链表中删除结点,结果返回了链表显示为空表明找不到该链表。我以为是写的删除结点的函数出了问题,原来是这里。

错误如下

int main() {
	str* abc;
	str* ABC;
	int n;
	abc = stu1();                        //利用函数创建链表
                                                              

	while (abc!=NULL)                                         
	{
		printf("%4d %4d\n",abc->num, abc->scoure);                           
		abc = abc->next;                                                        /
	}                       
	printf("Do you want to delete number?(0 to quit)\n");   //删除结点
	scanf_s("%d", &n);
	while (n != 0) {
		abc = stu2(abc, n);
		printf("Do you want to delete number?(0 to quit)\n");
		scanf_s("%d", &n);	
	}

我不明所以,后来我才发现,在打印过程中,原来创建的指针已经指向它的next进行循环打印了。结束后,指向的地址也已经不是原来的地址了,而是while (p1!=NULL)中的NULL,所以,链表不存在。

后来,便发觉为打印的过程创建一个函数,每次打印调用也很方便,也不必在主函数赘述那么多东西。但也要将传进来的指针赋值给另一个指针,否则又会犯刚才的错误。

int main(){}


int main() {
	str* abc;
	str* ABC;
	int n;
	abc = stu1();              //利用函数创建链表
	pr(abc);    
         

  //删除结点
	printf("Do you want to delete number?(0 to quit)\n");
	scanf_s("%d", &n);
	while (n != 0) {
		abc = stu2(abc, n);
		printf("Do you want to delete number?(0 to quit)\n");
		scanf_s("%d", &n);	
	}
	pr(abc);


//创建新结点,并插入原链表
	ABC = (struct Student*)malloc(sizeof(struct Student));                       //为ABC申请一份空间,建立新结点
	printf("Do you want to add datda?(0 to quit)\n");
	scanf_s("%d", &ABC->num);	scanf_s("%d", &ABC->scoure);
	while (ABC->num!=0)
	{
		abc = str3(abc, ABC);
		
		printf("Do you want to add datda?(0 to quit)\n");
		ABC = (str*)malloc(sizeof(str));
		scanf_s("%d", &ABC->num);	scanf_s("%d", &ABC->scoure);
	}
	pr(abc); 
	free(ABC);                                                                  //释放ABC空间
	return 0;
}

完工 

最后的内容整合起来就是这样了。

#include<stdio.h>
#include<malloc.h>//提供malloc函数原型
#include<string.h>//sizeof
#define NULLL 0
typedef struct Student {                                                     //利用typedef定义新类型名,用法 typedef int counter;
	                                                                         //就可用 counter代替int,表明计数器,该结构体中,用str代替struct Student
	int num;
	int scoure;
	struct Student* next;
}str;

void pr(str* a);                                                            //声明函数,用以打印链表
struct Student* stu1();                                                     //创建链表
struct Student* stu2(struct Student* head, int n);                          //删除结点
str* str3(str* head, str* add);//插入结点


struct Student* stu1() {                                                     //创建链表
	struct Student* head, * p1, * p2;                                        //定义一个头指针,两个工作指针,节点数n初始化为0
	int n = 0;                                                               //将头指针指向空,为p1,p2申请内存
	head = NULL;
	p2 = p1 = (struct Student*)malloc(sizeof(struct Student));
	printf("Please input students' numer and scoure.\n");
	scanf_s("%d", &p1->num); scanf_s("%d", &p1->scoure);
	while (p1->num!=0)                                                       //开始循环,以num为0作为结束条件
	{
		n++;                                                                 //结点数加一
		if (n == 1)head = p1;                                                //将p1给头指针
		else p2->next = p1;                                                  //将p1链到p2尾部
		p2 = p1;                                                             //将p1赋给p2
		p1 = (struct Student*)malloc(sizeof(struct Student));                //再次为p1申请内存
		scanf_s("%d", &p1->num); scanf_s("%d", &p1->scoure);
	}
	p2->next = NULL;                                                         //循环结束后,将p2的尾部链到空,避免成为野指针
	free(p1);                                                                //将p1的空间释放
	return head;   
}
struct Student* stu2(struct Student* head, int n) {                          //删除结点
	str* p1, * p2;	
	if (!head)                                                               //判断链表是否存在
			{
				printf("the list null]\n");
				return head;                                                //不存在就直接返回

			}
	p1 = head; p2 = NULL;                                                    //若存在则进行下一步,将传入该函数的指针赋给p1,p2初始化为空
	while ((n != p1->num)&&(p1->next!=NULL)) {                               //开始遍历,结束条件是在链表中找到了n,亦或p1的下一个指针为空
		p2 = p1; p1 = p1->next;                                              //将指针保存下来给p2,再将next赋给p1
	}
	if (n == p1->num) {                                                     //如果找到了,开始判断是否为头指针
		if (head == p1) head = p1->next;                                    //如果是,将p1的下一个指针给头指针,将原来的头指针从链表中断掉
		else p2->next = p1->next;                                           //如果不是,就将之前保存到p2的指针尾部链到p1的尾部,这样,就将p1从链表中删去
		free(p1);                                                           //去掉p1后将p1释放掉
		printf("%d delete.\n",n);
	}
	else printf("the num was unfound.\n");                                  //else对应前面的if (n == p1->num)
	return head;
}
void pr(str* a) {                                                            //打印
	str* p1;                                                                //定义一个结构体指针,将传进来的指针赋给它
	p1 = a;
	while (p1!=NULL)                                         
	{
		printf("%4d %4d\n", p1->num, p1->scoure);                           
		p1 = p1->next;                                                        // 打印完成,指向下一个指针,直到NULL,结束循环
	} 
}
str* str3(str* head, str* add) {                            //插入结点
	str* p0, * p1, * p2=NULL;                               //定义三个指针,其中p0存放要插入的指针
	p1 = head; p0 = add; 
	if (!head) {                                            //判断,如果原链表不存在,则要插入的链表就形成新链表
		head = p0;
		p0->next = NULL;                                       //再在主函数中创建的add没有尾部没有指向空?!
	}
	else {
		while ((p0->scoure <= p1->scoure) && (p1->next != NULL)) {     //如果原链表存在,则开始遍历,如果要插入的链表中scoure小于原链表中的scoure,
			p2 = p1;                                                     //就搜索next的数据,将p1赋给p2,p1指向next
			p1 = p1->next;
		}

		if (p0->scoure >= p1->scoure) {                                  //如果要插入的链表scoure大于等于原链表数据
			if (p1 == head)p0 = head;                                    //判断这个数据是不是头指针,如果是,头指针直接赋给要插入的指针
			else p2->next = p0;                                          //如果不是,就将p0赋给p2的next,前面有将p1的数据保存到p2
			p0->next = p1;                                               //最后,再将p1接上
		}
		else {
			p1->next = p0;                                              //else对应if (p0->scoure >= p1->scoure),如果找不到,则表明p1->next == NULL
			p0->next = NULL;                                            //就直接链到p1尾部,将p0尾部指向空
		}
	}
	return head;
}

int main() {
	str* abc;
	str* ABC;
	int n;
	abc = stu1();                                                             //利用函数创建链表
	pr(abc);                                                                  //打印链表
	printf("Do you want to delete number?(0 to quit)\n");
	scanf_s("%d", &n);
	while (n != 0) {
		abc = stu2(abc, n);
		printf("Do you want to delete number?(0 to quit)\n");
		scanf_s("%d", &n);	
	}
	pr(abc);
	ABC = (struct Student*)malloc(sizeof(struct Student));                       //为ABC申请一份空间,建立新结点
	printf("Do you want to add datda?(0 to quit)\n");
	scanf_s("%d", &ABC->num);	scanf_s("%d", &ABC->scoure);
	while (ABC->num!=0)
	{
		abc = str3(abc, ABC);
		
		printf("Do you want to add datda?(0 to quit)\n");
		ABC = (str*)malloc(sizeof(str));
		scanf_s("%d", &ABC->num);	scanf_s("%d", &ABC->scoure);
	}
	pr(abc); 
	free(ABC);                                                                  //释放ABC空间
	return 0;
}

尾声

好了,对于链表的学习我目前是到了这里。我喜欢看书学习,但总会产生许许多多的疑问。本来是计划着一两个晚上能不能将它学个大概,但事实证明这块骨头我啃了好几天,总是搞不清为什么,怎么是这样写的。我也去看过一些视频,但也没有将我的疑惑给整明白。

于是乎,我开始看着书凭自己印象来写代码,试图搞懂它。事实也证明了,这是有效的。在我一次次运行失败后,我的疑惑也逐渐揭露了,现在看着书本,也觉得就是这么一回事,可悲的是我没有将在我大脑中的疑惑尽数记下。

如果当初我只用一两个晚上搞它,可能我的基础是不牢固的,但是现在我能将我对链表的理解写成这样一篇东西,真的很庆幸我当初是慢了下来,而没有贪快。

路漫漫其修远兮,吾将上下而求索。

唉,苦我也久矣┐(─__─)┌

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值