当前位置: 首页 > news >正文

【C语言】最详细的单链表(两遍包会!)

🦄个人主页:小米里的大麦-CSDN博客

🎏所属专栏:C语言数据结构_小米里的大麦的博客-CSDN博客

🎁代码托管:黄灿灿/数据结构 (gitee.com)

⚙️操作环境:Visual Studio 2022

目录

一、前言

二、单链表的概念

1. 单链表的特点

2. 单链表的基本操作

3. 画图思想

三、单链表分步详解

1. 函数声明

2. 打印函数 

3. 尾插函数(很重要!) 

4. 头插函数

5. 尾删函数

6. 头删函数

7. 查找函数

8. 在指定节点 pos 之前插入一个新节点

9. 删除指定节点 pos

10. 在指定节点 pos 之后插入一个新节点

11. 删除指定节点 pos 之后的节点

12. 链表的销毁

13. 测试用例

四、源文件展示(链表 · 07c4d36 · 黄灿灿/数据结构 - Gitee.com)

头文件:SList.h

函数部分源文件:SList.c

测试部分文件(同上):text.c

总结

希望这篇文章能帮助读者你更好地理解单链表的概念和操作。如果您有任何疑问或建议,请随时留言交流。

共勉


一、前言

在计算机科学中,链表是一种常见的数据结构,它通过一系列的节点来存储数据元素,其中每个节点包含一个数据字段和一个指向链表中下一个节点的引用。单链表是最简单的链表形式,本文将深入探讨单链表的基本概念、实现步骤及其易错点!帮助大家以最快速度学会单链表!

本文从0到尾,基本涵盖了所有经典问题,所有的资料全都有 (单词注释、链接直达),全部都准备好了!不用您再东找找、四看看了!接下来,我们会先分步讲单独的函数,不想细看的,可以直接跳转至文尾的 “源文件” 处!

一点要求如下:

  • 先要对单链表有概念常识,清楚单链表的链表形式。
  • 仔细思考示例中的实现方法,尤其是能够理解错误示例的错误点!(这是第一遍)
  • 凭借画图和逻辑思考(尽量不要有所参考),自行尝试手撕代码进行单链表的实现(这是第二遍)

二、单链表的概念

单链表由一系列节点组成,每个节点包含两个部分:

  1. 数据域:用于存储实际的数据。
  2. 指针域:存储指向链表中下一个节点的地址。

链表的第一个节点通常被称为“头节点”或“首节点”,而最后一个节点的指针域通常为空(NULL),表明链表的结束。

如果你不理解,可以点击链接跳转至 “单链表动画” (五分钟左右),清楚理解其链表形式后再回来!(【C语言】发明链表的人真是太有才啦_哔哩哔哩_bilibili 或者【动画演示】链表详解及其底层机制 C语言_哔哩哔哩_bilibili)

1. 单链表的特点

  • 动态分配:与数组不同,链表的大小可以在运行时改变,因此可以有效地管理内存资源。
  • 插入和删除操作简便:不需要像数组那样移动大量元素,只需修改相关节点的指针即可完成插入或删除操作。
  • 顺序访问:由于链表中的元素不是连续存储的,因此只能通过遍历的方式访问元素,不能像数组那样随机访问。

2. 单链表的基本操作

单链表的基本操作包括:

  • 初始化
  • 取值
  • 按值查找
  • 插入
  • 删除

3. 画图思想

数据结构最核心的思想是画图!!!这里给出几个样例,可自行导入画图板进行演示:

一个个人演示示例(画图板很实用!):

好了,到此,你应该对单链表有了一定的认识,难的是如何实现,下面我们开始分步讲解 (注意,这里的示例为8种链表中经典的无头单项非循环链表)

选择原因:无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

三、单链表分步详解

1. 函数声明

要实现单链表,首先就要有节点,我们先创建节点,构建我们的数据域指针域,这里的数据域用最简单的整形代替,学会进阶可增加/丰富数据域的内容,使其可以存储更多更丰富发内容!

//Single-linked lists:单链表(SList)
//list:列表#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//node:节点
//next:下一个
//data:数据
//Singly linked list nodes:单链表节点(SLLN)typedef struct SListnode
{int data;struct SListnode* next;
}SLLN;
//等价于:
//struct SListnode
//{
//	int data;
//	struct SListnode* next;
//};
//typedef struct SListnode SLLN;

此处顺便将所有函数声明给出,可看可不看,

  • 看:要注意函数命名,函数较多,切记不要混淆(说实话,这里的函数名不那么恰当,可自行更改)
  • 不看:只要记得有此处的函数声明即可
//找节点
SLLN* lookup(int x);// 动态申请一个节点:Dynamically apply for a node
//SLLN* Da_node(int x);// 单链表打印
void SList_print(SLLN* phead);// 单链表尾插	Tail plugging:尾插
//void SList_tplug(SLLN* phead, int x);//错!
void SList_tplug(SLLN** pphead, int x);//对!// 单链表的头插		Header:头插
void SList_header(SLLN** pphead, int x);// 单链表的尾删		Tail deletion:尾删
void SList_tdel(SLLN** pphead);// 单链表头删		Header deleted:头删
void SList_hdel(SLLN** pphead);// 单链表查找		Find:查找
SLLN* SList_find(SLLN* phead, int x);
//void SList_print_matching(SLLN* phead, int x);//在指定节点 pos 之前插入一个新节点:Inserts a new node before specifying it
void SList_before(SLLN** pphead, SLLN* pos, int x);//删除指定节点 pos :Deletes the specified node		
void SList_del(SLLN** pphead, SLLN* pos);//后:after
//在指定节点 pos 之后插入一个新节点:Inserts a new node after the specified node
void SList_after(SLLN* pos, int x);//删除指定节点 pos 之后的节点:Deletes a node after the specified node
void SList_del2(SLLN* pos);//链表销毁,Destroyed:销毁
void Destroyed(SLLN* phead);
//void Destroyed(SLLN** pphead);或者

