前言
链表作为数据结构中的基础线性结构,其指针操作和边界处理是面试高频考点。本文针对两道经典链表题 ------查找倒数第 k 个节点 和删除所有值为 x 的节点,从算法思想、代码实现到方法总结进行全面解析,帮助大家吃透链表解题的核心逻辑。
题目一:查找链表倒数第 k 个节点
题目描述
已知带头结点的单链表,仅给出头指针list,要求在不改变链表结构的前提下,高效查找倒数第 k 个位置的节点(k 为正整数)。查找成功则输出节点 data 值并返回 1,否则返回 0。
一、算法核心思想
本题最优解采用 双指针法(快慢指针),核心是利用 "指针位置差" 实现一次遍历完成查找,时间复杂度 O (n),空间复杂度 O (1)(无额外空间占用)。
- 快指针先向前移动 k 步,与慢指针形成 k 个节点的距离;
- 之后快慢指针同步向后移动,当快指针到达链表尾部(NULL)时,慢指针恰好指向倒数第 k 个节点;
- 优势:避免传统 "两次遍历(先算长度再找位置)" 的冗余操作,兼顾效率和空间。
二、修正后的完整代码(C 语言)
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构(带头结点)
typedef struct Node {
int data; // 数据域
struct Node* link; // 指针域(next/link)
} Node;
// 查找倒数第k个节点:成功返回1并输出data,失败返回0
int FindNodeFS(Node* L, int k) {
// 边界条件1:头指针为空或k非正整数(题目要求k为正整数)
if (L == NULL || k <= 0) {
printf("查找失败:头指针为空或k值非法\n");
return 0;
}
// 边界条件2:链表无有效节点(仅头结点)
if (L->link == NULL) {
printf("查找失败:链表为空\n");
return 0;
}
Node* fastPtr = L->link; // 快指针:初始指向第一个有效节点
Node* slowPtr = L->link; // 慢指针:初始指向第一个有效节点
// 步骤1:快指针先移动k步,构建位置差
for (int i = 0; i < k; i++) {
if (fastPtr != NULL) {
fastPtr = fastPtr->link; // 快指针后移
} else {
// 快指针提前到达尾部,说明k>链表长度
printf("查找失败:k值超过链表长度\n");
return 0;
}
}
// 步骤2:快慢指针同步移动,直到快指针到尾
while (fastPtr != NULL) {
fastPtr = fastPtr->link; // 原代码bug:未赋值给指针,导致死循环
slowPtr = slowPtr->link;
}
// 步骤3:输出结果(慢指针指向倒数第k个节点)
printf("查找成功:倒数第%d个节点的data值为%d\n", k, slowPtr->data);
return 1;
}
// 辅助函数:创建链表(用于测试)
Node* createLinkedList(int arr[], int len) {
Node* head = (Node*)malloc(sizeof(Node));
head->link = NULL;
Node* curr = head;
for (int i = 0; i < len; i++) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = arr[i];
newNode->link = NULL;
curr->link = newNode;
curr = newNode;
}
return head;
}
// 主函数测试
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int len = sizeof(arr) / sizeof(arr[0]);
Node* list = createLinkedList(arr, len);
FindNodeFS(list, 3); // 预期输出:倒数第3个节点data=7
FindNodeFS(list, 8); // 预期输出:k值超过链表长度
FindNodeFS(list, -2); // 预期输出:k值非法
return 0;
}
三、关键注意点
- 边界处理:必须判断L==NULL(头指针为空)、k<=0(非法输入)、k>链表长度(查找失败);
- 指针移动:原代码fast->next;无赋值操作,会导致死循环,需修正为fast = fast->link;;
- 结构一致性:题目节点指针域为link,避免与next混淆(易踩语法 bug)。
题目二:删除链表中所有值为 x 的节点
题目描述
带头结点的单链表 L 中,删除所有值为 x 的节点(可能不唯一),释放节点空间,要求不改变链表其他结构。
一、算法核心思想
采用 前驱指针追踪法,通过 "遍历 + 跳过目标节点" 实现删除,时间复杂度 O (n),空间复杂度 O (1)。
- 用curr指针遍历链表,prev指针始终指向curr的前驱节点;
- 当curr节点值为 x 时,通过prev->link = curr->link跳过目标节点,释放curr空间;
- 当curr节点值不为 x 时,prev和curr同步后移;
- 优势:一次遍历完成所有删除操作,避免重复遍历,且严格释放内存(避免内存泄漏)。
二、完整代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
// 复用节点结构定义
typedef struct Node {
int data;
struct Node* link;
} Node;
// 删除所有值为x的节点:成功返回处理后的头指针,失败返回NULL
Node* deleteAllX(Node* L, int x) {
// 边界条件:头指针为空
if (L == NULL) {
printf("删除失败:头指针为空\n");
return NULL;
}
Node* prev = L; // 前驱指针:初始指向头结点(关键!避免处理首节点删除问题)
Node* curr = L->link; // 当前指针:初始指向第一个有效节点
while (curr != NULL) {
if (curr->data == x) {
// 步骤1:跳过目标节点,保存待删除节点
prev->link = curr->link;
free(curr); // 释放内存,避免内存泄漏
curr = prev->link; // 当前指针后移(无需前驱指针移动)
} else {
// 步骤2:节点值不匹配,双指针同步后移
prev = curr;
curr = curr->link;
}
}
printf("删除完成:所有值为%d的节点已移除\n", x);
return L;
}
// 辅助函数:打印链表(用于测试)
void printLinkedList(Node* L) {
if (L->link == NULL) {
printf("链表为空\n");
return;
}
Node* curr = L->link;
printf("链表内容:");
while (curr != NULL) {
printf("%d ", curr->data);
curr = curr->link;
}
printf("\n");
}
// 主函数测试
int main() {
int arr[] = {2, 4, 2, 6, 2, 8, 10};
int len = sizeof(arr) / sizeof(arr[0]);
Node* list = createLinkedList(arr, len); // 复用创建链表函数
printf("删除前:");
printLinkedList(list);
deleteAllX(list, 2); // 删除所有值为2的节点
printf("删除后:");
printLinkedList(list); // 预期输出:4 6 8 10
return 0;
}
三、关键注意点
- 前驱指针初始化:从头结点开始,而非第一个有效节点,避免首节点为 x 时无法删除;
- 内存释放:删除节点后必须调用free(curr),否则会造成内存泄漏(面试高频扣分点);
- 指针移动逻辑:删除节点时,仅需curr后移;不删除时,prev和curr同步后移。
两道题的核心方法总结
1. 链表解题的通用原则
- 边界优先处理:所有链表题首先判断头指针是否为空、链表是否为空、输入参数是否合法(如 k<=0),避免空指针异常;
- 空间效率优先:优先使用 O (1) 空间的算法(如双指针、前驱追踪),避免额外数组 / 哈希表(O (n) 空间);
- 内存管理:C/C++ 中删除节点必须释放内存,Java 无需手动释放,但需注意引用失效问题。
2. 高频算法模板提炼
|---------|-----------------|------------------------|-------|
| 算法类型 | 适用场景 | 核心思路 | 时间复杂度 |
| 双指针法 | 倒数第 k 个节点、链表环检测 | 快慢指针构建位置差 / 速度差,一次遍历解决 | O(n) |
| 前驱指针追踪法 | 删除指定值节点、插入节点 | 前驱指针记录前一个节点,避免指针丢失 | O(n) |
3. 避坑指南
- 指针移动时务必赋值(如fast = fast->link,而非fast->link;);
- 带头结点的链表,操作时优先利用头结点简化首节点处理;
- 循环条件避免死循环(如while(fast != NULL)而非while(fast->link != NULL),需根据场景调整)。
结语
链表题的本质是指针操作的逻辑梳理 和边界条件的全面覆盖。以上两道题覆盖了 "查找" 和 "删除" 两大核心操作,掌握双指针法和前驱追踪法后,可迁移解决反转链表、合并链表等更多高频题。建议大家多动手调试代码,重点关注指针移动的每一步,才能真正吃透链表解题逻辑~