前面解决了顺序表是如何工作的,并用代码实现顺序表。
用顺序表的好处有:
- 
动态内存管理
 - 
高效随机访问
 - 
简化数据操作
顺序表搞定后,接下来就开始单链表的运用顺序表
 
一、单链表的定义
单链表是一种线性数据结构 ,它通过指针 将一个个 "节点"(类比火车车厢)串联起来,形成一个有序的序列。每个节点只存储下一个节点的地址 ,因此它的遍历方向是单向的(只能从 "头" 走到 "尾",不能反向)。
二、单链表的结构组成
单链表的节点结构是单链表的核心组成单元:
            
            
              c
              
              
            
          
          typedef int SLTNode; //将用到的数据用SLTNode来定义,方便类型的修改
typedef struct SListNode
{
    SLTNode data;               // 数据域:存储具体数据(类比"车厢里的货物")
    struct SListNode* next; // 指针域:存储下一个节点的地址(类比"车厢之间的连接钩")
}SLTNode; //将修改节点的名称,节省代码量
        typedef (数据类型) SLTNode:可以直接修改类型,方便数据域的运用。- 数据域(
data):存储节点的实际数据(比如整数、字符串、对象等),对应 "车厢里的货物"。 - 指针域(
next) :存储下一个节点的地址,通过它将所有节点 "链" 在一起,对应 "车厢之间的连接钩"。 
三、单链表的核心特性
结合 "火车" 类比,可以更清晰地理解它的特性:
- 
单向性
就像火车只能 "从车头到车尾" 单向行驶,单链表的节点只能通过
next指针从前往后遍历,无法直接从后一个节点跳回前一个节点(除非额外记录前驱节点地址)。 - 
动态性
单链表的节点是动态分配内存 的(运行时才创建 / 销毁),不像数组需要预先分配固定大小的空间。这意味着单链表的长度可以灵活增减(类比火车可以随时加车厢、减车厢)。
 - 
头节点与空链表
- 头节点 :单链表的 "起始节点",我们通常用一个指针(如 
struct SListNode* head)指向它,类比 "火车头"。 - 空链表 :当 
head == NULL时,链表中没有任何节点,类比 "没有车厢的空火车"。
在链表里,"火车"是这样子的

 
 - 头节点 :单链表的 "起始节点",我们通常用一个指针(如 
 
四、单链表的基本操作(入门必懂)
理解结构后,可以通过这些操作进一步掌握它:
| 操作 | 功能类比 | 实现思路 | 
|---|---|---|
| 初始化 | 准备一列空火车 | 将头指针 head 置为 NULL。 | 
| 头插法(新增) | 在火车头前加一节车厢 | 新建节点 → 新节点的 next 指向原头节点 → 头指针 head 指向新节点。 | 
| 尾插法(新增) | 在火车尾加一节车厢 | 遍历到最后一个节点 → 最后一个节点的 next 指向新节点。 | 
| 遍历 | 从车头到车尾检查每节车厢 | 从 head 开始,循环访问当前节点的 data,再通过 next 跳转到下一个节点,直到 next == NULL。 | 
| 查找 | 在火车里找某节装特定货物的车厢 | 遍历链表,逐个对比节点的 data,找到则返回节点地址,否则返回 NULL。 | 
| 删除 | 移除某节车厢 | 找到目标节点的前驱节点 → 前驱节点的 next 指向目标节点的 next → 释放目标节点的内存。 | 
五、链表打印(基础)
在一个给定的链表结构中,如何实现节点从头到尾的打印?
1.1头文件
            
            
              C
              
              
            
          
          #pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* Next;
}SLTNode;
//打印单链表的头函数
void SLTPrint(SLTNode* Phead);
        1.2函数实现
            
            
              C
              
              
            
          
          #define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void SLTPrint(SLTNode* Phead)
{
	while (Phead)
	{
		//打印链表中每一个数据
		printf("%d->", Phead->data);
		//遍历每个车厢
		Phead = Phead->Next;
	}
	printf("NULL\n");
}
        1.3测试
            
            
              C
              
              
            
          
          #define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void SLTTest01()
{
	//手动创建每个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	//将节点中的数据给赋值
	node1->data = 1;
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;
	//让每个节点相互连接
	node1->Next = node2;
	node2->Next = node3;
	node3->Next = node4;
	node4->Next = NULL;
	//创建一个指针,指向列车的开头
	SLTNode* Phead = node1;
	//传入列车的开头,然后打印单链表
	SLTPrint(Phead);
}
int main()
{
	SLTTest01();
	return 0;
}
        打印的结果为:
1->2->3->4->NULL
        六、尾插实现
形式如下:
            
            
              C
              
              
            
          
          void SLTPopBack(SLTNode** PPhead);
SLTPushBack(&Phead, 2);
        单链表的尾插,也就是让一个创建节点放在末尾节点的后面,但有两种情况
- 情况1:链表为空
 - 情况2:链表中有多个节点
 - 要使用头插和尾插,首先要实现节点的创建
 
1.1创建节点
            
            
              C
              
              
            
          
          //创建节点
