数据结构--单链表(C语言实现)

一、单链表

单链表(Singly Linked List) 是一种最基础的线性表,它不使用连续的内存空间存储数据,而是通过指针把一个个独立的节点串联起来,形成一条链式结构。

1.核心结构

单链表由一个个节点(Node) 组成,每个节点包含两部分:

  1. 数据域:存放当前节点的数据(如数字、字符、结构体等)
  2. 指针域:存放下一个节点的地址,用来指向下一个节点

节点结构:

数据 data

指针 next

2. 整体结构

整个链表有一个头指针(Head),指向第一个节点;最后一个节点的指针指向 NULL(空地址),表示链表结束。

头指针 → 节点1 → 节点2 → 节点3 → ... → 尾节点 → NULL

3.关键特点

  1. 不占用连续内存:可以灵活利用零散空间
  2. 长度可动态变化:插入、删除节点不需要移动大量元素
  3. 只能单向遍历:只能从前往后找,不能回头
  4. 访问效率低:不能像数组那样直接按下标随机访问,必须从头遍历
  5. 插入 / 删除高效:只需要修改指针指向,时间复杂度 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;
}

函数功能:为单链表创建并初始化一个新节点,是单链表所有插入操作的基础工具函数

实现逻辑:

  1. 使用 malloc 动态分配一个节点大小的内存
  2. 检查内存分配是否成功,失败则打印错误并退出程序
  3. 将传入的数据 x 赋值给节点的 data 成员
  4. 将节点的指针域 next 置为NULL,表示暂时没有后继节点
  5. 返回新创建节点的地址

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 结尾,直观展示链表结构

实现逻辑:

  1. 先用断言校验链表非空,防止空指针访问
  2. 创建临时指针遍历链表,不修改原头指针
  3. 循环逐个打印节点数据,格式为 数据 ->
  4. 遍历结束后打印 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,彻底避免野指针和内存泄漏