函数部分注意引用部分:

#define _CRT_SECURE_NO_WARNINGS 1
//Single-linked lists:单链表
//Singly linked list nodes:单链表节点(SLLN)
//list:列表
//head:头
//tail:尾,尾巴
//temp:临时的#include "SList.h"

2. 打印函数 

好了,下面开始我们的第一个函数——打印函数:

提醒一下:空链表可以打印,空链表是指没有数据节点的链表,也就是说,除了可能存在的头节点外,链表中没有任何数据节点。在空链表中,通常会有一个头节点(有时也称为哨兵节点),这个节点不包含任何数据,它的作用主要是方便对链表进行操作,例如插入和删除等。

  • 空链表本身没有数据节点。
  • 是否包含头节点取决于具体的实现方式。
  • 空链表的特征是链表的最后一个节点(如果有的话)的next指针指向NULL
// 单链表打印
//可以使用二级指针,但没必要,要改变实参用二级,不改变用一级!可以加 const,不建议,会带来一些不必要的麻烦!
void SList_print(SLLN* phead)
{//此处切不可断言(assert),空链表可以打印,此处使用断言会终止程序!//(单链表为空,此处指针(phead)为空。顺序表此处需断言(assert),顺序表是一个结构体,结构体里有一个指针指向一块数组空间,结构体指针为空,程序就走不了了)SLLN* temp = phead;//注意:切不可写成 while (temp->next != NULL),最后一个数据无法打印!//正确写法://while (temp != NULL)while (temp){printf("%d->", temp->data);temp = temp->next;//temp++/++temp,注意:切不可加加!节点地址不能保证连续!}printf("NULL\n");
}

3. 尾插函数(很重要!) 

下面是最为复杂的尾插函数,注意深刻体会注释部分,明白注释中错误部分非常非常重要!!!对应的行数部分注释只有在 “源文件” 处才对应得上!!

//提取一个公共函数:找一个新节点,lookup:查找
//这个函数全部内容来自void SList_tplug(SLLN** pphead, int x)函数的 122-135 行 
SLLN* lookup(int x)
{SLLN* newnode = (SLLN*)malloc(sizeof(SLLN)); // newnode: 新节点if (newnode == NULL) // 检查{perror("malloc fail");return NULL; // 返回 NULL 表示失败}// 新节点初始化为空newnode->data = x;newnode->next = NULL;return newnode;
}虽然是错,但要从中吸取教训!!!总结结论!!!(着重看和理解注释部分!!!)单链表尾插	Tail plugging:尾插
//void SList_tplug(SLLN* phead, int x)
//{
//	//此处切不可断言(assert),链表为空,指针(phead)为空,此处使用断言会终止程序!
//
//	SLLN* newnode = (SLLN*)malloc(sizeof(SLLN));//newnode:新节点
//	if (newnode == NULL)//检查
//	{
//		perror("malloc fail");
//		return;
//	}
//
//	//新节点初始化为空
//	newnode->data = x;
//	newnode->next = NULL;
//
//	if (phead == NULL)
//	{
//		phead = newnode;
//	}
//	else
//	{
//		//找尾,尾插的本质:原尾节点中要存储新的尾节点地址
//		//"正确"写法(相对于此情况,但此情况(函数部分)有错):
//		SLLN* tail = phead;//tail:尾
//		while (tail->next != NULL)
//		{
//			tail = tail->next;
//		}
//		tail->next = newnode;//精华所在!
//
//		//错误写法:
//		//SLLN* tail = phead;//tail:尾
//		//while (tail != NULL)
//		//{
//		//	tail = tail->next;
//		//}
//		//tail = newnode;
//		//原因:函数栈帧知识:tail是局部变量,之后会销毁!
//	}
//}//整个函数错误原因:
请对比:void temp(int *p)						void temp(int *ptr)
{										{*p = 1;									ptr = (int*)malloc(sizeof(int));
}										}
int main()				    和			int main()
{							和			{int x = 0;				和				int* px=NULL;temp(&x);								temp(px);return 0;								return 0;
}										}改变的是int,使用的是int的指针;改变int*要使用int*的地址,int**指针!!!
所以要改变*ptr,使用的是*ptr的指针
即:
int main()
{int* px = NULL;Func(&px);free(px);return 0;
}
正确的函数写法:
void SList_tplug(SLLN** pphead, int x)
{//为了使用 35 行的 lookup 函数将此 122 到 135 行注释掉!改成 137 行的 SLLN* newnode = lookup(x);此处切不可断言(assert),链表为空,指针(phead)为空,此处使用断言会终止程序!//SLLN* newnode = (SLLN*)malloc(sizeof(SLLN));//newnode:新节点//if (newnode == NULL)//检查//{//	perror("malloc fail");//	return;//}新节点初始化为空//newnode->data = x;//newnode->next = NULL;SLLN* newnode = lookup(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾,尾插的本质:原尾节点中要存储新的尾节点地址SLLN* tail = *pphead;//tail:尾while (tail->next != NULL){tail = tail->next;}tail->next = newnode;//精华所在!!!(改变指针,改变结构体成员,用一级指针,函数栈帧的知识要深挖!!!)}
}

4. 头插函数

比较简单,注意调用关系,层次关系,要理解二级指针!动画演示:链表的头插法,数据结构与算法完整代码动画,考研408 期末考试数据结构_哔哩哔哩_bilibili

