链表面试高频题实战:倒数第 k 个节点查找 + 指定值删除

前言

链表作为数据结构中的基础线性结构,其指针操作和边界处理是面试高频考点。本文针对两道经典链表题 ------查找倒数第 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;

}

三、关键注意点
  1. 边界处理:必须判断L==NULL(头指针为空)、k<=0(非法输入)、k>链表长度(查找失败);
  1. 指针移动:原代码fast->next;无赋值操作,会导致死循环,需修正为fast = fast->link;;
  1. 结构一致性:题目节点指针域为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;

}

三、关键注意点
  1. 前驱指针初始化:从头结点开始,而非第一个有效节点,避免首节点为 x 时无法删除;
  1. 内存释放:删除节点后必须调用free(curr),否则会造成内存泄漏(面试高频扣分点);
  1. 指针移动逻辑:删除节点时,仅需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),需根据场景调整)。

结语

链表题的本质是指针操作的逻辑梳理边界条件的全面覆盖。以上两道题覆盖了 "查找" 和 "删除" 两大核心操作,掌握双指针法和前驱追踪法后,可迁移解决反转链表、合并链表等更多高频题。建议大家多动手调试代码,重点关注指针移动的每一步,才能真正吃透链表解题逻辑~

相关推荐
聆风吟20172 小时前
【数据结构入门手札】数据结构基础:从数据到抽象数据类型
数据结构
AI科技星2 小时前
自然本源——空间元、氢尺、探针与场方程
数据结构·人工智能·算法·机器学习·计算机视觉
吃着火锅x唱着歌2 小时前
LeetCode 2874.有序三元组中的最大值II
数据结构·算法·leetcode
小熳芋3 小时前
排序链表- python-非进阶做法
数据结构·算法·链表
zore_c3 小时前
【C语言】数据在内存中的存储(超详解)
c语言·开发语言·数据结构·经验分享·笔记
程序员-周李斌3 小时前
ArrayList 源码深度分析(基于 JDK 8)
java·开发语言·数据结构·算法·list
爪哇部落算法小助手4 小时前
爪哇周赛 Round 3
数据结构·c++·算法
迷途之人不知返4 小时前
二叉树的链式结构
数据结构
不会c嘎嘎4 小时前
【数据结构】红黑树详解:从原理到C++实现
开发语言·数据结构