一、单链表
单链表(Singly Linked List) 是一种最基础的线性表,它不使用连续的内存空间存储数据,而是通过指针把一个个独立的节点串联起来,形成一条链式结构。
1.核心结构
单链表由一个个节点(Node) 组成,每个节点包含两部分:
- 数据域:存放当前节点的数据(如数字、字符、结构体等)
- 指针域:存放下一个节点的地址,用来指向下一个节点
节点结构:
数据 data
指针 next
2. 整体结构
整个链表有一个头指针(Head),指向第一个节点;最后一个节点的指针指向 NULL(空地址),表示链表结束。
头指针 → 节点1 → 节点2 → 节点3 → ... → 尾节点 → NULL
3.关键特点
- 不占用连续内存:可以灵活利用零散空间
- 长度可动态变化:插入、删除节点不需要移动大量元素
- 只能单向遍历:只能从前往后找,不能回头
- 访问效率低:不能像数组那样直接按下标随机访问,必须从头遍历
- 插入 / 删除高效:只需要修改指针指向,时间复杂度 O (1)(已知位置时)
二、单链表实现
1. 定义结点结构
c
// 给 int 类型起别名 SLTDataType
// 好处:以后要修改链表存储的数据类型(比如改成 char、float),只改这一行即可,不用到处修改
typedef int SLTDataType;
// 定义单链表的节点结构体
// 每个节点包含:数据域 + 指针域
typedef struct SListNode
{
// 数据域:存储当前节点的数据
SLTDataType data;
// 指针域:指向**下一个节点**的地址
// 因为下一个节点也是 SListNode 类型,所以用 struct SListNode*
struct SListNode* next;
}SLTNode; // SLTNode 是结构体 SListNode 的别名,方便使用
SLTDataType:给数据类型起别名,方便修改
SLTNode:单链表节点结构体
data:存数据
next:指向下一个节点,串联整个链表
2. 单链表可以实现的功能
放在SList.h文件,声明函数
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int SLTDataType;
// 单链表节点结构体
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//========================= 基础核心函数 =========================//
// 1. 创建新节点(底层支持函数)
SLTNode* BuySListNode(SLTDataType x);
// 2. 打印链表
void SListPrint(SLTNode* phead);
// 3. 销毁链表
void SListDestroy(SLTNode** pphead);
//========================= 基础增删函数 =========================//
// 4. 尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
// 5. 头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
// 6. 尾删
void SListPopBack(SLTNode** pphead);
// 7. 头删
void SListPopFront(SLTNode** pphead);
//========================= 查找与访问 =========================//
// 8. 查找节点
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 9. 判断链表是否为空
bool SListEmpty(SLTNode* phead);
// 10. 获取链表节点个数
int SListSize(SLTNode* phead);
// 11. 返回第一个节点的数据
SLTDataType SListFront(SLTNode* phead);
// 12. 返回最后一个节点的数据
SLTDataType SListBack(SLTNode* phead);
//========================= 指定位置操作 =========================//
// 13. 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 14. 在 pos 位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
// 15. 删除pos位置节点
void SListErase(SLTNode** pphead, SLTNode* pos);
// 16. 删除pos的下一个的结点
void SLTEraseAfter(SLTNode* pos);
//========================= 按值删除 =========================//
// 17. 删除第一个值为 x 的节点
void SListRemove(SLTNode** pphead, SLTDataType x);
// 18. 删除所有值为 x 的节点
void SListRemoveAll(SLTNode** pphead, SLTDataType x);
//========================= 高级算法 =========================//
// 19. 链表反转(三指针)
void SListReverse(SLTNode** pphead);
// 20. 查找倒数第 k 个节点
SLTNode* SListFindKthFromTail(SLTNode* phead, int k);
// 21. 合并两个有序链表
SLTNode* SListMerge(SLTNode* l1, SLTNode* l2);
三、函数实现
函数的定义放在SList.c
1.创建新节点
c
// 1. 创建新节点
// 功能:向内存申请一个新的链表节点,并初始化数据和指针
// 参数:x - 要存入新节点的数据
// 返回值:返回创建好的新节点的地址
SLTNode* BuySListNode(SLTDataType x)
{
// 申请一块和链表节点大小一样的内存空间
// malloc 申请内存后返回 void* 类型,需要强制类型转换为 SLTNode*
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
// 检查 malloc 是否申请内存成功
// 如果申请失败(内存不足等原因),NewNode 会是 NULL
if (NewNode == NULL)
{
// 打印错误信息(系统会提示具体的失败原因)
perror("malloc failed");
// 直接退出程序,避免后续代码出错
exit(-1);
}
// 给新节点的数据域赋值
// 将传入的 x 存放到节点的 data 成员中
NewNode->data = x;
// 给新节点的指针域初始化
// 新节点暂时还没有下一个节点,所以 next 置为 NULL
NewNode->next = NULL;
// 返回创建并初始化完成的新节点地址
return NewNode;
}
函数功能:为单链表创建并初始化一个新节点,是单链表所有插入操作的基础工具函数
实现逻辑:
- 使用
malloc动态分配一个节点大小的内存 - 检查内存分配是否成功,失败则打印错误并退出程序
- 将传入的数据
x赋值给节点的data成员 - 将节点的指针域
next置为NULL,表示暂时没有后继节点 - 返回新创建节点的地址
2.判断链表是否为空
c
// 2. 判断链表是否为空
// 功能:检查链表是否没有任何节点(空链表)
// 参数:phead - 链表的头指针(指向第一个节点)
// 返回值:bool 类型
// true -> 链表为空
// false -> 链表不为空
bool SListEmpty(SLTNode* phead)
{
// 核心判断逻辑:
// 如果头指针 phead 等于 NULL → 没有节点 → 空链表,返回 true
// 如果头指针 phead 不等于 NULL → 有节点 → 非空链表,返回 false
// 表达式 phead == NULL 的结果本身就是 true / false,直接返回即可
return phead == NULL;
}
函数功能:判断单链表是否为空链表,是链表操作中最基础的工具函数
实现逻辑:直接判断头指针 phead 是否等于 NULL,等于则链表为空,返回 true;否则返回 false
3.打印链表
c
// 3. 打印链表
// 函数功能:从头节点开始,依次打印链表中所有节点的数据,最后输出 NULL 表示链表结束
// 参数:phead 是链表的**头指针**(指向链表第一个节点)
void SListPrint(SLTNode* phead)
{
// 断言:检查链表是否为空
// 如果链表为空(SListEmpty返回true),程序直接报错终止,避免非法访问
// 作用:防止对空链表进行打印操作,提高代码健壮性
assert(!SListEmpty(phead));
// 定义一个临时指针变量 pList,让它指向链表头节点
// 不直接使用 phead 遍历:保护原头指针不被修改,保证链表结构不受影响
SLTNode* pList = phead;
// 循环遍历链表:当 pList 不为 NULL 时,说明还有节点未打印
// pList 初始指向第一个节点,每次循环后指向下一个节点,直到指向 NULL 结束
while (pList)
{
// 打印当前节点的数据,格式为:数据 ->
// %d 打印整型数据,pList->data 表示访问当前节点的 data 成员
printf("%d -> ", pList->data);
// 让临时指针 pList 指向**当前节点的下一个节点**
// 实现链表的向后遍历,移动到下一个待打印节点
pList = pList->next;
}
// 链表遍历结束(pList 变为 NULL),打印 NULL 表示链表尾部
// 让链表打印格式更清晰、完整
printf("NULL\n");
}
函数功能:从头节点开始遍历整个单链表 ,按格式打印所有节点的数据,最后以 NULL 结尾,直观展示链表结构
实现逻辑:
- 先用断言校验链表非空,防止空指针访问
- 创建临时指针遍历链表,不修改原头指针
- 循环逐个打印节点数据,格式为 数据
-> - 遍历结束后打印
NULL表示链表尾部
4.销毁链表
c
// 4. 销毁链表
// 功能:释放整个链表所有节点的动态内存,防止内存泄漏
// 参数:二级指针 pphead,因为要修改头指针本身,让它最终置空
void SListDestroy(SLTNode** pphead)
{
// 断言检查:保证传入的二级指针地址本身不为空
// 避免传 NULL 导致解引用崩溃
assert(pphead);
// 用 pcur 指向当前要释放的节点,初始指向链表第一个节点
SLTNode* pcur = *pphead;
// 循环:只要当前节点不为空,就继续释放
while (pcur)
{
// 关键点:必须先保存下一个节点地址!
// 因为 free(pcur) 后,pcur 指向空间失效,无法再通过 pcur->next 找下一个
SLTNode* next = pcur->next;
// 释放当前节点的动态内存
free(pcur);
// pcur 指向刚才保存好的下一个节点,继续循环释放
pcur = next;
}
// 所有节点释放完毕后,将头指针置为 NULL
// 避免头指针变成野指针
*pphead = NULL;
}
函数功能:完整销毁整个单链表,释放所有节点的动态内存 ,并将头指针置为NULL,彻底避免野指针和内存泄漏
实现逻辑:
- 断言校验二级指针
pphead本身不为空 - 用临时指针遍历链表,先保存下一个节点地址,再释放当前节点
- 所有节点释放完成后,将头指针置为`NULL
5.尾插
c
// 5. 尾插
// 功能:在链表的**尾部**插入一个新节点
// 参数:
// pphead - 二级指针,指向链表头指针的地址(需要修改头指针时必须用二级指针)
// x - 要插入的数据
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
// 断言检查:保证传入的二级指针不为空
// 防止传入 NULL 导致解引用崩溃
assert(pphead);
// 调用 BuySListNode 函数创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 情况1:链表为空(没有任何节点)
if (*pphead == NULL)
{
// 直接让头指针指向新节点,新节点就是第一个节点
*pphead = NewNode;
}
// 情况2:链表不为空,需要找到尾节点再插入
else
{
// 定义 tail 指针,从头节点开始找尾节点
SLTNode* tail = *pphead;
// 找尾节点:循环走到 tail->next == NULL 时停止
while (tail->next)
{
tail = tail->next;
}
// 把原来的尾节点的 next 指向新节点,完成尾插
tail->next = NewNode;
}
}
函数功能:在单链表的尾部添加一个新节点(尾插法),支持空链表和非空链表两种场景
实现逻辑:
- 断言校验二级指针合法,防止空指针访问
- 调用节点创建函数生成新节点
- 空链表处理:直接让头指针指向新节点
- 非空链表处理:遍历找到尾节点,将尾节点的
next指向新节点
6. 头插
c
// 6. 头插
// 功能:在链表**最前面**插入一个新节点
// 参数:
// pphead:二级指针,用来修改链表的头指针
// x:要插入的数据
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
// 断言检查:二级指针不能为空,防止程序崩溃
assert(pphead);
// 创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 关键步骤1:
// 新节点的 next 指向原来的头节点
// 把后面的链表先"挂"在新节点上
NewNode->next = *pphead;
// 关键步骤2:
// 更新头指针,让头指针指向新节点
// 新节点正式成为链表第一个节点
*pphead = NewNode;
}
函数功能:在单链表的头部插入一个新节点,成为新的头节点,是单链表最高效的插入方式
实现逻辑:
- 断言校验二级指针pphead合法,避免空指针访问
- 创建新节点
- 核心步骤:新节点先指向原头节点,再将头指针指向新节点
- 支持空链表插入,无需额外判断
7. 尾删
c
// 7. 尾删
// 功能:删除链表**最后一个节点**
// 参数:pphead 二级指针,需要修改头指针/节点指向
void SListPopBack(SLTNode** pphead)
{
// 断言1:检查二级指针本身是否合法,不能为NULL
assert(pphead);
// 断言2:链表不能为空,空链表不能删节点
assert(!SListEmpty(*pphead));
// 情况1:链表只有一个节点
if ((*pphead)->next == NULL)
{
// 直接释放这个唯一节点
free(*pphead);
// 头指针置空,避免野指针
*pphead = NULL;
}
// 情况2:链表有两个及以上节点
else
{
// 定义前驱指针,用来保存尾节点的前一个节点
SLTNode* prev = NULL;
// 定义尾指针,从头开始遍历找尾
SLTNode* tail = *pphead;
// 遍历找到最后一个节点
while (tail->next)
{
// prev 始终跟着 tail 走,保存前一个节点
prev = tail;
// tail 向后移动
tail = tail->next;
}
// 释放最后一个节点
free(tail);
// 让倒数第二个节点的 next 置空,成为新的尾节点
prev->next = NULL;
}
}
函数功能:删除单链表的最后一个节点(尾删),并正确处理空链表、只有一个节点、多个节点三种场景,释放内存防止泄漏
实现逻辑:
- 断言校验二级指针合法 + 链表非空;
- 只有一个节点:直接释放并置空头指针;
- 多个节点:遍历找到尾节点及其前驱节点,释放尾节点后将前驱节点置空
8.头删
c
// 8. 头删
// 功能:删除链表的第一个节点(头节点)
// 参数:pphead 二级指针,用于修改头指针的指向
void SListPopFront(SLTNode** pphead)
{
// 检查二级指针本身不能为空,防止非法访问
assert(pphead);
// 链表不能为空,空链表不能执行删除
assert(!SListEmpty(*pphead));
// 先保存头节点的下一个节点地址
// 因为 free 头节点后就无法再获取 next 了
SLTNode* next = (*pphead)->next;
// 释放原头节点的动态内存
free(*pphead);
// 让头指针指向原来的第二个节点,成为新的头节点
*pphead = next;
}
函数功能:删除单链表的第一个节点(头删),释放节点内存并更新头指针,是单链表最高效的删除操作
实现逻辑:
- 断言校验二级指针合法、链表非空,防止非法操作;
- 先保存第二个节点的地址(避免释放头节点后丢失链表)
- 释放原头节点内存
- 将头指针更新为第二个节点,成为新的头节点
9.查找结点
c
// 9. 查找节点
// 功能:在链表中查找值为 x 的节点
// 参数:
// phead - 链表头指针(只遍历不修改链表,所以用一级指针)
// x - 要查找的目标数据
// 返回值:
// 找到 -> 返回该节点的地址(指针)
// 没找到 -> 返回 NULL
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
// 定义临时遍历指针 pList,初始指向链表头节点
// 不直接移动原头指针 phead,保护链表结构
SLTNode* pList = phead;
// 遍历链表:只要当前指针不为空,就继续查找
while (pList)
{
// 判断:当前节点存储的数据 是否等于 目标数据 x
if (pList->data == x)
{
// 找到目标节点!
// 直接返回当前节点的地址(指针)
// 找到就立刻返回,不会继续往后找
return pList;
}
// 没找到 -> 指针向后移动,访问下一个节点
pList = pList->next;
}
// 能走到这里,说明遍历完整个链表都没找到目标值
// 返回 NULL 表示查找失败
return NULL;
}
函数功能:在单链表中查找值为x的节点,找到返回该节点的地址,找不到返回 NULL
实现逻辑:
- 用临时指针从头节点开始遍历链表
- 逐个比较节点数据与目标值
x - 匹配成功立即返回当前节点指针
- 遍历结束未找到则返回
NULL
10.获取链表节点个数
c
// 10. 获取链表节点个数
// 功能:遍历整个链表,统计并返回节点的总数量
// 参数:phead 链表头指针
// 返回值:节点个数(整型)
int SListSize(SLTNode* phead)
{
// 定义遍历指针,从头节点开始遍历
SLTNode* pList = phead;
// 定义计数器,初始化为 0
int count = 0;
// 遍历整个链表,当前节点不为空就继续
while (pList)
{
// 每访问一个节点,计数器 +1
count++;
// 指针向后移动
pList = pList->next;
}
// 遍历结束,返回总节点数
return count;
}
函数功能:遍历单链表,统计并返回链表中节点的总数量,空链表返回 0
实现逻辑:
- 使用临时指针遍历链表,不修改原头指针
- 初始化计数器
count = 0 - 每遍历一个有效节点,计数器
+1,指针后移 - 遍历完成后返回计数器值,即为节点总数
11.返回第一个节点的数据
c
// 11. 返回第一个节点的数据
// 功能:获取链表**第一个节点**中存储的数据
// 参数:phead - 链表头指针
// 返回值:第一个节点的数据值
SLTDataType SListFront(SLTNode* phead)
{
// 断言检查:链表不能为空
// 如果链表为空(SListEmpty返回true),!true 就是 false,程序会报错终止
// 作用:防止访问空链表的节点数据,避免程序崩溃
assert(!SListEmpty(phead));
// 直接返回头指针指向的节点的数据
// phead 指向第一个节点,-> 用来访问结构体成员 data
return phead->data;
}
函数功能:获取并返回单链表 ** 第一个节点(头节点)** 的数据,是链表的 "取队首 / 取栈顶" 类常用接口
实现逻辑:
- 用断言判断链表非空,防止空指针访问导致崩溃
2.直接返回头节点phead->data
12.返回最后一个节点的数据
c
// 12. 返回最后一个节点的数据
// 功能:获取链表中最后一个节点存储的数据
// 参数:phead - 链表头指针
// 返回值:最后一个节点的数据
SLTDataType SListBack(SLTNode* phead)
{
// 断言:链表不能为空,否则无法获取数据
assert(!SListEmpty(phead));
// 定义遍历指针,从头节点开始找尾节点
SLTNode* pcur = phead;
// 循环找尾节点:当 pcur->next 不为空时,继续往后走
// 退出循环时,pcur 正好指向最后一个节点
while (pcur->next)
{
pcur = pcur->next;
}
// 返回最后一个节点的数据
return pcur->data;
}
函数功能:获取并返回单链表 ** 最后一个节点(尾节点)** 的数据值
实现逻辑:
- 断言校验链表非空,避免空指针访问
- 定义临时指针从头节点开始遍历
- 循环移动指针,直到指向最后一个节点
pcur->next == NULL - 返回尾节点的数据
13. 在pos位置之前插入
c
// 13. 在 pos 位置之前插入
// 功能:在链表指定位置 pos 节点的**前面**插入一个新节点
// 参数:
// pphead - 二级指针,指向链表头指针的地址(可能需要修改头指针)
// pos - 要插入位置的节点地址(在这个节点前面插入)
// x - 新节点的数据
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
// 断言检查1:传入的二级指针本身不能为空,防止非法访问
assert(pphead);
// 断言检查2:插入位置 pos 必须是有效节点,不能为空
assert(pos);
// 定义 prev 指针,初始指向链表第一个节点
SLTNode* prev = *pphead;
// 情况1:要插入的位置 pos 就是**头节点**
// 在头节点之前插入 = 头插,直接调用头插函数
if (prev == pos)
{
SListPushFront(pphead, x);
}
// 情况2:pos 不是头节点,需要找到 pos 的前一个节点
else
{
// 遍历找前驱节点:循环直到 prev->next 等于 pos
// 结束时,prev 就是 pos 前面的那个节点
while (prev->next != pos)
{
prev = prev->next;
}
// 创建新节点
SLTNode* NewNode = BuySListNode(x);
// 插入核心步骤(指针链接)
// 1. 前一个节点 prev 指向新节点
prev->next = NewNode;
// 2. 新节点指向原来的 pos 节点,完成插入
NewNode->next = pos;
}
}
函数功能:在单链表的指定节点 pos 之前插入一个新节点,完美处理插在头部和插在中间 / 尾部两种情况,是链表通用插入接口
实现逻辑:
- 双重断言保证二级指针、插入位置
pos合法有效 - 如果
pos是头节点,直接复用头插函数,代码简洁 - 否则遍历找到
pos的前驱节点 - 创建新节点,调整指针完成插入
14.在 pos 位置之后插入
c
// 14. 在 pos 位置之后插入
// 功能:在指定节点 pos 的**后面**插入一个新节点
// 参数:
// pos - 要在其后面插入的节点地址
// x - 新节点存储的数据
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
// 断言:pos 必须是有效节点,不能为空
assert(pos);
// 创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 关键步骤1:
// 新节点先指向 pos 原来的下一个节点
NewNode->next = pos->next;
// 关键步骤2:
// pos 指向新节点,把新节点链到 pos 后面
pos->next = NewNode;
}
函数功能:在指定节点 pos 的后面插入一个新节点(后插),是单链表最简单、最高效的插入方式
实现逻辑:
- 断言保证
pos节点有效 - 创建新节点
- 核心指针操作:新节点先指向
pos的下一个节点,再把pos指向新节点 - 完成插入,不需要遍历、不需要找前驱
15.删除pos位置节点
c
// 15. 删除 pos 位置节点
// 功能:删除链表中 pos 指向的节点
// 参数:
// pphead - 二级指针,可能需要修改头指针(比如删除第一个节点)
// pos - 要删除的节点地址(必须是链表中合法节点)
void SListErase(SLTNode** pphead, SLTNode* pos)
{
// 断言1:二级指针本身不能为空,防止程序崩溃
assert(pphead);
// 断言2:链表不能为空,空链表不能删除节点
assert(!SListEmpty(*pphead));
// 断言3:要删除的位置 pos 必须是有效节点,不能为空
assert(pos);
// 情况1:要删除的节点就是头节点
// 等价于头删,直接调用头删函数即可
if (*pphead == pos)
{
SListPopFront(pphead);
}
// 情况2:要删除的不是头节点
else
{
// 定义 prev 指针,从头开始找 pos 的**前一个节点**
SLTNode* prev = *pphead;
// 循环:找到 prev->next == pos 时停止
// 此时 prev 就是 pos 前面的那个节点(前驱节点)
while (prev->next != pos)
{
prev = prev->next;
}
// 核心删除步骤1:
// 把 prev 的 next 指向 pos 的下一个节点
// 让链表跳过 pos 节点,把 pos 从链表中"摘下来"
prev->next = pos->next;
// 核心删除步骤2:
// 释放 pos 节点的动态内存,防止内存泄漏
free(pos);
// 把 pos 置空,避免成为野指针
pos = NULL;
}
}
函数功能:删除单链表中指定位置 pos 的节点,完美处理头节点 / 中间节点 / 尾节点,释放内存并正确修改指针,无内存泄漏、不断链
实现逻辑:
- 三重断言保证二级指针、链表非空、
pos有效; - 如果
pos是头节点,直接复用头删函数; - 否则遍历找到
pos的前驱节点; - 前驱节点跳过
pos,释放pos节点内存
16.删除pos的下一个的结点
c
// 16. 删除 pos 的下一个的结点
// 功能:删除指定节点 pos 的**后一个节点**
// 优点:不需要遍历找前驱,效率极高
// 参数:pos - 当前节点(删除它的下一个)
void SLTEraseAfter(SLTNode* pos)
{
// 断言1:pos 节点必须有效,不能为空
assert(pos);
// 如果 pos 已经是最后一个节点(没有下一个节点)
// 直接返回,不做任何操作
if (pos->next == NULL)
{
return;
}
// 定义 del 指针,保存要删除的节点(pos 的下一个)
SLTNode* del = pos->next;
// 核心步骤:
// 让 pos 跳过要删除的节点,直接指向 del 的下一个节点
// 把 del 从链表中脱离
pos->next = del->next;
// 释放要删除的节点内存
free(del);
// 将 del 置空,避免野指针
del = NULL;
}
函数功能:删除指定节点 pos 的下一个节点,不删除 pos 本身。是单链表中效率极高的删除操作
实现逻辑:
1.断言校验 pos 不为空,保证节点有效
2.如果pos 已是尾节点(无下一个节点),直接返回,不做操作
3.保存待删除节点 del = pos->next
4.跳过待删除节点:pos->next = del->next
5.释放内存并置空,避免内存泄漏
17.删除第一个值为 x 的节点
c
// 17. 删除第一个值为 x 的节点
// 功能:在链表中找到**第一个值等于 x**的节点,并将其删除
// 优点:代码复用性极强,不用重复写逻辑,简洁高效
// 参数:
// pphead - 二级指针,链表头指针地址
// x - 要删除的目标数据
void SListRemove(SLTNode** pphead, SLTDataType x)
{
// 断言1:检查二级指针本身不能为空,防止崩溃
assert(pphead);
// 断言2:链表不能为空,空链表不能删节点
assert(!SListEmpty(*pphead));
// 第一步:调用查找函数,找到值为 x 的节点地址
// 找到 → 返回节点地址 pos;没找到 → 返回 NULL
SLTNode* pos = SListFind(*pphead, x);
// 第二步:判断是否找到目标节点
if (pos != NULL)
{
// 找到 → 直接调用指定位置删除函数 SListErase 删除 pos 节点
SListErase(pphead, pos);
}
// 如果没找到(pos == NULL),函数直接结束,不做任何操作
}
函数功能:在单链表中查找并删除第一个值为 x 的节点,是面向业务的 "按值删除" 接口,封装了查找和删除两步操作
实现逻辑:
- 断言校验二级指针合法、链表非空
- 调用
SListFind查找值为x的节点 - 如果找到节点,直接调用
ListErase删除该节点 - 如果没找到,不做任何操作
18 删除所有值为 x 的节点
c
// 18. 删除所有值为 x 的节点
// 功能:删除链表中**所有**数据等于 x 的节点(不止删一个,是删全部)
// 参数:
// pphead - 二级指针,可能需要修改头指针
// x - 要删除的目标数据
void SListRemoveAll(SLTNode** pphead, SLTDataType x)
{
// 断言1:二级指针本身不能为空
assert(pphead);
// 断言2:链表不能为空
assert(!SListEmpty(*pphead));
// prev:指向当前节点的**前一个节点**(前驱)
SLTNode* prev = NULL;
// pcur:指向**当前正在遍历/检查的节点**
SLTNode* pcur = *pphead;
// 循环遍历整个链表
while (pcur)
{
// 如果当前节点的数据 == 要删除的值 x → 准备删除
if (pcur->data == x)
{
// 先保存要删除的节点地址,后面 free
SLTNode* del = pcur;
// 情况1:要删除的是**头节点**(prev == NULL 说明前面没节点)
if (prev == NULL)
{
// 头指针直接指向第二个节点
*pphead = pcur->next;
// pcur 移动到新的头节点
pcur = *pphead;
}
// 情况2:要删除的是**中间/尾节点**
else
{
// 让前驱节点跳过当前节点,指向后面
prev->next = pcur->next;
// pcur 跳到后面节点
pcur = prev->next;
}
// 释放被删除节点的内存
free(del);
del = NULL;
}
// 当前节点数据 != x → 不删,继续往后走
else
{
// prev 跟着 pcur 走
prev = pcur;
// pcur 往后遍历
pcur = pcur->next;
}
}
}
函数功能:删除链表中所有值为 x 的节点(包括头节点、中间节点、尾节点、连续重复节点),彻底清空目标值,释放所有对应节点内存
实现逻辑:
- 断言校验二级指针合法、链表非空;
- 使用
prev + pcur双指针遍历链表(前驱 + 当前); - 找到目标节点时:
a.若是头节点,直接更新头指针
b.若是中间 / 尾节点,前驱跳过当前节点 - 释放节点内存,继续遍历,直到删除所有匹配节点
19.链表反转(三指针)
c
/ 19. 链表反转(三指针)
// 功能:将整个链表的指向反转
// 原链表:1 -> 2 -> 3 -> 4 -> NULL
// 反转后:4 -> 3 -> 2 -> 1 -> NULL
// 方法:三指针法(最常用、最高效)
void SListReverse(SLTNode** pphead)
{
// 断言1:检查二级指针本身是否合法
assert(pphead);
// 断言2:链表不能为空,空链表无需反转
assert(!SListEmpty(*pphead));
// 定义三个核心指针(三指针法)
SLTNode* pcur = *pphead; // 当前遍历到的节点(从头节点开始)
SLTNode* next = NULL; // 保存下一个节点(防止断链)
SLTNode* prev = NULL; // 保存前一个节点(最终会变成新头节点)
// 遍历整个链表,直到当前节点为 NULL
while (pcur)
{
// 第一步:保存下一个节点的地址
// 因为马上要修改 pcur->next,必须先存好后面的路
next = pcur->next;
// 第二步:核心反转!
// 让当前节点的 next 指向前一个节点(箭头反转)
pcur->next = prev;
// 第三步:指针向后移动,准备处理下一个节点
prev = pcur; // prev 走到当前节点位置
pcur = next; // pcur 走到刚才保存的下一个节点位置
}
// 循环结束时,prev 指向原链表最后一个节点
// 它就是反转后的新头节点,更新头指针
*pphead = prev;
}
函数功能:将整个单链表的指向完全反转
实现逻辑:
- 三个指针的作用:
prev:记录上一个节点(初始为NULL)
pcur:记录当前正在处理的节点(从头节点开始)
next:记录下一个节点(防止链表断裂) - 循环内的 4 步固定操作(核心)
从头走到尾,每一个节点都做这 4 件事:
保存后路:next = pcur->next
先把下一个节点存起来,不然改完指针就找不到后面了
反转箭头:pcur->next = prev
最关键一步:让当前节点往回指,指向前面的节点
prev前移:prev = pcur
前驱指针跟上,走到当前节点的位置
pcur 前移:pcur = next
当前指针走向之前保存的下一个节点 - 循环结束
pcur变成NULL(走到链表尾部)
prev正好指向原链表最后一个节点
最后执行:*pphead = prev
更新头指针,让它指向新的头节点
20.查找倒数第 k 个节点
c
// 20. 查找倒数第 k 个节点
// 功能:找到链表中**倒数第 k 个**节点,并返回该节点的地址
// 方法:快慢指针法(最优解法,只遍历一次链表)
// 参数:
// phead - 链表头指针
// k - 倒数第 k 个
// 返回值:找到返回节点地址,没找到返回 NULL
SLTNode* SListFindKthFromTail(SLTNode* phead, int k)
{
// 断言1:链表不能为空
assert(!SListEmpty(phead));
// 断言2:k 必须是正数(不能是 0 或负数)
assert(k > 0);
// 创建快慢两个指针,一开始都指向头节点
SLTNode* fast = phead; // 快指针
SLTNode* slow = phead; // 慢指针
// 第一步:让快指针先走 k 步!
// 核心思想:让快指针和慢指针拉开 k 个节点的距离
while (k--)
{
// 如果快指针还没走完 k 步就变成 NULL
// 说明 k 比链表长度大,非法,直接返回 NULL
if (fast == NULL)
{
return NULL;
}
fast = fast->next;
}
// 第二步:快慢指针一起走,直到快指针走到 NULL
// 此时慢指针正好指向 倒数第 k 个节点
while (fast)
{
slow = slow->next;
fast = fast->next;
}
// 慢指针就是答案
return slow;
}
函数功能:使用快慢指针法查找链表倒数第 k 个节点,找到返回节点地址,非法情况返回 NULL
实现逻辑:
- 断言校验链表非空、
k为正整数 - 快指针先走
k步,与慢指针拉开距离 - 若快指针提前为空,说明
k超出链表长度,直接返回NULL - 快慢指针同步向后遍历,快指针为空时慢指针即为倒数第
k个节点
21.合并两个有序链表
c
// 21. 合并两个有序链表
// 功能:将两个**有序**的单链表,合并成一个新的有序链表
// 方法:双指针遍历 + 哨兵位(带头节点简化操作)
// 参数:
// l1 - 有序链表1
// l2 - 有序链表2
// 返回值:合并后的新链表头指针
SLTNode* SListMerge(SLTNode* l1, SLTNode* l2)
{
// 断言:两个链表不能同时为空
assert((!SListEmpty(l1)) || (!SListEmpty(l2)));
// 边界情况1:如果 l1 为空,直接返回 l2
if (SListEmpty(l1))
{
return l2;
}
// 边界情况2:如果 l2 为空,直接返回 l1
if (SListEmpty(l2))
{
return l1;
}
// 🎯 核心:创建哨兵位节点(哑节点)
// 作用:不用单独处理头节点,让代码逻辑统一,超级方便!
SLTNode* guard = BuySListNode(0);
// tail 指针:始终指向新链表的最后一个节点,负责尾插
SLTNode* tail = guard;
// 循环:两个链表都没走完时,比较节点大小,小的先接入新链表
while (l1 && l2)
{
// 谁小,就把谁链到 tail 后面
if (l1->data <= l2->data)
{
tail->next = l1; // 把 l1 节点接到新链表尾部
l1 = l1->next; // l1 指针后移
}
else
{
tail->next = l2; // 把 l2 节点接到新链表尾部
l2 = l2->next; // l2 指针后移
}
tail = tail->next; // tail 永远跟着走到新链表尾部
}
// 一个链表走完了,把另一个链表**剩余的所有节点**直接接上去
if (l1 != NULL)
{
tail->next = l1;
}
if (l2 != NULL)
{
tail->next = l2;
}
// 新链表的真实头节点,是哨兵位的下一个节点
SLTNode* NewHead = guard->next;
// 释放哨兵位节点(它只是工具人,用完就扔)
free(guard);
guard = NULL;
// 返回真正的新链表头
return NewHead;
}
函数功能:合并两个有序单链表,合并后依然保持有序,返回新链表的头指针
实现逻辑:
- 处理边界:若其中一个链表为空,直接返回另一个
- 创建哨兵位
guar节点,简化头节点处理逻辑 - 双指针遍历两个链表,将较小节点依次链接到新链表尾部
- 遍历结束后,将剩余未遍历完的链表直接链接到尾部
- 释放哨兵节点,返回新链表真实头节点
四、所有代码
1.SList.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int SLTDataType;
// 单链表节点结构体
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//========================= 基础核心函数 =========================//
// 1. 创建新节点(底层支持函数)
SLTNode* BuySListNode(SLTDataType x);
// 2. 打印链表
void SListPrint(SLTNode* phead);
// 3. 销毁链表
void SListDestroy(SLTNode** pphead);
//========================= 基础增删函数 =========================//
// 4. 尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
// 5. 头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
// 6. 尾删
void SListPopBack(SLTNode** pphead);
// 7. 头删
void SListPopFront(SLTNode** pphead);
//========================= 查找与访问 =========================//
// 8. 查找节点
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 9. 判断链表是否为空
bool SListEmpty(SLTNode* phead);
// 10. 获取链表节点个数
int SListSize(SLTNode* phead);
// 11. 返回第一个节点的数据
SLTDataType SListFront(SLTNode* phead);
// 12. 返回最后一个节点的数据
SLTDataType SListBack(SLTNode* phead);
//========================= 指定位置操作 =========================//
// 13. 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 14. 在 pos 位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
// 15. 删除pos位置节点
void SListErase(SLTNode** pphead, SLTNode* pos);
// 16. 删除pos的下一个的结点
void SLTEraseAfter(SLTNode* pos);
//========================= 按值删除 =========================//
// 17. 删除第一个值为 x 的节点
void SListRemove(SLTNode** pphead, SLTDataType x);
// 18. 删除所有值为 x 的节点
void SListRemoveAll(SLTNode** pphead, SLTDataType x);
//========================= 高级算法 =========================//
// 19. 链表反转(三指针)
void SListReverse(SLTNode** pphead);
// 20. 查找倒数第 k 个节点
SLTNode* SListFindKthFromTail(SLTNode* phead, int k);
// 21. 合并两个有序链表
SLTNode* SListMerge(SLTNode* l1, SLTNode* l2);
2.SList.c
c
#include "SList.h"
// 1. 创建新节点
// 功能:向内存申请一个新的链表节点,并初始化数据和指针
// 参数:x - 要存入新节点的数据
// 返回值:返回创建好的新节点的地址
SLTNode* BuySListNode(SLTDataType x)
{
// 申请一块和链表节点大小一样的内存空间
// malloc 申请内存后返回 void* 类型,需要强制类型转换为 SLTNode*
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
// 检查 malloc 是否申请内存成功
// 如果申请失败(内存不足等原因),NewNode 会是 NULL
if (NewNode == NULL)
{
// 打印错误信息(系统会提示具体的失败原因)
perror("malloc failed");
// 直接退出程序,避免后续代码出错
exit(-1);
}
// 给新节点的数据域赋值
// 将传入的 x 存放到节点的 data 成员中
NewNode->data = x;
// 给新节点的指针域初始化
// 新节点暂时还没有下一个节点,所以 next 置为 NULL
NewNode->next = NULL;
// 返回创建并初始化完成的新节点地址
return NewNode;
}
// 2. 判断链表是否为空
// 功能:检查链表是否没有任何节点(空链表)
// 参数:phead - 链表的头指针(指向第一个节点)
// 返回值:bool 类型
// true -> 链表为空
// false -> 链表不为空
bool SListEmpty(SLTNode* phead)
{
// 核心判断逻辑:
// 如果头指针 phead 等于 NULL → 没有节点 → 空链表,返回 true
// 如果头指针 phead 不等于 NULL → 有节点 → 非空链表,返回 false
// 表达式 phead == NULL 的结果本身就是 true / false,直接返回即可
return phead == NULL;
}
// 3. 打印链表
// 函数功能:从头节点开始,依次打印链表中所有节点的数据,最后输出 NULL 表示链表结束
// 参数:phead 是链表的**头指针**(指向链表第一个节点)
void SListPrint(SLTNode* phead)
{
// 断言:检查链表是否为空
// 如果链表为空(SListEmpty返回true),程序直接报错终止,避免非法访问
// 作用:防止对空链表进行打印操作,提高代码健壮性
assert(!SListEmpty(phead));
// 定义一个临时指针变量 pList,让它指向链表头节点
// 不直接使用 phead 遍历:保护原头指针不被修改,保证链表结构不受影响
SLTNode* pList = phead;
// 循环遍历链表:当 pList 不为 NULL 时,说明还有节点未打印
// pList 初始指向第一个节点,每次循环后指向下一个节点,直到指向 NULL 结束
while (pList)
{
// 打印当前节点的数据,格式为:数据 ->
// %d 打印整型数据,pList->data 表示访问当前节点的 data 成员
printf("%d -> ", pList->data);
// 让临时指针 pList 指向**当前节点的下一个节点**
// 实现链表的向后遍历,移动到下一个待打印节点
pList = pList->next;
}
// 链表遍历结束(pList 变为 NULL),打印 NULL 表示链表尾部
// 让链表打印格式更清晰、完整
printf("NULL\n");
}
// 4. 销毁链表
// 功能:释放整个链表所有节点的动态内存,防止内存泄漏
// 参数:二级指针 pphead,因为要修改头指针本身,让它最终置空
void SListDestroy(SLTNode** pphead)
{
// 断言检查:保证传入的二级指针地址本身不为空
// 避免传 NULL 导致解引用崩溃
assert(pphead);
// 用 pcur 指向当前要释放的节点,初始指向链表第一个节点
SLTNode* pcur = *pphead;
// 循环:只要当前节点不为空,就继续释放
while (pcur)
{
// 关键点:必须先保存下一个节点地址!
// 因为 free(pcur) 后,pcur 指向空间失效,无法再通过 pcur->next 找下一个
SLTNode* next = pcur->next;
// 释放当前节点的动态内存
free(pcur);
// pcur 指向刚才保存好的下一个节点,继续循环释放
pcur = next;
}
// 所有节点释放完毕后,将头指针置为 NULL
// 避免头指针变成野指针
*pphead = NULL;
}
// 5. 尾插
// 功能:在链表的**尾部**插入一个新节点
// 参数:
// pphead - 二级指针,指向链表头指针的地址(需要修改头指针时必须用二级指针)
// x - 要插入的数据
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
// 断言检查:保证传入的二级指针不为空
// 防止传入 NULL 导致解引用崩溃
assert(pphead);
// 调用 BuySListNode 函数创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 情况1:链表为空(没有任何节点)
if (*pphead == NULL)
{
// 直接让头指针指向新节点,新节点就是第一个节点
*pphead = NewNode;
}
// 情况2:链表不为空,需要找到尾节点再插入
else
{
// 定义 tail 指针,从头节点开始找尾节点
SLTNode* tail = *pphead;
// 找尾节点:循环走到 tail->next == NULL 时停止
while (tail->next)
{
tail = tail->next;
}
// 把原来的尾节点的 next 指向新节点,完成尾插
tail->next = NewNode;
}
}
// 6. 头插
// 功能:在链表**最前面**插入一个新节点
// 参数:
// pphead:二级指针,用来修改链表的头指针
// x:要插入的数据
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
// 断言检查:二级指针不能为空,防止程序崩溃
assert(pphead);
// 创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 关键步骤1:
// 新节点的 next 指向原来的头节点
// 把后面的链表先"挂"在新节点上
NewNode->next = *pphead;
// 关键步骤2:
// 更新头指针,让头指针指向新节点
// 新节点正式成为链表第一个节点
*pphead = NewNode;
}
// 7. 尾删
// 功能:删除链表**最后一个节点**
// 参数:pphead 二级指针,需要修改头指针/节点指向
void SListPopBack(SLTNode** pphead)
{
// 断言1:检查二级指针本身是否合法,不能为NULL
assert(pphead);
// 断言2:链表不能为空,空链表不能删节点
assert(!SListEmpty(*pphead));
// 情况1:链表只有一个节点
if ((*pphead)->next == NULL)
{
// 直接释放这个唯一节点
free(*pphead);
// 头指针置空,避免野指针
*pphead = NULL;
}
// 情况2:链表有两个及以上节点
else
{
// 定义前驱指针,用来保存尾节点的前一个节点
SLTNode* prev = NULL;
// 定义尾指针,从头开始遍历找尾
SLTNode* tail = *pphead;
// 遍历找到最后一个节点
while (tail->next)
{
// prev 始终跟着 tail 走,保存前一个节点
prev = tail;
// tail 向后移动
tail = tail->next;
}
// 释放最后一个节点
free(tail);
// 让倒数第二个节点的 next 置空,成为新的尾节点
prev->next = NULL;
}
}
// 8. 头删
// 功能:删除链表的第一个节点(头节点)
// 参数:pphead 二级指针,用于修改头指针的指向
void SListPopFront(SLTNode** pphead)
{
// 检查二级指针本身不能为空,防止非法访问
assert(pphead);
// 链表不能为空,空链表不能执行删除
assert(!SListEmpty(*pphead));
// 先保存头节点的下一个节点地址
// 因为 free 头节点后就无法再获取 next 了
SLTNode* next = (*pphead)->next;
// 释放原头节点的动态内存
free(*pphead);
// 让头指针指向原来的第二个节点,成为新的头节点
*pphead = next;
}
// 9. 查找节点
// 功能:在链表中查找值为 x 的节点
// 参数:
// phead - 链表头指针(只遍历不修改链表,所以用一级指针)
// x - 要查找的目标数据
// 返回值:
// 找到 -> 返回该节点的地址(指针)
// 没找到 -> 返回 NULL
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
// 定义临时遍历指针 pList,初始指向链表头节点
// 不直接移动原头指针 phead,保护链表结构
SLTNode* pList = phead;
// 遍历链表:只要当前指针不为空,就继续查找
while (pList)
{
// 判断:当前节点存储的数据 是否等于 目标数据 x
if (pList->data == x)
{
// 找到目标节点!
// 直接返回当前节点的地址(指针)
// 找到就立刻返回,不会继续往后找
return pList;
}
// 没找到 -> 指针向后移动,访问下一个节点
pList = pList->next;
}
// 能走到这里,说明遍历完整个链表都没找到目标值
// 返回 NULL 表示查找失败
return NULL;
}
// 10. 获取链表节点个数
// 功能:遍历整个链表,统计并返回节点的总数量
// 参数:phead 链表头指针
// 返回值:节点个数(整型)
int SListSize(SLTNode* phead)
{
// 定义遍历指针,从头节点开始遍历
SLTNode* pList = phead;
// 定义计数器,初始化为 0
int count = 0;
// 遍历整个链表,当前节点不为空就继续
while (pList)
{
// 每访问一个节点,计数器 +1
count++;
// 指针向后移动
pList = pList->next;
}
// 遍历结束,返回总节点数
return count;
}
// 11. 返回第一个节点的数据
// 功能:获取链表**第一个节点**中存储的数据
// 参数:phead - 链表头指针
// 返回值:第一个节点的数据值
SLTDataType SListFront(SLTNode* phead)
{
// 断言检查:链表不能为空
// 如果链表为空(SListEmpty返回true),!true 就是 false,程序会报错终止
// 作用:防止访问空链表的节点数据,避免程序崩溃
assert(!SListEmpty(phead));
// 直接返回头指针指向的节点的数据
// phead 指向第一个节点,-> 用来访问结构体成员 data
return phead->data;
}
// 12. 返回最后一个节点的数据
// 功能:获取链表中最后一个节点存储的数据
// 参数:phead - 链表头指针
// 返回值:最后一个节点的数据
SLTDataType SListBack(SLTNode* phead)
{
// 断言:链表不能为空,否则无法获取数据
assert(!SListEmpty(phead));
// 定义遍历指针,从头节点开始找尾节点
SLTNode* pcur = phead;
// 循环找尾节点:当 pcur->next 不为空时,继续往后走
// 退出循环时,pcur 正好指向最后一个节点
while (pcur->next)
{
pcur = pcur->next;
}
// 返回最后一个节点的数据
return pcur->data;
}
// 13. 在 pos 位置之前插入
// 功能:在链表指定位置 pos 节点的**前面**插入一个新节点
// 参数:
// pphead - 二级指针,指向链表头指针的地址(可能需要修改头指针)
// pos - 要插入位置的节点地址(在这个节点前面插入)
// x - 新节点的数据
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
// 断言检查1:传入的二级指针本身不能为空,防止非法访问
assert(pphead);
// 断言检查2:插入位置 pos 必须是有效节点,不能为空
assert(pos);
// 定义 prev 指针,初始指向链表第一个节点
SLTNode* prev = *pphead;
// 情况1:要插入的位置 pos 就是**头节点**
// 在头节点之前插入 = 头插,直接调用头插函数
if (prev == pos)
{
SListPushFront(pphead, x);
}
// 情况2:pos 不是头节点,需要找到 pos 的前一个节点
else
{
// 遍历找前驱节点:循环直到 prev->next 等于 pos
// 结束时,prev 就是 pos 前面的那个节点
while (prev->next != pos)
{
prev = prev->next;
}
// 创建新节点
SLTNode* NewNode = BuySListNode(x);
// 插入核心步骤(指针链接)
// 1. 前一个节点 prev 指向新节点
prev->next = NewNode;
// 2. 新节点指向原来的 pos 节点,完成插入
NewNode->next = pos;
}
}
// 14. 在 pos 位置之后插入
// 功能:在指定节点 pos 的**后面**插入一个新节点
// 参数:
// pos - 要在其后面插入的节点地址
// x - 新节点存储的数据
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
// 断言:pos 必须是有效节点,不能为空
assert(pos);
// 创建一个值为 x 的新节点
SLTNode* NewNode = BuySListNode(x);
// 关键步骤1:
// 新节点先指向 pos 原来的下一个节点
NewNode->next = pos->next;
// 关键步骤2:
// pos 指向新节点,把新节点链到 pos 后面
pos->next = NewNode;
}
// 15. 删除 pos 位置节点
// 功能:删除链表中 pos 指向的节点
// 参数:
// pphead - 二级指针,可能需要修改头指针(比如删除第一个节点)
// pos - 要删除的节点地址(必须是链表中合法节点)
void SListErase(SLTNode** pphead, SLTNode* pos)
{
// 断言1:二级指针本身不能为空,防止程序崩溃
assert(pphead);
// 断言2:链表不能为空,空链表不能删除节点
assert(!SListEmpty(*pphead));
// 断言3:要删除的位置 pos 必须是有效节点,不能为空
assert(pos);
// 情况1:要删除的节点就是头节点
// 等价于头删,直接调用头删函数即可
if (*pphead == pos)
{
SListPopFront(pphead);
}
// 情况2:要删除的不是头节点
else
{
// 定义 prev 指针,从头开始找 pos 的**前一个节点**
SLTNode* prev = *pphead;
// 循环:找到 prev->next == pos 时停止
// 此时 prev 就是 pos 前面的那个节点(前驱节点)
while (prev->next != pos)
{
prev = prev->next;
}
// 核心删除步骤1:
// 把 prev 的 next 指向 pos 的下一个节点
// 让链表跳过 pos 节点,把 pos 从链表中"摘下来"
prev->next = pos->next;
// 核心删除步骤2:
// 释放 pos 节点的动态内存,防止内存泄漏
free(pos);
// 把 pos 置空,避免成为野指针
pos = NULL;
}
}
// 16. 删除 pos 的下一个的结点
// 功能:删除指定节点 pos 的**后一个节点**
// 优点:不需要遍历找前驱,效率极高
// 参数:pos - 当前节点(删除它的下一个)
void SLTEraseAfter(SLTNode* pos)
{
// 断言1:pos 节点必须有效,不能为空
assert(pos);
// 如果 pos 已经是最后一个节点(没有下一个节点)
// 直接返回,不做任何操作
if (pos->next == NULL)
{
return;
}
// 定义 del 指针,保存要删除的节点(pos 的下一个)
SLTNode* del = pos->next;
// 核心步骤:
// 让 pos 跳过要删除的节点,直接指向 del 的下一个节点
// 把 del 从链表中脱离
pos->next = del->next;
// 释放要删除的节点内存
free(del);
// 将 del 置空,避免野指针
del = NULL;
}
// 17. 删除第一个值为 x 的节点
// 功能:在链表中找到**第一个值等于 x**的节点,并将其删除
// 优点:代码复用性极强,不用重复写逻辑,简洁高效
// 参数:
// pphead - 二级指针,链表头指针地址
// x - 要删除的目标数据
void SListRemove(SLTNode** pphead, SLTDataType x)
{
// 断言1:检查二级指针本身不能为空,防止崩溃
assert(pphead);
// 断言2:链表不能为空,空链表不能删节点
assert(!SListEmpty(*pphead));
// 第一步:调用查找函数,找到值为 x 的节点地址
// 找到 → 返回节点地址 pos;没找到 → 返回 NULL
SLTNode* pos = SListFind(*pphead, x);
// 第二步:判断是否找到目标节点
if (pos != NULL)
{
// 找到 → 直接调用指定位置删除函数 SListErase 删除 pos 节点
SListErase(pphead, pos);
}
// 如果没找到(pos == NULL),函数直接结束,不做任何操作
}
// 18. 删除所有值为 x 的节点
// 功能:删除链表中**所有**数据等于 x 的节点(不止删一个,是删全部)
// 参数:
// pphead - 二级指针,可能需要修改头指针
// x - 要删除的目标数据
void SListRemoveAll(SLTNode** pphead, SLTDataType x)
{
// 断言1:二级指针本身不能为空
assert(pphead);
// 断言2:链表不能为空
assert(!SListEmpty(*pphead));
// prev:指向当前节点的**前一个节点**(前驱)
SLTNode* prev = NULL;
// pcur:指向**当前正在遍历/检查的节点**
SLTNode* pcur = *pphead;
// 循环遍历整个链表
while (pcur)
{
// 如果当前节点的数据 == 要删除的值 x → 准备删除
if (pcur->data == x)
{
// 先保存要删除的节点地址,后面 free
SLTNode* del = pcur;
// 情况1:要删除的是**头节点**(prev == NULL 说明前面没节点)
if (prev == NULL)
{
// 头指针直接指向第二个节点
*pphead = pcur->next;
// pcur 移动到新的头节点
pcur = *pphead;
}
// 情况2:要删除的是**中间/尾节点**
else
{
// 让前驱节点跳过当前节点,指向后面
prev->next = pcur->next;
// pcur 跳到后面节点
pcur = prev->next;
}
// 释放被删除节点的内存
free(del);
del = NULL;
}
// 当前节点数据 != x → 不删,继续往后走
else
{
// prev 跟着 pcur 走
prev = pcur;
// pcur 往后遍历
pcur = pcur->next;
}
}
}
/ 19. 链表反转(三指针)
// 功能:将整个链表的指向反转
// 原链表:1 -> 2 -> 3 -> 4 -> NULL
// 反转后:4 -> 3 -> 2 -> 1 -> NULL
// 方法:三指针法(最常用、最高效)
void SListReverse(SLTNode** pphead)
{
// 断言1:检查二级指针本身是否合法
assert(pphead);
// 断言2:链表不能为空,空链表无需反转
assert(!SListEmpty(*pphead));
// 定义三个核心指针(三指针法)
SLTNode* pcur = *pphead; // 当前遍历到的节点(从头节点开始)
SLTNode* next = NULL; // 保存下一个节点(防止断链)
SLTNode* prev = NULL; // 保存前一个节点(最终会变成新头节点)
// 遍历整个链表,直到当前节点为 NULL
while (pcur)
{
// 第一步:保存下一个节点的地址
// 因为马上要修改 pcur->next,必须先存好后面的路
next = pcur->next;
// 第二步:核心反转!
// 让当前节点的 next 指向前一个节点(箭头反转)
pcur->next = prev;
// 第三步:指针向后移动,准备处理下一个节点
prev = pcur; // prev 走到当前节点位置
pcur = next; // pcur 走到刚才保存的下一个节点位置
}
// 循环结束时,prev 指向原链表最后一个节点
// 它就是反转后的新头节点,更新头指针
*pphead = prev;
}
// 20. 查找倒数第 k 个节点
// 功能:找到链表中**倒数第 k 个**节点,并返回该节点的地址
// 方法:快慢指针法(最优解法,只遍历一次链表)
// 参数:
// phead - 链表头指针
// k - 倒数第 k 个
// 返回值:找到返回节点地址,没找到返回 NULL
SLTNode* SListFindKthFromTail(SLTNode* phead, int k)
{
// 断言1:链表不能为空
assert(!SListEmpty(phead));
// 断言2:k 必须是正数(不能是 0 或负数)
assert(k > 0);
// 创建快慢两个指针,一开始都指向头节点
SLTNode* fast = phead; // 快指针
SLTNode* slow = phead; // 慢指针
// 第一步:让快指针先走 k 步!
// 核心思想:让快指针和慢指针拉开 k 个节点的距离
while (k--)
{
// 如果快指针还没走完 k 步就变成 NULL
// 说明 k 比链表长度大,非法,直接返回 NULL
if (fast == NULL)
{
return NULL;
}
fast = fast->next;
}
// 第二步:快慢指针一起走,直到快指针走到 NULL
// 此时慢指针正好指向 倒数第 k 个节点
while (fast)
{
slow = slow->next;
fast = fast->next;
}
// 慢指针就是答案
return slow;
}
// 21. 合并两个有序链表
// 功能:将两个**有序**的单链表,合并成一个新的有序链表
// 方法:双指针遍历 + 哨兵位(带头节点简化操作)
// 参数:
// l1 - 有序链表1
// l2 - 有序链表2
// 返回值:合并后的新链表头指针
SLTNode* SListMerge(SLTNode* l1, SLTNode* l2)
{
// 断言:两个链表不能同时为空
assert((!SListEmpty(l1)) || (!SListEmpty(l2)));
// 边界情况1:如果 l1 为空,直接返回 l2
if (SListEmpty(l1))
{
return l2;
}
// 边界情况2:如果 l2 为空,直接返回 l1
if (SListEmpty(l2))
{
return l1;
}
// 🎯 核心:创建哨兵位节点(哑节点)
// 作用:不用单独处理头节点,让代码逻辑统一,超级方便!
SLTNode* guard = BuySListNode(0);
// tail 指针:始终指向新链表的最后一个节点,负责尾插
SLTNode* tail = guard;
// 循环:两个链表都没走完时,比较节点大小,小的先接入新链表
while (l1 && l2)
{
// 谁小,就把谁链到 tail 后面
if (l1->data <= l2->data)
{
tail->next = l1; // 把 l1 节点接到新链表尾部
l1 = l1->next; // l1 指针后移
}
else
{
tail->next = l2; // 把 l2 节点接到新链表尾部
l2 = l2->next; // l2 指针后移
}
tail = tail->next; // tail 永远跟着走到新链表尾部
}
// 一个链表走完了,把另一个链表**剩余的所有节点**直接接上去
if (l1 != NULL)
{
tail->next = l1;
}
if (l2 != NULL)
{
tail->next = l2;
}
// 新链表的真实头节点,是哨兵位的下一个节点
SLTNode* NewHead = guard->next;
// 释放哨兵位节点(它只是工具人,用完就扔)
free(guard);
guard = NULL;
// 返回真正的新链表头
return NewHead;
}