// 单链表的头插		Header:头插
void SList_header(SLLN** pphead, int x)
{assert(pphead);SLLN* newnode = lookup(x);newnode->next = *pphead;*pphead = newnode;
}

5. 尾删函数

注意链表本身的顺序和节点规律,这里唯一重要的是要清晰当前节点和旁边的两个节点关系(后面很多地方也是一样!!),动画演示:链表的尾插法 完整代码动画版,数据结构与算法。附在线数据结构交互式工具_哔哩哔哩_bilibili

// 单链表的尾删		Tail deletion:尾删
void SList_tdel(SLLN** pphead)
{错误写法://SLLN* tail = *pphead;//while (tail->next != NULL)//{//	tail = tail->next;//}//free(tail);//tail = NULL;//错误原因:此行tail是局部变量,没有把前一个next节点置空,前一个节点是一个结构体,要将前一个结构体节点置空,需要一个结构体的指针!//检查二选一//暴力的检查assert(pphead);assert(*pphead);//或者assert(*pphead!=NULL);//温柔的检查//if (*pphead == NULL)//{//	return;//}if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{//正确写法1:SLLN* temp = NULL;SLLN* tail = *pphead;while (tail->next != NULL){temp = tail;tail = tail->next;}free(tail);tail = NULL;temp->next = NULL;//正确写法2://SLLN* tail = *pphead;//while (tail->next->next != NULL)//{//tail = tail->next;//}//free(tail->next);//tail->next = NULL;}
}

6. 头删函数

比较简单,不进行过多赘述。

// 单链表头删		Header deleted:头删
void SList_hdel(SLLN** pphead)
{//检查二选一//暴力的检查assert(pphead);assert(*pphead);//或者assert(*pphead!=NULL);//温柔的检查//if (*pphead == NULL)//{//	return;//}SLLN* first = *pphead;//first:第一*pphead = first->next;free(first);first = NULL;
}

7. 查找函数

这里本身的查找函数,人是不好观察的,所以又提供了注释部分的查找打印函数,这里需要理解的是更新/迭代的逻辑!

// 单链表查找		Find:查找
SLLN* SList_find(SLLN* phead, int x)
{SLLN* temp = phead;while (temp){if (temp->data == x){return temp;}temp = temp->next;//更新temp}return NULL;
}
//或者使用查找打印函数:
//void SList_print_matching(SLLN* phead, int x)
//{
//	SLLN* temp = phead;
//	int found = 0; // 标记是否找到了匹配的节点
//
//	while (temp)
//	{
//		if (temp->data == x)
//		{
//			printf("%d ", temp->data); // 打印匹配的节点数据
//			found = 1; // 标记找到了匹配的节点
//		}
//		temp = temp->next;
//	}
//
//	if (!found)
//	{
//		printf("No matching elements found.\n"); // 如果没有找到匹配的节点
//	}
//	else
//	{
//		printf("\n"); // 打印换行符,使输出更加清晰
//	}
//}

8. 在指定节点 pos 之前插入一个新节点

注意分情况,因为有一种情况是头插,可以 “偷懒” 。

//在指定节点 pos 之前插入一个新节点:Inserts a new node before specifying it
void SList_before(SLLN** pphead, SLLN* pos, int x)
{assert(pos);assert(pphead);if (pos == *pphead){SList_header(pphead, x);}else{//找pos的前一个位置SLLN* temp = *pphead;while (temp->next != pos){temp = temp->next;}SLLN* newnode = lookup(x);temp->next = newnode;newnode->next = pos;}
}

9. 删除指定节点 pos

注意情况分析!

//删除指定节点 pos :Deletes the specified node		
void SList_del(SLLN** pphead, SLLN* pos)
{assert(pphead);assert(pos);//assert(*pphead);可要可不要if (*pphead == pos){SList_hdel(pphead);}else{//找到pos的前一个位置SLLN* temp = *pphead;while (temp->next != pos){temp = temp->next;}temp->next = pos->next;free(pos);//pos=NULL;没用,因为形参不改变实参(对应text6)}
}

10. 在指定节点 pos 之后插入一个新节点

迭代问题,注意保存下一个节点地址。

//后:after
//在指定节点 pos 之后插入一个新节点:Inserts a new node after the specified node
void SList_after(SLLN* pos, int x)
{//经典错误!!!//assert(pos);//SLLN* newnode = lookup(x);//pos->next = newnode;//newnode->next = pos->next;//在将 pos 的 next 指针指向新节点 newnode 之前,必须先保存 pos 节点的下一个节点的地址,否则这部分链表将丢失。//正确写法:assert(pos);SLLN* newnode = lookup(x);newnode->next = pos->next;//保存 pos 节点的下一个节点的地址pos->next = newnode;//将 pos 的 next 指针指向新节点 newnode
}

11. 删除指定节点 pos 之后的节点

还是注意更新迭代的问题。

//删除指定节点 pos 之后的节点:Deletes a node after the specified node
void SList_del2(SLLN* pos)
{assert(pos);assert(pos->next);//错误写法(不可单独出现)://pos->next = pos->next->next;//正确写法1:SLLN* temp = pos->next;pos->next = pos->next->next;free(temp);temp = NULL;//正确写法2://SLLN* temp = pos->next;//pos->next = temp->next;//free(temp);//temp = NULL;
}

12. 链表的销毁

注意销毁的顺序迭代问题,这里要着重注意,稍有疏忽,很容易造成内存泄漏!

//链表销毁,Destroyed:销毁
void Destroyed(SLLN* phead)
{//经典错误://错误写法1://SLLN* temp = phead;//while (temp)//{//	free(temp);//	temp = temp->next;//}//错误写法2://SLLN* temp = phead;//while (temp)//{//	SLLN* tmp = temp;//	free(temp);//	temp = tmp->next;//}//正确写法:SLLN* temp = phead;while (temp){SLLN* tmp = temp->next;free(temp);temp = tmp;}//phead=NULL;
}
//或者:
//void Destroyed(SLLN** pphead)
//{
//	assert(pphead);
//	SLLN* temp = *pphead;
//	while (temp)
//	{
//		SLLN* tmp = temp->next;
//		free(temp);
//		temp = tmp;
//	}
//
//	*pphead = NULL;
//}

13. 测试用例

 下面是我的个人测试用例,大家可自行采纳,欢迎大家去实践、去测试、去加入自己的想法!

#define _CRT_SECURE_NO_WARNINGS 1
//list:列表
//text:测试#include "SList.h"//第一次测试:
void text1()
{//此处错误对应 SList.c 文件 51 到 119 行,两处错误需同时取消注释,并将正确部分注释掉,可查看错误效果!/*SLLN* plist = NULL;SList_tplug(plist, 1);SList_tplug(plist, 2);SList_tplug(plist, 3);SList_tplug(plist, 4);SList_tplug(plist, 5);*///不改变实参直接传,要改变传地址!//正确写法(要传地址!):SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);//不改变实参直接传,要改变传地址!}//第二次测试:
void text2()
{SLLN* plist = NULL;SList_header(&plist, 1);SList_header(&plist, 2);SList_header(&plist, 3);SList_header(&plist, 4);SList_header(&plist, 5);SList_print(plist);SList_tdel(&plist);SList_print(plist);SList_tdel(&plist);SList_print(plist);SList_tdel(&plist);SList_print(plist);
}//第三次测试:
void text3()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);
}//第四次测试:
void text4()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);//值为2的那个节点 *2SLLN* temp = SList_find(plist, 2);temp->data *= 2;SList_print(plist);}//第五次测试:
void text5()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);// 值为2那个节点  *2SLLN* temp = SList_find(plist, 2);SList_before(&plist, temp, 20);SList_print(plist);}//第六次测试:
void text6()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);// 值为2那个节点  *2SLLN* temp = SList_find(plist, 2);SList_del(&plist, temp);temp = NULL;//此处置空,或者别用即可SList_print(plist);Destroyed(plist);plist = NULL;//或者( 用于void Destroyed(SLLN** pphead); 生效时 !!)://Destroyed(&plist);//plist = NULL;
}int main()
{//每次开一个测试,也可自行写测试内容!//text1();//text2();text3();//text4();//text5();//text6();return 0;
}