SLTNode* SLTByNode(SLTDataType x)
{
	//手动申请空间
	SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (NewNode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	NewNode->data = x;
	NewNode->Next = NULL;
	return NewNode;
}
        1.2情况1:无节点
遇到情况1时,可以先画图进行分析

此时的链表为空,则要给链表增加节点,使新的空间成为首个节点
代码如下:
            
            
              C
              
              
            
          
          //创建节点
SLTNode* NewNode = SLTByNode(x);
//情况1(只有一个节点):若无节点,申请空间给链表
if (*PPhead == NULL)
{
	*PPhead = NewNode;
}
        1.3情况2,有节点
情况2如下图所示

代码如下:
            
            
              C
              
              
            
          
          //情况2(有多个节点):创建一个指针指向开头,往下遍历直到空指针停止
else
{
	SLTNode* Ptail = *PPhead;
	while (Ptail->Next)
	{
		Ptail = Ptail->Next;
	}
	Ptail->Next = NewNode;
}
        七、头插
1.1形式:
            
            
              C
              
              
            
          
          void SLTPushFront(SLTNode** PPhead, SLTDataType x);
SLTPushFront(&Phead, 2);
        1.2头插实现

头文件
            
            
              C
              
              
            
          
          /*头文件*/
void SLTPushFront(SLTNode** PPhead, SLTDataType x);
        测试
            
            
              C
              
              
            
          
          /*test文件*/
SLTPushFront(&Phead, 2);
        函数实现
            
            
              C
              
              
            
          
          /*函数实现*/
void SLTPushFront(SLTNode** PPhead, SLTDataType x)
{
	//判断是否为空链表
	assert("PPhead");
	assert("*PPhead");
	//创建新节点
	SLTNode* NewNode = SLTByNode(x);
	//将空间置于前面
	NewNode->Next = *PPhead;
	//把指针指向新空间
	*PPhead = NewNode;
}
        八、尾删
1.1形式
            
            
              C
              
              
            
          
          void SLTPopBack(SLTNode** PPhead);
SLTPopBack(&Phead);
        在进行尾删时会有两种情况
- 只有一个节点
 - 有多个节点
 
1.2情况1:一个节点

            
            
              C
              
              
            
          
          //情况1:只有一个节点
if ((*PPhead)->Next == NULL)
{
	//把唯一的节点删除
	free(*PPhead);
	*PPhead = NULL;
}
        - ->的优先级比
*高,必须用大括号括起来 
1.3情况2:多个节点

            
            
              C
              
              
            
          
          //情况2:有多个节点
else
{
	//设立两个指针,一个指向结尾,一个指向结尾前一项
	SLTNode* Ptail = *PPhead;
	SLTNode* Ptailadd = *PPhead;
	//让指针遍历到结尾
	while (Ptail->Next)
	{
		Ptailadd = Ptail;
		Ptail = Ptail->Next;
	}
	//释放结尾
	free(Ptail);
	Ptail = NULL;
	Ptailadd->Next = NULL;
}
        1.4总合
            
            
              C
              
              
            
          
          //尾删
void SLTPopBack(SLTNode** PPhead);
//实现函数
//尾删
void SLTPopBack(SLTNode** PPhead)
{
	assert(*PPhead && PPhead);
	//情况1:只有一个节点
	if ((*PPhead)->Next == NULL)
	{
		//把唯一的节点删除
		free(*PPhead);
		*PPhead = NULL;
	}
	//情况2:有多个节点
	else
	{
		//设立两个指针,一个指向结尾,一个指向结尾前一项
		SLTNode* Ptail = *PPhead;
		SLTNode* Ptailadd = *PPhead;
		//让指针遍历到结尾
		while (Ptail->Next)
		{
			Ptailadd = Ptail;
			Ptail = Ptail->Next;
		}
		//释放结尾
		free(Ptail);
		Ptail = NULL;
		Ptailadd->Next = NULL;
	}
}
//test文件
SLTPopBack(&Phead);
        九、头删
1.1形式
            
            
              C
              
              
            
          
          void SLTPopFront(SLTNode** PPhead);
SLTPopFront(&Phead)
        1.2头删实现
头删相比尾删就非常简洁,且只有一种情况:不仅能判断一个节点,也能判断多个节点
            
            
              C
              
              
            
          
          //头删
void SLTPopFront(SLTNode** PPhead)
{
	//保证传的不能为空
	assert(*PPhead && PPhead);
	//让新的指针指向节点的下一个节点
	SLTNode* Ptail = (*PPhead)->Next;
	//把首元素删除
	free(*PPhead);
	//将头指针指向上面创建好的新指针
	*PPhead = Ptail;
}
        十、查找
1.1形式
            
            
              C
              
              
            
          
          SLTNode* SLTFind(SLTNode* Phead, SLTDataType x);
SLTNode* find = SLTFind(Phead, 3);
        查找元素时,需要一个变量来接收返回值,思路如下
- 只查找不会改变链表中的数据,传变量即可
 - 建个指针指向形参,若后面指向空时,还可以再建个指针继续查找
 
1.2查找实现
            
            
              C
              
              
            
          
          //查找
SLTNode* SLTFind(SLTNode* Phead, SLTDataType x)
{
	SLTNode* Ptail = Phead;
	while (Ptail)
	{
		if (Ptail->Next == x)
		{
			//找到返回对应的指针
			return Ptail;
		}
		Ptail = Ptail->Next;
	}
	//没找到返回空
	return NULL;
}
        在测试文件时,如下:
            
            
              C
              
              
            
          
          //查找
SLTNode* Find = SLTFind(Phead, 3);
if (Find == NULL)
{
	printf("没找到!!\n");
}
else
{
	printf("找到了!!\n");
}
        - 单链表中有3的话则打印找到,相反就没找到
 
十一、指定位置头插(配合查找)
1.1形式
            
            
              C
              
              
            
          
          SLTNode* SLTInsert(SLTNode** PPhead, SLTNode* pos, SLTDataType x);
SLTInsert(&Phead, find, 4);
        - 其中pos是查找函数返回的地址,也就是返回的指定位置
 
1.2指定位置头插实现
头插需要配合查找进行,思路如下:
- 创建新的节点
 - 创建指针指向地址
 - 遍历链表,当指针指到形参pos(Find)时,则找到
 - 让指针指向新节点
 - 新节点的Next指向pos
 
            
            
              C
              
              
            
          
          //在指定位置之前插入数据
SLTNode* SLTInsert(SLTNode** PPhead, SLTNode* pos, SLTDataType x)
{
	assert(PPhead && *PPhead);
	SLTNode* NewNode = SLTByNode(x);
	
	//情况1:当插到首节点之前
	if (*PPhead == pos)
	{
		SLTPushBack(PPhead, x);
	}
	//情况2:插到其他地方之前
	else
	{
		SLTNode* Ptail = *PPhead;
		while (Ptail->Next != pos)
		{
			Ptail = Ptail->Next;
		}
		NewNode->Next = pos;
		Ptail->Next = NewNode;
	}
}
        形参中的pos其实是查找中的Find,使用时两者配合使用
十二、指定位置尾插(配合查找)
1.1形式
与指定位置头插一样,需要配合查找函数,
            
            
              C
              
              
            
          
          //在指定位置之后插入数据
SLTNode* SLTInserAfter(SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
SLTInserAfter(find1, 5);
        1.2指定位置尾插实现
- 用查找函数,查找想插入的指定位置
 
- 不需要传入链表,可直接用查找到的地址
 - 创建节点,节点指向查找的地址的下一个节点
 - 用查找节点指向新建立的节点
 
            
            
              C
              
              
            
          
          //在指定位置之后插入数据
SLTNode* SLTInserAfter(SLTNode* pos, SLTDataType x)
{
	SLTNode* NewNode = SLTByNode(x);
	
	//新节点指向查找的下一个节点
	NewNode->Next = pos->Next;
	//查找的节点指向新节点
	pos->Next = NewNode;
}
        十三、删除指定节点
需要与查找函数一起使用,且有两种情况:头删和删其他
1.1形式
            
            
              C
              
              
            
          
          //删除节点
void SLTErase(SLTNode** PPhead, SLTNode* pos);
SLTErase(&Phead, find2);
        1.2实现

            
            
              C
              
              
            
          
          //删除节点
void SLTErase(SLTNode** PPhead, SLTNode* pos)
{
	assert(PPhead && *PPhead);
	assert(pos);
	//情况1:删除头节点
	if (pos == *PPhead)
	{
		//头删函数
		SLTPopFront(PPhead);
	}
	else
	{
		SLTNode* Ptail = *PPhead;
		//情况2:删除其他节点
		while (Ptail->Next != pos)
		{
			Ptail = Ptail->Next;
		}
		Ptail->Next = pos->Next;
		free(pos);
		pos = NULL;
	}
}
        十四、删除指定位置之后的节点
删除指定位置之后的节点只需要传查找函数的find即可
1.1形式
            
            
              C
              
              
            
          
          //删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos);
//删除指定位置之后的节点
SLTEraseAfter(find3);
        1.2实现

            
            
              C
              
              
            
          
          //删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->Next);
	//定义一个指针指向要删除的节点
	SLTNode* del = pos->Next;
	//pos del del->next
	//让删除节点的前一个节点指向删除节点的后一个节点
	pos->Next = del->Next;
	//释放删除的节点并指向空
	free(del);
	del = NULL;
}
        十五、销毁链表
在使用完链表时,就要销毁链表
1.1形式
            
            
              C
              
              
            
          
          //销毁链表(销毁一个一个的节点)
void SLTDesTroy(SLTNode** PPhead);
        1.2实现
            
            
              C
              
              
            
          
          //销毁链表(销毁一个一个的节点)
void SLTDesTroy(SLTNode** PPhead)
{
	assert(PPhead && *PPhead);
	SLTNode* Pcur = *PPhead;
	while (Pcur)
	{
		SLTNode* next = Pcur->Next;
		free(Pcur);
		//让销毁节点的指针指向要销毁下一个的节点
		Pcur = next;
		//当Pcur为空,就跳出循环
	}
	*PPhead = NULL;
}