实现逻辑:

  1. 断言校验二级指针pphead本身不为空
  2. 用临时指针遍历链表,先保存下一个节点地址,再释放当前节点
  3. 所有节点释放完成后,将头指针置为`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;
	}
}

函数功能:在单链表的尾部添加一个新节点(尾插法),支持空链表和非空链表两种场景

实现逻辑:

  1. 断言校验二级指针合法,防止空指针访问
  2. 调用节点创建函数生成新节点
  3. 空链表处理:直接让头指针指向新节点
  4. 非空链表处理:遍历找到尾节点,将尾节点的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;
}

函数功能:在单链表的头部插入一个新节点,成为新的头节点,是单链表最高效的插入方式

实现逻辑:

  1. 断言校验二级指针pphead合法,避免空指针访问
  2. 创建新节点
  3. 核心步骤:新节点先指向原头节点,再将头指针指向新节点
  4. 支持空链表插入,无需额外判断

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;
	}
}

函数功能:删除单链表的最后一个节点(尾删),并正确处理空链表、只有一个节点、多个节点三种场景,释放内存防止泄漏

实现逻辑:

  1. 断言校验二级指针合法 + 链表非空;
  2. 只有一个节点:直接释放并置空头指针;
  3. 多个节点:遍历找到尾节点及其前驱节点,释放尾节点后将前驱节点置空

8.头删

c 复制代码
// 8. 头删
// 功能:删除链表的第一个节点(头节点)
// 参数:pphead 二级指针,用于修改头指针的指向
void SListPopFront(SLTNode** pphead)
{
	// 检查二级指针本身不能为空,防止非法访问
	assert(pphead);

	// 链表不能为空,空链表不能执行删除
	assert(!SListEmpty(*pphead));

	// 先保存头节点的下一个节点地址
	// 因为 free 头节点后就无法再获取 next 了
	SLTNode* next = (*pphead)->next;

	// 释放原头节点的动态内存
	free(*pphead);

	// 让头指针指向原来的第二个节点,成为新的头节点
	*pphead = next;
}

函数功能:删除单链表的第一个节点(头删),释放节点内存并更新头指针,是单链表最高效的删除操作

实现逻辑:

  1. 断言校验二级指针合法、链表非空,防止非法操作;
  2. 先保存第二个节点的地址(避免释放头节点后丢失链表)
  3. 释放原头节点内存
  4. 将头指针更新为第二个节点,成为新的头节点

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

实现逻辑:

  1. 用临时指针从头节点开始遍历链表
  2. 逐个比较节点数据与目标值 x
  3. 匹配成功立即返回当前节点指针
  4. 遍历结束未找到则返回 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

实现逻辑:

  1. 使用临时指针遍历链表,不修改原头指针
  2. 初始化计数器 count = 0
  3. 每遍历一个有效节点,计数器 +1,指针后移
  4. 遍历完成后返回计数器值,即为节点总数

11.返回第一个节点的数据

c 复制代码
// 11. 返回第一个节点的数据
// 功能:获取链表**第一个节点**中存储的数据
// 参数:phead - 链表头指针
// 返回值:第一个节点的数据值
SLTDataType SListFront(SLTNode* phead)
{
	// 断言检查:链表不能为空
	// 如果链表为空(SListEmpty返回true),!true 就是 false,程序会报错终止
	// 作用:防止访问空链表的节点数据,避免程序崩溃
	assert(!SListEmpty(phead));

	// 直接返回头指针指向的节点的数据
	// phead 指向第一个节点,-> 用来访问结构体成员 data
	return phead->data;
}

函数功能:获取并返回单链表 ** 第一个节点(头节点)** 的数据,是链表的 "取队首 / 取栈顶" 类常用接口

实现逻辑:

  1. 用断言判断链表非空,防止空指针访问导致崩溃
    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;
}

函数功能:获取并返回单链表 ** 最后一个节点(尾节点)** 的数据值

实现逻辑:

  1. 断言校验链表非空,避免空指针访问
  2. 定义临时指针从头节点开始遍历
  3. 循环移动指针,直到指向最后一个节点pcur->next == NULL
  4. 返回尾节点的数据

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 之前插入一个新节点,完美处理插在头部和插在中间 / 尾部两种情况,是链表通用插入接口

实现逻辑:

  1. 双重断言保证二级指针、插入位置 pos 合法有效
  2. 如果pos是头节点,直接复用头插函数,代码简洁
  3. 否则遍历找到 pos 的前驱节点
  4. 创建新节点,调整指针完成插入

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 的后面插入一个新节点(后插),是单链表最简单、最高效的插入方式

实现逻辑:

  1. 断言保证 pos 节点有效
  2. 创建新节点
  3. 核心指针操作:新节点先指向pos 的下一个节点,再把 pos 指向新节点
  4. 完成插入,不需要遍历、不需要找前驱

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 的节点,完美处理头节点 / 中间节点 / 尾节点,释放内存并正确修改指针,无内存泄漏、不断链

实现逻辑:

  1. 三重断言保证二级指针、链表非空、pos 有效;
  2. 如果 pos 是头节点,直接复用头删函数;
  3. 否则遍历找到 pos 的前驱节点;
  4. 前驱节点跳过 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 的节点,是面向业务的 "按值删除" 接口,封装了查找和删除两步操作

实现逻辑:

  1. 断言校验二级指针合法、链表非空
  2. 调用 SListFind 查找值为 x 的节点
  3. 如果找到节点,直接调用 ListErase 删除该节点
  4. 如果没找到,不做任何操作

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 的节点(包括头节点、中间节点、尾节点、连续重复节点),彻底清空目标值,释放所有对应节点内存

实现逻辑:

  1. 断言校验二级指针合法、链表非空;
  2. 使用 prev + pcur 双指针遍历链表(前驱 + 当前);
  3. 找到目标节点时:
    a.若是头节点,直接更新头指针
    b.若是中间 / 尾节点,前驱跳过当前节点
  4. 释放节点内存,继续遍历,直到删除所有匹配节点

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;
}

函数功能:将整个单链表的指向完全反转

实现逻辑:

  1. 三个指针的作用:
    prev:记录上一个节点(初始为 NULL
    pcur:记录当前正在处理的节点(从头节点开始)
    next:记录下一个节点(防止链表断裂)
  2. 循环内的 4 步固定操作(核心)
    从头走到尾,每一个节点都做这 4 件事:
    保存后路:next = pcur->next
    先把下一个节点存起来,不然改完指针就找不到后面了
    反转箭头:pcur->next = prev
    最关键一步:让当前节点往回指,指向前面的节点
    prev 前移:prev = pcur
    前驱指针跟上,走到当前节点的位置
    pcur 前移:pcur = next
    当前指针走向之前保存的下一个节点
  3. 循环结束
    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

实现逻辑:

  1. 断言校验链表非空、k 为正整数
  2. 快指针先走 k 步,与慢指针拉开距离
  3. 若快指针提前为空,说明 k 超出链表长度,直接返回NULL
  4. 快慢指针同步向后遍历,快指针为空时慢指针即为倒数第 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;
}

函数功能:合并两个有序单链表,合并后依然保持有序,返回新链表的头指针

实现逻辑:

  1. 处理边界:若其中一个链表为空,直接返回另一个
  2. 创建哨兵位guar 节点,简化头节点处理逻辑
  3. 双指针遍历两个链表,将较小节点依次链接到新链表尾部
  4. 遍历结束后,将剩余未遍历完的链表直接链接到尾部
  5. 释放哨兵节点,返回新链表真实头节点

四、所有代码

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;
}
相关推荐
赫瑞2 小时前
Java中的图论3 —— Floyd
java·开发语言·图论
SilentSlot2 小时前
【数据结构】红黑树定义及基本操作
数据结构
心之语歌2 小时前
Vue2 data + Vue3 ref/reactive 核心知识点总结
开发语言·前端·javascript
关于不上作者榜就原神启动那件事2 小时前
@Transactional事务失效总结
java·开发语言·jvm
jaysee-sjc2 小时前
【项目三】用GUI编程实现局域网群聊软件
java·开发语言·算法·安全·intellij-idea
牢七2 小时前
jfinal_cms-v5.1.0 白盒 nday
开发语言·python
code_whiter3 小时前
C\C++5(内存管理)
c语言·c++
词元Max3 小时前
2.5 Python 类型注解与运行时类型检查
开发语言·python
wangchunting3 小时前
数据结构-树
java·数据结构
HABuo3 小时前
【linux线程(二)】线程互斥、线程同步、条件变量详细剖析
linux·运维·服务器·c语言·c++·ubuntu·centos