四、源文件展示(链表 · 07c4d36 · 黄灿灿/数据结构 - Gitee.com

 在这里,注释对应的行号才准确!

头文件:SList.h

#pragma once
//Single-linked lists:单链表(SList)
//list:列表#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//node:节点
//next:下一个
//data:数据
//Singly linked list nodes:单链表节点(SLLN)typedef struct SListnode
{int data;struct SListnode* next;
}SLLN;
//等价于:
//struct SListnode
//{
//	int data;
//	struct SListnode* next;
//};
//typedef struct SListnode SLLN;//找节点
SLLN* lookup(int x);// 动态申请一个节点:Dynamically apply for a node
//SLLN* Da_node(int x);// 单链表打印
void SList_print(SLLN* phead);// 单链表尾插	Tail plugging:尾插
//void SList_tplug(SLLN* phead, int x);//错!
void SList_tplug(SLLN** pphead, int x);//对!// 单链表的头插		Header:头插
void SList_header(SLLN** pphead, int x);// 单链表的尾删		Tail deletion:尾删
void SList_tdel(SLLN** pphead);// 单链表头删		Header deleted:头删
void SList_hdel(SLLN** pphead);// 单链表查找		Find:查找
SLLN* SList_find(SLLN* phead, int x);
//void SList_print_matching(SLLN* phead, int x);//在指定节点 pos 之前插入一个新节点:Inserts a new node before specifying it
void SList_before(SLLN** pphead, SLLN* pos, int x);//删除指定节点 pos :Deletes the specified node		
void SList_del(SLLN** pphead, SLLN* pos);//后:after
//在指定节点 pos 之后插入一个新节点:Inserts a new node after the specified node
void SList_after(SLLN* pos, int x);//删除指定节点 pos 之后的节点:Deletes a node after the specified node
void SList_del2(SLLN* pos);//链表销毁,Destroyed:销毁
void Destroyed(SLLN* phead);
//void Destroyed(SLLN** pphead);或者

函数部分源文件:SList.c

#define _CRT_SECURE_NO_WARNINGS 1
//Single-linked lists:单链表
//Singly linked list nodes:单链表节点(SLLN)
//list:列表
//head:头
//tail:尾,尾巴
//temp:临时的#include "SList.h"// 单链表打印
//可以使用二级指针,但没必要,要改变实参用二级,不改变用一级!可以加 const,不建议,会带来一些不必要的麻烦!
void SList_print(SLLN* phead)
{//此处切不可断言(assert),空链表可以打印,此处使用断言会终止程序!//(单链表为空,此处指针(phead)为空。顺序表此处需断言(assert),顺序表是一个结构体,结构体里有一个指针指向一块数组空间,结构体指针为空,程序就走不了了)SLLN* temp = phead;//注意:切不可写成 while (temp->next != NULL),最后一个数据无法打印!//正确写法://while (temp != NULL)while (temp){printf("%d->", temp->data);temp = temp->next;//temp++/++temp,注意:切不可加加!节点地址不能保证连续!}printf("NULL\n");
}//提取一个公共函数:找一个新节点,lookup:查找
//这个函数全部内容来自void SList_tplug(SLLN** pphead, int x)函数的 122-135 行 
SLLN* lookup(int x)
{SLLN* newnode = (SLLN*)malloc(sizeof(SLLN)); // newnode: 新节点if (newnode == NULL) // 检查{perror("malloc fail");return NULL; // 返回 NULL 表示失败}// 新节点初始化为空newnode->data = x;newnode->next = NULL;return newnode;
}虽然是错,但要从中吸取教训!!!总结结论!!!(着重看和理解注释部分!!!)单链表尾插	Tail plugging:尾插
//void SList_tplug(SLLN* phead, int x)
//{
//	//此处切不可断言(assert),链表为空,指针(phead)为空,此处使用断言会终止程序!
//
//	SLLN* newnode = (SLLN*)malloc(sizeof(SLLN));//newnode:新节点
//	if (newnode == NULL)//检查
//	{
//		perror("malloc fail");
//		return;
//	}
//
//	//新节点初始化为空
//	newnode->data = x;
//	newnode->next = NULL;
//
//	if (phead == NULL)
//	{
//		phead = newnode;
//	}
//	else
//	{
//		//找尾,尾插的本质:原尾节点中要存储新的尾节点地址
//		//"正确"写法(相对于此情况,但此情况(函数部分)有错):
//		SLLN* tail = phead;//tail:尾
//		while (tail->next != NULL)
//		{
//			tail = tail->next;
//		}
//		tail->next = newnode;//精华所在!
//
//		//错误写法:
//		//SLLN* tail = phead;//tail:尾
//		//while (tail != NULL)
//		//{
//		//	tail = tail->next;
//		//}
//		//tail = newnode;
//		//原因:函数栈帧知识:tail是局部变量,之后会销毁!
//	}
//}//整个函数错误原因:
请对比:void temp(int *p)						void temp(int *ptr)
{										{*p = 1;									ptr = (int*)malloc(sizeof(int));
}										}
int main()				    和			int main()
{							和			{int x = 0;				和				int* px=NULL;temp(&x);								temp(px);return 0;								return 0;
}										}改变的是int,使用的是int的指针;改变int*要使用int*的地址,int**指针!!!
所以要改变*ptr,使用的是*ptr的指针
即:
int main()
{int* px = NULL;Func(&px);free(px);return 0;
}
正确的函数写法:
void SList_tplug(SLLN** pphead, int x)
{//为了使用 35 行的 lookup 函数将此 122 到 135 行注释掉!改成 137 行的 SLLN* newnode = lookup(x);此处切不可断言(assert),链表为空,指针(phead)为空,此处使用断言会终止程序!//SLLN* newnode = (SLLN*)malloc(sizeof(SLLN));//newnode:新节点//if (newnode == NULL)//检查//{//	perror("malloc fail");//	return;//}新节点初始化为空//newnode->data = x;//newnode->next = NULL;SLLN* newnode = lookup(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾,尾插的本质:原尾节点中要存储新的尾节点地址SLLN* tail = *pphead;//tail:尾while (tail->next != NULL){tail = tail->next;}tail->next = newnode;//精华所在!!!(改变指针,改变结构体成员,用一级指针,函数栈帧的知识要深挖!!!)}
}// 单链表的头插		Header:头插
void SList_header(SLLN** pphead, int x)
{assert(pphead);SLLN* newnode = lookup(x);newnode->next = *pphead;*pphead = newnode;
}// 单链表的尾删		Tail deletion:尾删
void SList_tdel(SLLN** pphead)
{错误写法://SLLN* tail = *pphead;//while (tail->next != NULL)//{//	tail = tail->next;//}//free(tail);//tail = NULL;//错误原因:此行tail是局部变量,没有把前一个next节点置空,前一个节点是一个结构体,要将前一个结构体节点置空,需要一个结构体的指针!//检查二选一//暴力的检查assert(pphead);assert(*pphead);//或者assert(*pphead!=NULL);//温柔的检查//if (*pphead == NULL)//{//	return;//}if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{//正确写法1:SLLN* temp = NULL;SLLN* tail = *pphead;while (tail->next != NULL){temp = tail;tail = tail->next;}free(tail);tail = NULL;temp->next = NULL;//正确写法2://SLLN* tail = *pphead;//while (tail->next->next != NULL)//{//tail = tail->next;//}//free(tail->next);//tail->next = NULL;}
}// 单链表头删		Header deleted:头删
void SList_hdel(SLLN** pphead)
{//检查二选一//暴力的检查assert(pphead);assert(*pphead);//或者assert(*pphead!=NULL);//温柔的检查//if (*pphead == NULL)//{//	return;//}SLLN* first = *pphead;//first:第一*pphead = first->next;free(first);first = NULL;
}// 单链表查找		Find:查找
SLLN* SList_find(SLLN* phead, int x)
{SLLN* temp = phead;while (temp){if (temp->data == x){return temp;}temp = temp->next;//更新temp}return NULL;
}
//或者使用查找打印函数:
//void SList_print_matching(SLLN* phead, int x)
//{
//	SLLN* temp = phead;
//	int found = 0; // 标记是否找到了匹配的节点
//
//	while (temp)
//	{
//		if (temp->data == x)
//		{
//			printf("%d ", temp->data); // 打印匹配的节点数据
//			found = 1; // 标记找到了匹配的节点
//		}
//		temp = temp->next;
//	}
//
//	if (!found)
//	{
//		printf("No matching elements found.\n"); // 如果没有找到匹配的节点
//	}
//	else
//	{
//		printf("\n"); // 打印换行符,使输出更加清晰
//	}
//}//在指定节点 pos 之前插入一个新节点:Inserts a new node before specifying it
void SList_before(SLLN** pphead, SLLN* pos, int x)
{assert(pos);assert(pphead);if (pos == *pphead){SList_header(pphead, x);}else{//找pos的前一个位置SLLN* temp = *pphead;while (temp->next != pos){temp = temp->next;}SLLN* newnode = lookup(x);temp->next = newnode;newnode->next = pos;}
}//删除指定节点 pos :Deletes the specified node		
void SList_del(SLLN** pphead, SLLN* pos)
{assert(pphead);assert(pos);//assert(*pphead);可要可不要if (*pphead == pos){SList_hdel(pphead);}else{//找到pos的前一个位置SLLN* temp = *pphead;while (temp->next != pos){temp = temp->next;}temp->next = pos->next;free(pos);//pos=NULL;没用,因为形参不改变实参(对应text6)}
}//后:after
//在指定节点 pos 之后插入一个新节点:Inserts a new node after the specified node
void SList_after(SLLN* pos, int x)
{//经典错误!!!//assert(pos);//SLLN* newnode = lookup(x);//pos->next = newnode;//newnode->next = pos->next;//在将 pos 的 next 指针指向新节点 newnode 之前,必须先保存 pos 节点的下一个节点的地址,否则这部分链表将丢失。//正确写法:assert(pos);SLLN* newnode = lookup(x);newnode->next = pos->next;//保存 pos 节点的下一个节点的地址pos->next = newnode;//将 pos 的 next 指针指向新节点 newnode
}//删除指定节点 pos 之后的节点:Deletes a node after the specified node
void SList_del2(SLLN* pos)
{assert(pos);assert(pos->next);//错误写法(不可单独出现)://pos->next = pos->next->next;//正确写法1:SLLN* temp = pos->next;pos->next = pos->next->next;free(temp);temp = NULL;//正确写法2://SLLN* temp = pos->next;//pos->next = temp->next;//free(temp);//temp = NULL;
}//链表销毁,Destroyed:销毁
void Destroyed(SLLN* phead)
{//经典错误://错误写法1://SLLN* temp = phead;//while (temp)//{//	free(temp);//	temp = temp->next;//}//错误写法2://SLLN* temp = phead;//while (temp)//{//	SLLN* tmp = temp;//	free(temp);//	temp = tmp->next;//}//正确写法:SLLN* temp = phead;while (temp){SLLN* tmp = temp->next;free(temp);temp = tmp;}//phead=NULL;
}
//或者:
//void Destroyed(SLLN** pphead)
//{
//	assert(pphead);
//	SLLN* temp = *pphead;
//	while (temp)
//	{
//		SLLN* tmp = temp->next;
//		free(temp);
//		temp = tmp;
//	}
//
//	*pphead = NULL;
//}

测试部分文件(同上):text.c

#define _CRT_SECURE_NO_WARNINGS 1
//list:列表
//text:测试#include "SList.h"//第一次测试:
void text1()
{//此处错误对应 SList.c 文件 51 到 119 行,两处错误需同时取消注释,并将正确部分注释掉,可查看错误效果!/*SLLN* plist = NULL;SList_tplug(plist, 1);SList_tplug(plist, 2);SList_tplug(plist, 3);SList_tplug(plist, 4);SList_tplug(plist, 5);*///不改变实参直接传,要改变传地址!//正确写法(要传地址!):SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);//不改变实参直接传,要改变传地址!}//第二次测试:
void text2()
{SLLN* plist = NULL;SList_header(&plist, 1);SList_header(&plist, 2);SList_header(&plist, 3);SList_header(&plist, 4);SList_header(&plist, 5);SList_print(plist);SList_tdel(&plist);SList_print(plist);SList_tdel(&plist);SList_print(plist);SList_tdel(&plist);SList_print(plist);
}//第三次测试:
void text3()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);SList_hdel(&plist);SList_print(plist);
}//第四次测试:
void text4()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);//值为2的那个节点 *2SLLN* temp = SList_find(plist, 2);temp->data *= 2;SList_print(plist);}//第五次测试:
void text5()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);// 值为2那个节点  *2SLLN* temp = SList_find(plist, 2);SList_before(&plist, temp, 20);SList_print(plist);}//第六次测试:
void text6()
{SLLN* plist = NULL;SList_tplug(&plist, 1);SList_tplug(&plist, 1);SList_tplug(&plist, 2);SList_tplug(&plist, 3);SList_tplug(&plist, 4);SList_tplug(&plist, 5);SList_print(plist);// 值为2那个节点  *2SLLN* temp = SList_find(plist, 2);SList_del(&plist, temp);temp = NULL;//此处置空,或者别用即可SList_print(plist);Destroyed(plist);plist = NULL;//或者( 用于void Destroyed(SLLN** pphead); 生效时 !!)://Destroyed(&plist);//plist = NULL;
}int main()
{//每次开一个测试,也可自行写测试内容!//text1();//text2();text3();//text4();//text5();//text6();return 0;
}

