开篇介绍:
hello 大家,前几篇博客里,我们一起攻克了不少力扣上的经典链表题,一路跟着练下来的小伙伴们应该都有体会,那些题目大多是稍有难度的综合题,很能锻炼对链表操作的灵活运用。
不过呢,学东西总得循序渐进,基础打牢了,后面才能走得更稳。正巧牛客网里也藏着不少链表基础题,这些题不玩太多花哨的技巧,更贴近初学者的入门节奏,特别适合用来巩固指针操作、边界处理这些核心基本功。
所以在本篇博客中,我们就来好好解析这些题目,把基础再捋得顺顺当当的。
值得一提的是,等这篇内容结束后,我对链表基础题的解析就暂告一段落啦。大家可以期待一下接下来的内容会是什么 —— 咱们很快再见~
下面我们依旧是老样子,亮链接:
牛牛的单向链表_牛客题霸_牛客网https://www.nowcoder.com/practice/95559da7e19c4241b6fa52d997a008c4?tpId=291&tqId=2371704&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page牛牛的链表交换_牛客题霸_牛客网
https://www.nowcoder.com/practice/0e009fba6f3d47f0b5026b5f8b0cb1bc?tpId=291&tqId=2371720&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page牛牛的单链表求和_牛客题霸_牛客网
https://www.nowcoder.com/practice/a674715b6b3845aca0d5009bc27380b5?tpId=291&tqId=2372023&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page牛牛的双链表求和_牛客题霸_牛客网
https://www.nowcoder.com/practice/efb8a1fe3d1f439691e326326f8f8c95?tpId=291&tqId=2372041&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page牛牛的链表添加节点_牛客题霸_牛客网
https://www.nowcoder.com/practice/e33b79c2e15a41f9b541e73cd256124a?tpId=291&tqId=2372135&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page牛牛的链表删除_牛客题霸_牛客网
https://www.nowcoder.com/practice/d3df844baa8a4c139e103ca1b1faae0f?tpId=291&tqId=2372054&sourceUrl=%2Fexam%2Foj%3FquestionJobId%3D10%26subTabName%3Donline_coding_page接下来,就让我们开始我们的解题之旅吧,不过在这里我要先说明一下,以上这些题目都是基础题,而且大部分的知识点,我都有在前面几篇博客中讲到,所以这边是建议大家先去把前面几篇博客都吃透,我在本篇博客中并不会进行太细致入微的讲解,毕竟前几篇有所提及。
CC7 牛牛的单向链表:
这道题,简直可以说是最最基础的链表问题了,我们先看题目:
题意分析:
这道题的核心是 **“数组转单向链表 + 链表遍历输出”**,具体分析如下:
1. 题目核心需求
- 输入:先输入数组长度
n
,再输入n
个整数组成的数组。 - 处理:用这个数组构建一个单向链表(数组元素顺序与链表节点顺序一致)。
- 输出:顺序遍历这个链表,输出每个节点的值。
2. 关键步骤拆解
- 构建链表:需要定义链表节点结构(包含 “值
val
” 和 “指向下一节点的指针next
”),然后依次将数组元素转为节点,通过 “尾插法” 连接成链表(保证节点顺序与数组一致)。 - 遍历输出:从链表的头节点开始,逐个访问节点,输出节点值,直到遇到空节点(链表末尾)。
3. 示例理解(以示例 1 为例)
输入:n=4
,数组 [5,4,2,1]
。
- 构建链表:依次创建值为
5
、4
、2
、1
的节点,每个节点的next
指向下一个节点(形成5→4→2→1→null
的结构)。 - 遍历输出:从第一个节点(值为
5
)开始,依次输出5
、4
、2
、1
。
4. 考察点
这是一道链表基础操作的入门题,主要考察:
- 链表节点的定义与创建;
- 链表的 “尾插法” 构建(保持元素顺序);
- 链表的顺序遍历(访问每个节点并输出)。
适合用来练习链表最基本的 “创建 - 连接 - 遍历” 逻辑,是初学者熟悉链表操作的典型题目。
继续解答:
那么大家,这道题无非就是把之前的从1到n生成链表的题目换成数组罢了,我们本质上还是使用尾插法去创建新链表并不断插入新节点,不懂的可以去看一下这一篇博客:
我们这里就直接给出代码,详细注释版本的,帮助大家理解:
#include // 包含标准输入输出函数库,用于printf、scanf等
#include // 包含动态内存分配函数库,用于malloc、free等
#include // 包含断言函数库,用于assert宏进行调试检查
// 1. 定义数据类型别名
// 将int类型重命名为name1,方便后续统一修改节点存储的数据类型
// 例如若需存储float类型,只需修改为typedef float name1即可
typedef int name1;
// 2. 定义链表节点结构
// 结构体slist表示单向链表的一个节点
struct slist
{
name1 data; // 节点中存储的数据,类型为上面定义的name1(即int)
struct slist* next; // 指向链表中下一个节点的指针,类型为struct slist*
};
// 3. 简化结构体名称
// 为struct slist定义别名sl,后续使用sl即可代表struct slist,简化代码书写
typedef struct slist sl;
// 4. 链表打印函数
// 功能:从链表头节点开始,依次打印所有节点的数据
// 参数:phead - 链表的头指针(指向链表第一个节点)
void slprint(sl* phead)
{
// 断言检查:确保传入的头指针不为NULL
// 若phead为NULL,程序会终止并提示错误位置,用于调试阶段发现问题
assert(phead);
// 定义当前节点指针pucr(可理解为current的缩写),初始指向头节点
// 用这个指针遍历链表,避免直接修改头指针导致链表"丢失"
sl* pucr = phead;
// 遍历循环:当当前节点指针不为NULL时,继续循环(未到达链表末尾)
while (pucr != NULL)
{
// 打印当前节点存储的数据,格式为整数+空格
printf("%d ", pucr->data);
// 将当前节点指针向后移动一位,指向它的下一个节点
// 这一步是遍历的核心,通过指针移动访问链表的每个节点
pucr = pucr->next;
}
}
// 5. 新节点创建函数
// 功能:创建一个新的链表节点,并初始化其数据和指针
// 参数:x - 要存储在新节点中的数据(类型为name1)
// 返回值:新创建节点的指针(sl*类型)
sl* newslist(name1 x)
{
// 动态分配内存:申请一块大小为sl结构体(一个节点)的内存空间
// malloc返回void*类型,需要强制转换为sl*类型
sl* newsl = (sl*)malloc(sizeof(sl));
// 注意:实际开发中应检查内存分配是否成功(newsl是否为NULL)
// 此处简化处理,默认内存分配成功
// 初始化新节点的数据域:将参数x存入data成员
newsl->data = x;
// 初始化新节点的指针域:新节点暂时没有下一个节点,故next指向NULL
newsl->next = NULL;
// 返回新创建节点的指针,供调用者使用
return newsl;
}
// 主函数:程序入口
int main() {
// 6. 读取数组长度
int n; // 用于存储数组的长度
scanf("%d", &n); // 从标准输入读取n的值(用户输入数组长度)
// 7. 创建存储数据的数组
// 动态分配一个能存储n个int类型元素的数组
int* arr = (int*)malloc(n * sizeof(int));
// 8. 读取数组元素
// 循环n次,依次从输入读取数据存入数组arr中
for (int i = 0; i next = newslist(arr[i]);
// ② 将尾指针ptail向后移动,指向刚添加的新节点
// 保证ptail始终指向链表的最后一个节点,方便下次添加新节点
ptail = ptail->next;
}
// 10. 打印链表
// 调用slprint函数,传入头指针phead,打印整个链表的数据
slprint(phead);
// 11. 释放资源
// 释放之前动态分配的数组内存,避免内存泄漏
free(arr);
arr = NULL; // 好习惯:释放后将指针置空,避免野指针
// 注意:此处未释放链表节点的内存(简化处理)
// 实际开发中应遍历链表,逐个释放每个节点的内存
return 0; // 程序正常结束
}
至此,本题结束。
CC9 牛牛的单链表求和
其实,这道题也是很简单,我们先看题目:
题意分析:
这道题的核心是 **“数组转单链表 + 链表节点值求和”**,具体分析如下:
1. 题目核心需求
- 输入:首先输入数组长度
n
,接着输入n
个正整数组成的数组。 - 处理:将这个数组转换为单链表(数组元素顺序与链表节点顺序一致),然后遍历链表,对所有节点的值进行求和。
- 输出:输出链表所有节点值的总和。
2. 关键步骤拆解
- 数组转链表:定义链表节点结构(包含 “值
val
” 和 “指向下一节点的指针next
”),通过 “尾插法” 依次将数组元素转为节点并连接成链表(保证节点顺序与数组一致)。 - 遍历求和:从链表的头节点开始,逐个访问节点,将节点值累加,直到遇到空节点(链表末尾),最终得到总和并输出。
3. 示例理解(以示例 1 为例)
输入:n=5
,数组 [5,2,3,1,1]
。
- 数组转链表:依次创建值为
5
、2
、3
、1
、1
的节点,每个节点的next
指向下一个节点(形成5→2→3→1→1→null
的结构)。 - 遍历求和:从第一个节点(值为
5
)开始,依次累加节点值:5 + 2 + 3 + 1 + 1 = 12
,最终输出12
。
4. 考察点
这是一道链表基础操作 + 简单逻辑计算的题目,主要考察:
- 链表节点的定义、创建与连接(数组转链表的 “尾插法” 实现);
- 链表的顺序遍历(访问每个节点并累加值);
- 基本的求和逻辑与输出。
适合巩固 “链表创建 - 遍历” 的基础流程,同时加入简单的数值计算需求。
继续解答:
那么本题其实思路也很简单,就是我们先创建存储了数组数据的链表,然后再借助sum去对链表中每个节点存储的数据进行相加即可,这也是老生常谈的题目了,我们直接看详细注释版的代码即可,
#include // 引入标准输入输出库,提供printf、scanf等函数
#include // 引入标准库,提供malloc、free等动态内存管理函数
// 1. 定义数据类型别名
// 将int类型重命名为name1,这样如果后续需要修改节点存储的数据类型(如float),只需修改此处即可
typedef int name1;
// 2. 定义单链表节点结构
// 每个节点包含两部分:存储的数据和指向下一个节点的指针
struct slist
{
name1 data; // 节点中存储的具体数据,类型为上面定义的name1(即int)
struct slist* next; // 指针,用于指向链表中的下一个节点,类型为struct slist*
};
// 3. 简化结构体名称
// 为struct slist定义别名sl,后续代码中使用sl即可代表struct slist,减少代码冗余
typedef struct slist sl;
// 4. 创建新链表节点的函数
// 功能:在堆内存中创建一个新的链表节点,并初始化其数据和指针
// 参数:x - 要存入新节点的数据(类型为name1)
// 返回值:指向新创建节点的指针(sl*类型),若内存分配失败可能返回NULL(此处简化处理)
sl* newslist(name1 x)
{
// 调用malloc函数分配一块大小为sl结构体(一个节点)的内存空间
// 由于malloc返回void*类型,需要强制转换为sl*类型才能赋值给sl*类型的指针
sl* newsl = (sl*)malloc(sizeof(sl));
// 将传入的参数x存入新节点的数据域(data成员)
newsl->data = x;
// 新节点刚创建时没有后续节点,因此将指针域(next成员)初始化为NULL
newsl->next = NULL;
// 返回指向新节点的指针,供调用者使用
return newsl;
}
// 5. 计算链表所有节点数据之和并输出的函数
// 功能:从链表头节点开始遍历所有节点,累加每个节点的数据,最后输出总和
// 参数:phead - 链表的头指针(指向链表的第一个节点)
void slistadd(sl* phead)
{
// 定义sum变量用于存储累加的总和,初始值为0
name1 sum = 0;
// 遍历链表的循环:当phead不为NULL时(即还未到达链表末尾),继续循环
while (phead != NULL)
{
// 将当前节点的数据(phead->data)累加到sum中
sum = sum + phead->data;
// 将phead指针向后移动一位,指向当前节点的下一个节点
// 这是遍历链表的核心操作,通过指针移动访问下一个节点
phead = phead->next;
}
// 遍历结束后,sum中存储了所有节点数据的总和,通过printf输出
printf("%d", sum);
}
// 主函数:程序的入口点,负责接收输入、构建链表、调用求和函数等核心流程
int main() {
// 6. 接收用户输入的数组长度
int n; // 声明变量n,用于存储数组的长度
scanf("%d", &n); // 通过scanf函数从标准输入(键盘)读取n的值,&n表示取n的地址
// 7. 动态分配数组内存
// 调用malloc函数分配一块能存储n个int类型元素的内存空间,将返回的指针强制转换为int*类型
int* arr = (int*)malloc(n * sizeof(int));
// 8. 接收用户输入的数组元素
// 循环n次,依次读取n个整数存入数组arr中
for (int i = 0; i next = newslist(arr[i]);
// ② 将尾指针ptail向后移动,指向刚添加的新节点
// 保证ptail始终指向链表的最后一个节点,以便下一次添加新节点时直接使用
ptail = ptail->next;
}
// 10. 调用求和函数计算并输出链表所有节点数据的总和
// 将头指针phead传给slistadd函数,该函数会从表头开始遍历并求和
slistadd(phead);
// 11. 释放动态分配的资源,避免内存泄漏
free(arr); // 释放之前为数组分配的内存
arr = NULL; // 将指针置空,避免成为野指针(指向已释放内存的指针)
// 注意:此处未释放链表节点的内存(题目简化处理)
// 实际开发中,需要遍历链表并逐个释放每个节点的内存,避免内存泄漏
return 0; // 主函数返回0,表示程序正常结束
}
又一题结束。
CC10 牛牛的双链表求和
这道题其实和上一题类似,只是多了一个链表,让我们对两个链表的数据进行相加罢了,我们看题目:
题意分析:
这道题的核心是 **“双数组转双链表 + 对应位置节点值相加 + 输出结果链表”**,具体分析如下:
1. 题目核心需求
- 输入:
- 第一行输入正整数
n
,表示两个数组的长度(两个数组长度相同)。 - 第二行输入
n
个正整数,作为数组a
的元素。 - 第三行输入
n
个正整数,作为数组b
的元素。
- 第一行输入正整数
- 处理:
- 将数组
a
转换为链表a
,数组b
转换为链表b
(数组元素顺序与链表节点顺序一致)。 - 按顺序将链表
a
中每个节点的值,加到链表b
对应位置节点的值上(即链表a
的第i
个节点值,加到链表b
的第i
个节点值上)。
- 将数组
- 输出:输出相加后的链表
b
的所有节点值。
2. 关键步骤拆解
- 双数组转双链表:分别为数组
a
和数组b
定义链表结构,通过 “尾插法” 依次将数组元素转为节点并连接成两个链表(保证节点顺序与数组一致)。 - 对应节点值相加:同时遍历两个链表,将链表
a
节点的值累加到链表b
对应位置节点的值上(需保证两个链表同步遍历,因为长度相同)。 - 输出结果链表:遍历处理后的链表
b
,输出每个节点的值。
3. 示例理解(以示例 1 为例)
输入:
n=5
- 数组
a
:[5,4,2,1,3]
- 数组
b
:[2,4,5,8,9]
处理过程:
- 数组转链表:链表
a
为5→4→2→1→3→null
;链表b
为2→4→5→8→9→null
。 - 对应节点相加:
- 第 1 个节点:
5 + 2 = 7
- 第 2 个节点:
4 + 4 = 8
- 第 3 个节点:
2 + 5 = 7
- 第 4 个节点:
1 + 8 = 9
- 第 5 个节点:
3 + 9 = 12
- 第 1 个节点:
- 输出链表
b
:7→8→7→9→12→null
,最终输出7 8 7 9 12
。
4. 考察点
这是一道双链表操作 + 同步遍历与数值计算的题目,主要考察:
- 双链表的定义、创建与连接(两个数组分别转链表的 “尾插法” 实现);
- 双链表的同步遍历(保证两个链表同时访问对应位置节点);
- 节点值的相加逻辑与结果链表的输出。
需要注意链表长度一致的前提,以及同步遍历时指针的协调操作
继续解答:
所以,经过上述分析我们就知道,本题也是非常简单,只不过我们要注意的是把哪一个链表的数据加到哪一个链表的数据中,我们依旧是直接上代码:
#include // 引入标准输入输出库,提供printf、scanf等输入输出函数
#include // 引入标准库,提供malloc、free等动态内存分配与释放函数
// 1. 定义数据类型别名
// 将int类型重命名为name1,这样如果后续后续需要修改节点存储的数据类型(如float),只需修改此处即可
typedef int name1;
// 2. 定义单链表节点结构并简化名称
// 使用typedef将结构体struct slist直接重命名为sl,简化后续代码中的类型声明
typedef struct slist
{
name1 data; // 节点的数据域,用于存储具体数值,类型为name1(即int)
struct slist* next; // 节点的指针域,用于指向链表中的下一个节点,类型为struct slist*
}sl;
// 3. 创建新链表节点的函数
// 功能:在堆内存中申请一块空间用于存储新节点,并初始化节点的数据和指针
// 参数:x - 要存储在新节点中的数据(类型为name1)
// 返回值:指向新创建节点的指针(sl*类型),若内存分配失败可能返回NULL(此处简化处理)
sl* createsl(name1 x)
{
// 调用malloc函数分配一块大小为sl结构体(一个节点)的内存空间
// malloc返回void*类型,需要强制转换为sl*类型才能赋值给sl*类型的指针变量
sl* newsl = (sl*)malloc(sizeof(sl));
// 将传入的参数x赋值给新节点的数据域,完成数据初始化
newsl->data = x;
// 新节点刚创建时没有后续节点,因此将指针域初始化为NULL
newsl->next = NULL;
// 返回指向新节点的指针,供调用者使用
return newsl;
}
// 4. 打印链表所有节点数据的函数
// 功能:从链表的头节点开始,依次遍历每个节点并打印其数据
// 参数:phead - 链表的头指针(指向链表的第一个节点)
void slprint(sl* phead)
{
// 定义临时指针temp并初始化为头指针phead
// 使用临时指针遍历可以避免修改原头指针,防止链表"丢失"
sl* temp = phead;
// 遍历循环:当temp不为NULL时(即当前节点存在,未到达链表末尾),继续循环
// while(temp) 等价于 while(temp != NULL),是一种简洁的写法
while (temp)
{
// 打印当前节点的数据域的值,格式为"整数 + 空格"
printf("%d ", temp->data);
// 将临时指针temp向后移动一位,指向当前节点的下一个节点
// 这是遍历链表的核心操作,通过指针移动实现对每个节点的访问
temp = temp->next;
}
}
// 5. 两个链表对应节点值相加的函数
// 功能:同步遍历链表a和链表b,将链表a中每个节点的值累加到链表b对应位置的节点上
// 参数:pheada - 链表a的头指针;pheadb - 链表b的头指针
// 说明:题目保证两个链表长度相同,因此无需处理长度不一致的情况
void sladd(sl* pheada, sl* pheadb)
{
// 同步遍历两个链表:当两个指针都不为NULL时(即两个链表都未遍历结束),继续循环
while (pheada != NULL && pheadb != NULL)
{
// 将链表a当前节点的数据(pheada->data)累加到链表b当前节点的数据(pheadb->data)上
pheadb->data += pheada->data;
// 将链表a的指针向后移动一位,指向它的下一个节点
pheada = pheada->next;
// 将链表b的指针向后移动一位,指向它的下一个节点
// 两个指针同步移动,保证访问的是对应位置的节点
pheadb = pheadb->next;
}
}
// 主函数:程序的入口点,负责接收输入数据、创建链表、调用核心功能函数
int main() {
// 6. 读取两个数组的长度(题目说明两个数组长度相同)
int n; // 声明变量n,用于存储数组的长度
scanf("%d", &n); // 通过scanf函数从标准输入(键盘)读取n的值,&n表示取n的地址
// 7. 为第一个数组(数组a)分配内存并读取数据
// 调用malloc函数动态分配一块能存储n个int类型元素的内存空间
// 将返回的指针强制转换为int*类型后赋值给指针变量a
int* a = (int*)malloc(n * sizeof(int));
// 循环n次,依次从输入读取n个整数并存入数组a中
for (int i = 0; i next = createsl(a[i]);
// ② 将尾指针ptaila向后移动,指向刚添加的新节点
// 保证ptaila始终指向链表a的最后一个节点,方便下次添加新节点
ptaila = ptaila->next;
}
// 10. 根据数组b创建链表b(与创建链表a的逻辑完全相同)
// 10.1 创建链表b的头节点:使用数组b的第一个元素(b[0])
sl* pheadb = createsl(b[0]);
// 定义链表b的尾指针ptailb,初始时指向头节点
sl* ptailb = pheadb;
// 10.2 循环创建链表b的剩余节点(从数组b的第二个元素开始)
for (int i = 1; i next = createsl(b[i]);
// ② 将尾指针ptailb向后移动到新节点
ptailb = ptailb->next;
}
// 11. 调用sladd函数,将链表a的节点值累加到链表b对应节点上
sladd(pheada, pheadb);
// 12. 调用slprint函数,打印相加后的链表b的所有节点数据
slprint(pheadb);
// 13. 释放动态分配的内存资源,避免内存泄漏
free(a); // 释放为数组a分配的内存
free(b); // 释放为数组b分配的内存
a = NULL; // 将指针a置空,避免成为野指针(指向已释放内存的指针)
b = NULL; // 将指针b置空,同样避免野指针
// 注意:此处未释放两个链表中节点的内存(题目简化处理)
// 在实际开发中,需要遍历两个链表,逐个释放每个节点的内存,否则会造成内存泄漏
return 0; // 主函数返回0,表示程序正常结束
}
大功告成。
CC11 牛牛的链表删除
本题和之前所讲的leetcode中的题目一样,大家可以直接去看这一篇博客:对于单链表相关经典算法题:203. 移除链表元素的解析-CSDN博客
在这里我就只给出详细代码:
#include // 引入标准输入输出库,提供printf、scanf等函数
#include // 引入标准库,提供malloc、free等内存管理函数
#include // 引入断言库,用于调试时的参数校验
// 定义数据类型别名,方便后续修改节点存储的数据类型
typedef int name1;
// 定义单链表节点结构
typedef struct slist
{
name1 data; // 节点存储的数据
struct slist* next; // 指向链表下一个节点的指针
} sl;
/**
* 创建新的链表节点
* @param x 要存储在新节点中的数据
* @return 指向新创建节点的指针,内存分配失败时返回NULL
*/
sl* createsl(name1 x)
{
// 为新节点分配内存空间
sl* newsl = (sl*)malloc(sizeof(sl));
// 检查内存分配是否成功
if (newsl == NULL)
{
printf("内存分配失败\n");
return NULL;
}
// 初始化节点数据
newsl->data = x;
// 新节点初始没有后续节点,指针置空
newsl->next = NULL;
return newsl;
}
/**
* 打印链表中所有节点的数据
* @param phead 链表的头指针
*/
void slprint(sl* phead)
{
// 遍历链表,当前节点不为空时继续
while (phead != NULL)
{
// 打印当前节点的数据
printf("%d ", phead->data);
// 移动到下一个节点
phead = phead->next;
}
}
/**
* 删除链表中指定位置的节点
* @param phead 链表的头指针
* @param position 要删除的节点指针
* @note 该函数假设position是链表中的有效节点,且phead不为空
*/
void slbreak(sl* phead, sl* position)
{
// 断言确保传入的指针有效
assert(phead != NULL && position != NULL);
// 找到要删除节点的前一个节点
sl* temp = phead;
while (temp->next != position)
{
temp = temp->next;
// 防止position不在链表中导致无限循环
assert(temp != NULL);
}
// 保存要删除节点的下一个节点地址
sl* temp1 = position->next;
// 将前一个节点的指针指向要删除节点的下一个节点,完成删除
temp->next = temp1;
// 释放被删除节点的内存,避免内存泄漏
free(position);
position = NULL; // 避免野指针
}
/**
* 从链表中删除所有值为x的节点
* @param phead 链表的头指针的指针(使用二级指针以处理头节点被删除的情况)
* @param x 要删除的节点值
*/
void slfinderase(sl** phead, name1 x)
{
// 断言确保头指针的指针有效
assert(phead != NULL);
// 处理头节点为空的情况
if (*phead == NULL)
return;
// 临时指针用于遍历链表
sl* position = *phead;
// 前驱指针,用于处理头节点被删除的情况
sl* prev = NULL;
while (position != NULL)
{
// 找到值为x的节点
if (position->data == x)
{
// 保存当前节点的下一个节点地址
sl* nextNode = position->next;
// 特殊处理头节点
if (position == *phead)
{
// 更新头指针
*phead = nextNode;
// 释放原头节点内存
free(position);
}
else
{
// 非头节点,通过前驱节点删除
prev->next = nextNode;
free(position);
}
// 移动到下一个节点
position = nextNode;
}
else
{
// 不是要删除的节点,更新前驱指针并移动到下一个节点
prev = position;
position = position->next;
}
}
}
/**
* 释放整个链表占用的内存
* @param phead 链表的头指针的指针
*/
void freeList(sl** phead)
{
if (phead == NULL || *phead == NULL)
return;
sl* current = *phead;
sl* next = NULL;
// 逐个释放节点
while (current != NULL)
{
next = current->next;
free(current);
current = next;
}
// 头指针置空,避免野指针
*phead = NULL;
}
int main() {
int n, x;
// 读取数组长度n和要删除的值x
scanf("%d%d", &n, &x);
// 为数组分配内存
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL)
{
printf("内存分配失败\n");
return 1; // 异常退出
}
// 读取数组元素
for (int i = 0; i next = newNode;
ptail = ptail->next;
}
// 从链表中删除所有值为x的节点
// 使用二级指针传递头指针,以处理头节点被删除的情况
slfinderase(&phead, x);
// 打印处理后的链表
slprint(phead);
// 释放资源
free(arr); // 释放数组内存
arr = NULL;
freeList(&phead); // 释放链表内存
return 0; // 程序正常结束
}
至此,又一题被我们解决了。
CC12 牛牛的链表添加节点
这一题,才算是加上了一丢丢难度,但是也不难,我们看题目:
题意分析:
这道题的核心是 **“数组转链表 + 在指定位置后插入新节点”**,具体分析如下:
1. 题目核心需求
- 输入:
- 第一行输入两个正整数
n
和i
,n
是数组长度,i
是要添加新节点的位置(在第i
个节点后插入)以及新节点的值(值为i
)。 - 第二行输入
n
个正整数,作为数组的元素。
- 第一行输入两个正整数
- 处理:
- 将数组转换为单链表(数组元素顺序与链表节点顺序一致)。
- 在链表的第
i
个节点后面,插入一个值为i
的新节点(节点计数从 1 开始)。
- 输出:输出插入新节点后的完整链表。
2. 关键步骤拆解
- 数组转链表:定义链表节点结构(包含 “值
val
” 和 “指向下一节点的指针next
”),通过 “尾插法” 依次将数组元素转为节点并连接成链表(保证节点顺序与数组一致)。 - 定位插入位置:遍历链表,找到第
i
个节点。 - 插入新节点:在第
i
个节点后,插入值为i
的新节点(需处理 “找到节点后,修改指针连接新节点” 的逻辑)。
3. 示例理解(以示例 1 为例)
输入:
n=5
,i=3
- 数组:
[5,4,8,6,3]
处理过程:
- 数组转链表:链表为
5→4→8→6→3→null
。 - 定位第
3
个节点:值为8
的节点。 - 插入新节点:在值为
8
的节点后,插入值为3
的新节点,链表变为5→4→8→3→6→3→null
。 - 输出链表:
5 4 8 3 6 3
。
4. 考察点
这是一道链表插入操作的题目,主要考察:
- 链表节点的定义、创建与连接(数组转链表的 “尾插法” 实现);
- 链表的遍历(定位到第
i
个节点); - 链表的插入操作(修改指针,在指定节点后插入新节点,需注意指针的修改顺序,避免断链)。
继续解答:
看完上面的分析之后,大家应该已经对这道题知根知底了,但是为什么我会说这道题稍微有一点难度呢,那是因为在于我们要去正确的找到第i个节点,并且顺利的在链表的第i个节点后插入存储i的新节点,那么细节也就出现在这里了,我们要怎么去找到第i个节点呢?
其实这个问题与我们之前在对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析-CSDN博客这一篇博客中讲的寻找中间节点的题目很像,大家可以去看看里面half的移动,其与本题一样,我这里也就不赘述,我们直接看寻找第i个节点的代码:
sl* slfind(sl* phead,name1 i)
{
i = i-1;//点睛之笔,才能找到第i个节点
sl* position=phead;
while(i)
{
position=position->next;
i--;
}
//出来之后的position就是第i个节点
return position;
}
下面是详细注释版本:
/**
* 查找链表中指定位置的节点(节点位置从1开始计数)
* @param phead 链表的头指针,指向链表的第一个节点
* @param i 要查找的节点位置(正整数,从1开始)
* @return 指向第i个节点的指针;若i无效(如大于链表长度),可能返回野指针(需提前调用前确保i有效)
*/
sl* slfind(sl* phead, name1 i)
{
// 核心转换:将"从1开始的位置"转为"从0开始的偏移量"
// 例如:要找第1个节点 → 偏移量为0(无需移动指针)
// 要找第2个节点 → 偏移量为1(移动1次指针)
// 要找第5个节点 → 偏移量为4(移动4次指针)
i = i - 1;
// 定义位置指针position,初始指向链表的第一个节点(第1个节点)
// 此时position指针指向的是潜在的"第1个节点"
sl* position = phead;
// 循环移动指针,次数为转换后的i值(偏移量)
// 当i为0时,循环不执行,直接返回初始节点(第1个节点)
while (i) // 等价于 while(i != 0)
{
// 将position指针指针向后指针向后移动一位,指向当前节点的下一个节点
// 每执行一次,指针向链表尾部移动一个节点
position = position->next;
// 偏移量减1,控制循环执行次数
i--;
}
// 循环结束后,position指针恰好指向目标节点
// 例如:初始i=3(找第3个节点)→ 转换后i=2 → 循环2次 → 指针移动2次 → 指向第3个节点
return position;
}
大家看了之后就能理解了,解决了这一个小细节之后,本题也就毫无难度可言了,我们看本题的完整详细注释版代码:
#include // 引入标准输入输出库,提供printf、scanf等函数
#include // 引入标准库,提供malloc等内存分配函数
// 1. 定义数据类型别名
// 将int类型重命名为name1,方便后续统一修改节点存储的数据类型
typedef int name1;
// 2. 定义单链表节点结构
// 每个节点包含数据域和指向下一节点的指针域
typedef struct slist
{
name1 data; // 节点存储的数据,类型为name1(即int)
struct slist* next; // 指向链表中下一个节点的指针
} sl;
/**
* 创建新的链表节点
* @param x 要存储在新节点中的数据
* @return 指向新创建节点的指针,内存分配失败时可能返回NULL
*/
sl* createsl(name1 x)
{
// 动态分配一个节点大小的内存空间
// malloc返回void*类型,此处处省略强制转换(C语言允许隐式转换)
sl* newsl = malloc(sizeof(sl));
// 初始化节点的数据域为x
newsl->data = x;
// 新节点初始没有后续节点,指针域置为NULL
newsl->next = NULL;
// 返回新节点的指针
return newsl;
}
/**
* 打印链表中所有节点的数据
* @param phead 链表的头指针(指向第一个节点)
*/
void slprint(sl* phead)
{
// 定义临时指针temp指向头节点,避免修改原头指针
sl* temp = phead;
// 遍历链表:当temp不为NULL时(未到达链表末尾)
while (temp) // 等价于 while(temp != NULL)
{
// 打印当前节点的数据,后跟空格
printf("%d ", temp->data);
// 移动临时指针到下一个节点
temp = temp->next;
}
}
/**
* 查找链表中第i个节点(节点位置从1开始计数)
* @param phead 链表的头指针
* @param i 要查找的节点位置(正整数)
* @return 指向第i个节点的指针(假设i是有效的,即不超过链表长度)
*/
sl* slfind(sl* phead, name1 i)
{
// 关键转换:将"从1开始的位置"转为"从0开始的偏移量"
// 例如:查找第1个节点 → i变为0(无需移动指针)
// 查找第3个节点 → i变为2(需要移动2次指针)
i = i - 1;
// 定义位置指针position,初始指向头节点(第1个节点)
sl* position = phead;
// 循环移动指针i次,定位到目标节点
while (i) // 当i>0时继续循环
{
// 指针向后移动一位,指向当前节点的下一个节点
position = position->next;
// 偏移量减1,控制循环次数
i--;
}
// 循环结束后,position指向第i+1个节点(即原需求的第i个节点)
return position;
}
/**
* 在链表的指定位置后插入新节点
* @param phead 链表的头指针(此处未使用,保留参数可用于扩展)
* @param position 要插入位置的前一个节点(在该节点后插入新节点)
* @param i 新节点要存储的数据(题目要求值为idx)
*/
void sladd(sl* phead, sl* position, name1 i)
{
// 创建新节点,数据为i
sl* newsl = createsl(i);
// 步骤1:新节点的next指针指向position节点的下一个节点
// 保存position原本的后续节点,避免链表断裂
newsl->next = position->next;
// 步骤2:position节点的next指针指向新节点
// 完成新节点的插入
position->next = newsl;
}
int main() {
// 3. 读取输入数据
int n, idx;
// 读取数组长度n和要插入的位置idx(在第idx个节点后插入)
scanf("%d%d", &n, &idx);
// 4. 读取数组元素
// 动态分配存储n个整数的数组
int* arr = (int*)malloc(n * sizeof(int));
// 循环读取n个整数存入数组
for (int i = 0; i next = createsl(arr[i]);
// 尾指针后移到新创建的节点,保持始终指向链表末尾
ptail = ptail->next;
}
// 6. 执行插入操作
// 6.1 查找第idx个节点的位置
sl* position = slfind(phead, idx);
// 6.2 在该节点后插入新节点,新节点的数据为idx
sladd(phead, position, idx);
// 7. 打印插入后的链表
slprint(phead);
// 8. 释放资源
free(arr); // 释放数组占用的内存
arr = NULL; // 指针置空,避免野指针
// 注意:实际开发中需遍历链表释放所有节点的内存(题目简化处理)
return 0; // 程序正常结束
}
CC8 牛牛的链表交换
个人觉得本题算是这几题里面唯一有点难度的题目,我们先看题目:
题意分析:
这道题的核心是 **“数组转链表 + 交换链表首尾特定节点”**,具体分析如下:
1. 题目核心需求
- 输入:
- 第一行输入正整数
n
,表示数组的长度。 - 第二行输入
n
个正整数,作为数组的元素。
- 第一行输入正整数
- 处理:
- 将数组转换为单链表(数组元素顺序与链表节点顺序一致)。
- 交换链表的前两个节点的位置。
- 交换链表的最后两个节点的位置。
- 输出:输出交换节点后的完整链表。
2. 关键步骤拆解
- 数组转链表:定义链表节点结构(包含 “值
val
” 和 “指向下一节点的指针next
”),通过 “尾插法” 依次将数组元素转为节点并连接成链表(保证节点顺序与数组一致)。 - 交换前两个节点:需处理 “头节点变更” 的情况(若原链表头节点是
node1
,交换后node2
成为新头节点,node1
接在node2
之后)。 - 交换最后两个节点:需先遍历找到倒数第二个节点和最后一个节点,再修改它们的指针(让倒数第二个节点的
next
指向null
,最后一个节点的next
指向倒数第二个节点,并让倒数第三个节点的next
指向最后一个节点)。
3. 示例理解
- 示例 1:
输入:n=4
,数组[2,3,4,5]
。
数组转链表:2→3→4→5→null
。
交换前两个节点:3→2→4→5→null
。
交换最后两个节点:3→2→5→4→null
,输出3 2 5 4
。 - 示例 2:
输入:n=3
,数组[3,2,1]
。
数组转链表:3→2→1→null
。
交换前两个节点:2→3→1→null
。
交换最后两个节点:2→1→3→null
,输出2 1 3
。
4. 考察点
这是一道链表节点交换的题目,主要考察:
- 链表节点的定义、创建与连接(数组转链表的 “尾插法” 实现);
- 链表节点交换的指针操作(前两个节点交换需处理头节点变更,最后两个节点交换需定位倒数节点并修改指针);
- 边界情况处理(如链表长度为 2 时,前两个和最后两个节点是同一组,交换一次即可;链表长度小于 2 时,无需交换)。
继续解答:
经过上面的分析,我们知道,本题是要求把一个链表的第一二个节点互相交换,然后再去把倒一倒二个节点进行互换,大家要注意先后顺序哦。
那么知道了之后,其实也就不难了这一道题目,我们的思路就是:
使用二级指针(即传址调用),去把第二个节点赋值给第一个节点,要记得节点中next指针的变换哦,然后进行了这一步之后,我们再去把链表的倒一倒二个节点进行互换,这里便不需要传址调用,我们只需注意节点中next指针的改变就行。
但是在倒一倒二的节点的寻找中,我们肯定是要借助循环去遍历的,同时我们也要找到倒三个节点,去对其next指针的改变,那么我们要怎么去找到倒三个节点呢?具体如下:
// 定位指针用于定位链表的最后三个节点
sl* p1 = *pphead; // p1 最终指向最后一个节点
sl* p2 = *pphead; // p2 最终指向倒数第二个节点
sl* p3 = NULL; // p3 最终指向倒数第三个节点(若存在)
// 遍历链表,定位最后三个节点
// 循环条件:当p1的next不为NULL时,说明p1不是最后一个节点
while (p1->next != NULL)
{
p3 = p2; // p3 移动到当前p2的位置(为p2后移做准备)
p2 = p1; // p2 移动到当前p1的位置
p1 = p1->next; // p1 向后移动一个节点
}
以下是其原理:
这段代码的核心逻辑是通过一次遍历,准确定链表中最后三个节点的位置,为后续交换最后两个节点做准备。下面是详细的逻辑拆解:
核心目标
找到链表的:
- 最后一个节点(
p1
) - 倒数第二个节点(
p2
) - 倒数第三个节点(
p3
,若存在)
初始状态
sl* p1 = *pphead; // p1 初始指向头节点(第一个节点)
sl* p2 = *pphead; // p2 初始指向头节点(第一个节点)
sl* p3 = NULL; // p3 初始为 NULL(暂时无指向)
pphead
是链表头指针的地址(二级指针),*pphead
就是链表的头节点。- 三个指针的初始状态:
p1
和p2
都指向头节点,p3
为空(因为还没有遍历到第三个节点)。
遍历过程(核心逻辑)
while (p1->next != NULL) // 循环条件:p1 不是最后一个节点
{
p3 = p2; // 步骤1:p3 移动到当前 p2 的位置
p2 = p1; // 步骤2:p2 移动到当前 p1 的位置
p1 = p1->next; // 步骤3:p1 向后移动一个节点(探索下一个节点)
}
遍历步骤拆解
以链表 A→B→C→D→E→NULL
(共 5 个节点)为例,跟踪指针移动过程:
初始状态(循环前):
p1
指向A
(头节点)p2
指向A
p3
为NULL
第 1 次循环(
p1->next
是B
,非空):p3 = p2
→p3
指向A
p2 = p1
→p2
指向A
p1 = p1->next
→p1
指向B
- 结果:
p1=B, p2=A, p3=A
第 2 次循环(
p1->next
是C
,非空):p3 = p2
→p3
指向A
p2 = p1
→p2
指向B
p1 = p1->next
→p1
指向C
- 结果:
p1=C, p2=B, p3=A
第 3 次循环(
p1->next
是D
,非空):p3 = p2
→p3
指向B
p2 = p1
→p2
指向C
p1 = p1->next
→p1
指向D
- 结果:
p1=D, p2=C, p3=B
第 4 次循环(
p1->next
是E
,非空):p3 = p2
→p3
指向C
p2 = p1
→p2
指向D
p1 = p1->next
→p1
指向E
- 结果:
p1=E, p2=D, p3=C
循环结束(
p1->next
是NULL
,p1
到达最后一个节点):- 最终指针位置:
p1
指向E
(最后一个节点)p2
指向D
(倒数第二个节点)p3
指向C
(倒数第三个节点)
- 最终指针位置:
最终结果
循环结束后:
p1
一定指向最后一个节点(因为循环在p1->next == NULL
时停止)。p2
指向倒数第二个节点(始终比p1
慢一步)。p3
指向倒数第三个节点(始终比p2
慢一步,若链表长度<3 则仍为NULL
)。
作用
通过这种 “三指针接力” 的遍历方式,只需一次遍历就能准确定位链表末尾的三个节点,为后续 “交换最后两个节点” 提供指针基础:
- 交换
p1
(最后一个节点)和p2
(倒数第二个节点)的位置时,需要p3
(倒数第三个节点)的指针来修改链表连接(p3->next
需指向新的倒数第二个节点)。
如此大家应该就能明白了。
那么到了这里,本题也就没有什么难度了,我们直接上完整注释版的详细代码:
#include // 引入标准输入输出库,提供printf、scanf等函数
#include // 引入标准库,提供malloc、free等内存管理函数
#include // 引入断言库,用于调试时的参数有效性检查
// 1. 定义数据类型别名
// 将int类型重命名为name1,方便后续统一修改节点存储的数据类型(如改为float)
typedef int name1;
// 2. 定义单链表节点结构
struct slist
{
name1 data; // 节点存储的数据,类型为name1(即int)
struct slist* next; // 指向链表中下一个节点的指针
};
// 为结构体struct slist定义别名sl,简化代码书写
typedef struct slist sl;
/**
* 创建新的链表节点
* @param x 要存储在新节点中的数据
* @return 指向新节点的指针,内存分配失败时返回NULL
*/
sl* newslist(name1 x)
{
// 动态分配一个节点大小的内存空间
sl* newsl = (sl*)malloc(sizeof(sl));
// 检查内存分配是否成功(健壮性处理)
if (newsl == NULL)
{
printf("内存分配失败,无法创建新节点\n");
return NULL;
}
// 初始化节点的数据域
newsl->data = x;
// 新节点初始无后续节点,指针域置为NULL
newsl->next = NULL;
return newsl;
}
/**
* 交换链表的前两个节点
* @param pphead 链表头指针的地址(二级指针)
* @note 仅当链表至少包含两个节点时有效
*/
void slswapfront(sl** pphead)
{
// 断言检查:确保二级指针有效、头节点存在、且至少有两个节点
// 避免空指针访问或对短链表执行无效操作
assert(pphead != NULL && *pphead != NULL && (*pphead)->next != NULL);
// 保存第一个节点的地址(原头节点)
sl* temp1 = *pphead;
// 保存第二个节点的地址
sl* temp2 = (*pphead)->next;
// 保存第三个节点的地址(可能为NULL,若链表只有两个节点)
sl* temp3 = temp2->next;
// 步骤1:第二个节点的next指针指向第一个节点
// 此时链表关系:temp2 → temp1
temp2->next = temp1;
// 步骤2:更新头指针(通过二级指针修改外部头节点)
// 让头指针指向第二个节点,此时temp2成为新的头节点
*pphead = temp2;
// 步骤3:第一个节点的next指针指向原来的第三个节点
// 完成交换后:temp2 → temp1 → temp3(原第三个节点及后续)
temp1->next = temp3;
}
/**
* 交换链表的最后两个节点
* @param pphead 链表头指针的地址(二级指针)
* @note 仅当链表至少包含两个节点时有效
*/
void slswapback(sl** pphead)
{
// 断言检查:确保二级指针有效、头节点存在、且至少有两个节点
assert(pphead != NULL && *pphead != NULL && (*pphead)->next != NULL);
// 初始化三个指针,用于定位链表尾部节点
sl* p1 = *pphead; // 最终指向最后一个节点
sl* p2 = *pphead; // 最终指向倒数第二个节点
sl* p3 = NULL; // 最终指向倒数第三个节点(若存在)
// 遍历链表,定位最后三个节点
// 循环条件:p1的next不为NULL(即p1不是最后一个节点)
while (p1->next != NULL)
{
p3 = p2; // p3跟进到p2当前位置
p2 = p1; // p2跟进到p1当前位置
p1 = p1->next; // p1向前移动一个节点
}
// 循环结束后:
// p1 = 最后一个节点,p2 = 倒数第二个节点,p3 = 倒数第三个节点
// 步骤1:倒数第三个节点的next指向最后一个节点
p3->next = p1;
// 步骤2:最后一个节点的next指向原来的倒数第二个节点
p1->next = p2;
// 步骤3:原来的倒数第二个节点的next置为NULL(成为新的尾节点)
p2->next = NULL;
}
/**
* 打印链表所有节点的数据
* @param phead 链表的头指针
*/
void slprint(sl* phead)
{
// 定义遍历指针pucr(可理解为"current"的缩写),初始指向头节点
sl* pucr = phead;
// 遍历链表:当pucr不为NULL时(未到达链表末尾)
while (pucr != NULL)
{
// 打印当前节点的数据,后跟空格
printf("%d ", pucr->data);
// 移动指针到下一个节点
pucr = pucr->next;
}
}
int main() {
// 3. 读取输入数据
int n; // 存储数组长度
scanf("%d", &n); // 从输入读取数组长度n
// 4. 读取数组元素
// 动态分配存储n个int元素的数组
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) // 检查数组内存分配是否成功
{
printf("内存分配失败,无法创建数组\n");
return 1; // 异常退出程序
}
// 循环读取n个整数存入数组
for (int i = 0; i next = newNode; // 将新节点连接到链表尾部
ptail = ptail->next; // 尾指针后移到新节点
}
// 6. 准备二级指针(头指针的地址)
// pphead是指向头指针phead的指针,用于在函数中修改phead的值
sl** pphead = &phead;
// 7. 执行交换操作(仅当链表长度≥2时)
if (n >= 2)
{
slswapfront(pphead); // 交换前两个节点
slswapback(pphead); // 交换最后两个节点
}
// 8. 打印交换后的链表
slprint(*pphead); // *pphead即当前的头指针
// 9. 释放资源
free(arr); // 释放数组内存
arr = NULL; // 指针置空,避免野指针
// 注意:实际开发中需遍历链表,释放所有节点的内存(此处简化处理)
return 0; // 程序正常结束
}
到此,本题也是大功告成。
结语:
到这里,牛客上这几道链表基础题就都解析完了。从最简单的数组转链表、遍历输出,到稍复杂的节点插入、删除,再到需要仔细处理指针关系的节点交换,每一道题都像一块拼图,拼出了链表操作的核心脉络 —— 指针的移动、节点的连接、边界的考量。
其实回过头看,这些题目本身并没有太多高深的技巧,更多的是对 “基本功” 的打磨。就像练武功要先扎马步,学链表也得把这些基础操作练到熟练于心:尾插法建链表时,尾指针的步步跟进;查找节点时,偏移量的巧妙转换;交换节点时,指针指向的小心调整…… 这些细节看似琐碎,却是日后解决更复杂链表问题的底气。
可能有小伙伴会觉得,反复练这些基础题有点枯燥,但正是这种 “枯燥” 的积累,才能让我们在面对更难的题目时,一眼看穿本质,快速找到思路。就像盖房子,地基打得越牢,往上添砖加瓦时才越稳。
随着这篇内容的结束,我们的链表基础题解析也暂时告一段落了。但这绝不是结束,而是新的开始 —— 接下来,我们会朝着更有挑战的内容进发,把这些基础能力运用到更复杂的场景中去。
感谢大家一路的陪伴和坚持,希望这些解析能帮你在链表的学习路上走得更扎实。咱们很快会在新的内容里再见,一起去攻克更多难关,解锁更多技能~