总结

单链表是一种灵活的数据结构,适用于多种应用场景。理解和掌握了单链表的基本操作后,可以更高效地使用它们来解决实际问题。在后续的文章中,我们还将继续探索单链表的其他高级特性和操作。

希望这篇文章能帮助读者你更好地理解单链表的概念和操作。如果您有任何疑问或建议,请随时留言交流。

共勉

相关文章:

【C语言】最详细的单链表(两遍包会!)

&#x1f984;个人主页:小米里的大麦-CSDN博客 &#x1f38f;所属专栏:C语言数据结构_小米里的大麦的博客-CSDN博客 &#x1f381;代码托管:黄灿灿/数据结构 (gitee.com) ⚙️操作环境:Visual Studio 2022 目录 一、前言 二、单链表的概念 1. 单链表的特点 2. 单链表的基本…...

QT:VS2019 CMake编译CEF

CEF介绍 CEF作为一个基于Chromium的开源Web浏览器控件&#xff0c;为第三方应用提供了强大的嵌入浏览器支持。其多平台支持、HTML5特性、自定义能力以及多进程架构等特性&#xff0c;使得CEF在浏览器开发、桌面应用、开发工具以及自动化测试等领域得到了广泛应用。 多平台支持…...

day31(8/19)——静态文件共享、playbook

目录 一、ansible模块 script模块 copy模块 使用command模块下载 nfs-utils rpcbind 在被控制的主机上添加static目录&#xff0c;并创建test文件 command模块 service模块 二、playbook 三、playbook编排vsftpd 1、安装 2、卸载 3、启动服务 4、修改配置文件设置不…...

白骑士的C#教学实战项目篇 4.4 游戏开发

系列目录 上一篇&#xff1a;白骑士的C#教学实战项目篇 4.3 Web开发 在这一部分&#xff0c;我们将探索如何使用 Unity 和 C# 开发游戏。游戏开发结合了编程、图形设计和创意&#xff0c;既充满挑战又充满乐趣。通过这一节的学习&#xff0c;您将了解游戏引擎的基础知识&#…...

在Vue工程中开发页面时,发现页面垂直方向出现两个滚动条的处理

在Vue工程中开发页面时&#xff0c;发现页面垂直方向出现两个滚动条 最近在开发页面时&#xff0c;发现页面多了两个滚动条&#xff0c;如图&#xff1a; 原因&#xff1a; 当一个页面的内容高度大于屏幕的高度时就会出现滚动条。一般情况下当一个页面高度大于屏幕高度时&a…...

【C++初阶】:C++入门篇(一)

文章目录 前言一、C命名空间1.1 命名空间的定义1.2 命名空间的使用 二、C的输入和输出2.1 cin和cout的使用 三、缺省参数3.1 缺省参数的分类 四、函数重载4.1 函数重载概念及其条件4.2 C支持函数重载原理 -- 名字修饰 前言 C是在C语言的基础之上&#xff0c;增加了一些面向对象…...

【JAVA CORE_API】Day14 Collection、Iterator、增强for、泛型、List、Set

Collection接口及常用方法 Collection<Object> collection new ArrayList();&#xff1a;实例化ArrayList集合对象&#xff1b; collectionName.add(Object obj);&#xff1a;在集合中增加元素&#xff1b; int sizeName collectionName.size();&#xff1a;获取集合…...

Go更换国内源配置环境变量

背景 要在中国境内下载和使用Go编程语言的包&#xff0c;可以使用国内的Go模块代理来加速下载速度。以下是一些常见的国内Go模块代理源以及如何切换到这些源的方法&#xff1a; 常见国内Go模块代理源 七牛云&#xff08;Qiniu&#xff09; https://goproxy.cn 阿里云&#xff0…...

澎湃认证显实力,浪潮信息存储兼容新篇章

浪潮信息在存储技术兼容性领域取得新突破&#xff0c;其集中式存储HF/AS系列与长擎安全操作系统24强强联合&#xff0c;成功完成澎湃技术认证。此次合作不仅验证了双方产品的无缝对接能力&#xff0c;更体现了浪潮信息在推动全产业链共建共享方面的坚定决心。 浪潮信息澎湃技术…...

Leetcode 3255. Find the Power of K-Size Subarrays II

Leetcode 3255. Find the Power of K-Size Subarrays II 1. 解题思路2. 代码实现 题目链接&#xff1a;3255. Find the Power of K-Size Subarrays II 1. 解题思路 这一题是题目3254的进阶版&#xff0c;其实主要就是增加了算法复杂度。 整体上来说的话思路还是一个分段的思…...

Kotlin学习02-变量、常量、整数、浮点数、操作符、元组、包、导入

变量、常量、整数、浮点数、操作符、元组、包、导入 Book.kt package com.wujialiang.packclass Book {var title: String "Hello" }val PI 3.14; val E 2.178;Main.kt //引入包 //import com.wujialiang.pack.Book; import com.wujialiang.pack.*; //重命名导…...

C++的模板简介

文章目录 一、前言二、函数模板&#xff08;Function Template&#xff09;三、类模板&#xff08;Class Template&#xff09;四、变参模板&#xff08;Variadic Template&#xff09;五、模板的递归与元编程六、模板的局限与陷阱七、常用模板的实例八、C20 的概念&#xff08…...

树莓派5 笔记25:第一次启动与配置树莓派5_8G

今日继续学习树莓派5 8G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 与 python 版本如下&#xff1a; 今日购得了树莓派5_8G版本&#xff0c;性能是同运…...

Melittin 蜂毒肽;GIGAVLKVLT TGLPALISWI KRKRQQ

【Melittin 蜂毒肽 简介】 蜂毒肽&#xff08;Melittin&#xff09;是蜜蜂毒液中的主要活性成分&#xff0c;由26个氨基酸组成&#xff0c;具有强碱性&#xff0c;易溶于水&#xff0c;是已知抗炎性最强的物质之一。蜂毒肽具有多种生物学、药理学和毒理学作用&#xff0c;包括…...

day32

更新源 cd /etc/apt/ sudo cp sources.list sources.list.save 将原镜像备份 sudo vim sources.list 将原镜像修改成阿里源/清华源&#xff0c;如所述 阿里源 deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiver…...

【clickhouse】 使用 SQLAlchemy 连接 ClickHouse 数据库的完整指南

我听见有人猜 你是敌人潜伏的内线 和你相知多年 我确信对你的了解 你舍命救我画面 一一在眼前浮现 司空见惯了鲜血 你忘记你本是娇娆的红颜 感觉你我彼此都那么依恋 &#x1f3b5; 许嵩《内线》 ClickHouse 是一款非常高效的开源列式数据库&#xff0c;因…...

按键收集单击,双击和长按

按键收集单击,双击和长按 引言 在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。 一次按键收集&#xff0c; 都是在一个按键收集周…...

进程的异常终止

进程的异常终止 进程收到了某些信号&#xff0c;他杀 进程自己调用abort函数&#xff0c;产生了SIGABRT(6)信号&#xff0c;自杀 进程的最后一个线程收到了"取消"操作&#xff0c;并且做出响应 如果进程是异常结束的&#xff0c;atexit\on_exit它们事先注册的遗言…...

并发编程 | Future是如何优化程序性能

在初识Future一文中介绍了Future的核心方法。本文中接着介绍如何用Future优化我们的程序性能。 在此之前&#xff0c;有必要介绍Future接口的一个实现类FutureTask。 FutureTask介绍 FutureTask继承结构 首先我们看一下FutureTask的继承结构&#xff1a; public class Futur…...

Oracle笔记

一、 如何解决 sqlplus 无法使用退格键和方向键 .bashrc 中添加如下内容&#xff0c;解决 退格键 stty erase ^h 安装 rlwap 后&#xff0c;执行如下命令可解决 方向键 rlwap sqlplus 二、 都有哪些备份数据到工具 三、 谈谈 你对 oracle 中实例和数据库的理解 数据库是一…...

LVS+Keepalived 双机热备

LVSKeepalived 双机热备 Keepalived案例分析Keepalived工具介绍Keepalived工具介绍一、功能特点 一、理解Keepalived实现原理实验报告资源列表一、安装keepalived以及ipvsadm Keepalived案例分析 企业应用中&#xff0c;单台服务器承担应用存在单点故障的危险单点故障一旦发生…...

Web Image scr图片从后端API获取基本实现

因系统开发中需求&#xff0c;会有页面显示图片直接从后端获取后显示&#xff0c;代码如下&#xff1a; 后端&#xff1a; /*** 获取图片流* param response* param fileName*/RequestMapping(value"getImgStream",method RequestMethod.GET)public void getImgStr…...

2024音频剪辑指南:探索四大高效工具!

音频剪辑不仅仅是技术活&#xff0c;更是一种艺术创作&#xff0c;它能够让声音更加生动、更具感染力。今天&#xff0c;我们就来探索几款优秀的音频剪辑工具。 福昕音频剪辑 链接&#xff1a;www.pdf365.cn/foxit-clip/ 福昕音频剪辑是一款界面简洁、操作直观的音频编辑软件…...

“CSS”第一步——WEB开发系列13

CSS (Cascading Style Sheets&#xff0c;层叠样式表&#xff09;&#xff0c;是一种用来为结构化文档&#xff08;如 HTML 文档或 XML 应用&#xff09;添加样式&#xff08;字体、间距和颜色等&#xff09;的计算机语言&#xff0c;CSS 文件扩展名为 .css。 一、什么是 CSS&a…...

IEEE802网络协议和标准

IEEE802网络协议和标准 802委员会IEEE 802介绍现有标准 IEEE 802.3介绍物理媒介类型MAC子层与LLC子层主要内容通讯标准POE供电标准802.3af、802.3at、802.3btIEEE802.3af的工作过程&#xff1a;IEEE802.3af主要供电参数&#xff1a;IEEE802.3af的分级参数&#xff1a;为什么会有…...

vulnhub靶机 DC-9(渗透测试详解)

一、靶机信息收集 1、靶机下载 https://download.vulnhub.com/dc/DC-9.zip 2、靶机IP扫描 3、探测靶机主机、端口、服务版本信息 4、靶机目录扫描 二、web渗透测试 1、访问靶机IP 查看页面功能点&#xff0c;发现一个搜索框和登录框 2、测试一下是否存在sql注入 查看当前数…...

javaweb的新能源充电系统pf

TOC springboot339javaweb的新能源充电系统pf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不可跨域…...

如何在桌面同时展示多个窗口

一、实现2分屏显示 win箭头 二、实现3分屏显示 1. 在实现2分屏显示的基础上&#xff0c;再次点击箭头图标&#xff0c;这次选择屏幕的上方或下方。 2. 点击后&#xff0c;第三个窗口将会出现在你选择的区域。现在&#xff0c;你可以在三个窗口之间自由切换&#xff0c;提高工…...

The Sandbox 游戏制作教程(第 5 部分):创建基于分类的系统

欢迎回到我们的系列&#xff0c;我们将记录 The Sandbox Game Maker 的 “On-Equip”&#xff08;装备&#xff09;功能的多种用途。 如果你刚加入 The Sandbox&#xff0c;装备功能是 “可收集组件”&#xff08;Collectable Component&#xff09;中的一个多功能工具&#x…...

HTML浏览器缓存(Browser Cache)

介绍&#xff1a; 浏览器缓存是Web缓存中最直接、最常见的一种形式。当浏览器首次请求某个资源时&#xff0c;如果服务器响应中包含了缓存控制指令&#xff08;如Cache-Control、Expires等&#xff09;&#xff0c;浏览器就会将这些资源存储在本地缓存中。后续请求相同资源时&a…...