当前位置: 首页 > news >正文

单链表专题(一)-应用篇:三道经典题吃透删除、反转与快慢指针

单链表是数据结构课程的第一个核心线性结构,也是算法入门与校招面试的必练内容。不同于数组支持下标随机访问,单链表依靠next指针串联节点,所有操作都围绕「指针指向修改」展开,极其考验对边界场景的把控能力。

本文以三道最经典的单链表应用题为例,从思路推导到代码实现、从易错点复盘到算法思想升华,系统梳理单链表的核心操作与解题套路。

一、单链表基础回顾

单链表的每个节点由「数据域」和「后继指针域」组成,只能从头节点出发单向遍历,无法反向访问,也不能直接通过下标定位节点。 C 语言中节点的标准定义如下:

struct ListNode { int val; struct ListNode *next; }; typedef struct ListNode ListNode;

单链表操作的三大核心特点:

  1. 已知前驱节点时,插入 / 删除节点的时间复杂度为 O (1),只需修改指针指向,无需移动元素。
  2. 访问任意节点必须从头遍历,时间复杂度 O (n),不支持随机访问。
  3. 头节点可能发生变化(如删除头节点、反转链表),是最容易出错的边界点。

在开始刷题前,先记住单链表的通用避坑原则:

  • 访问ptr->valptr->next前,必须确保ptr不为NULL,否则会触发空指针崩溃。
  • 修改节点的next指针前,先保存原后继节点,避免链表断裂、遍历中断。
  • 重新拼接链表后,必须确保最终尾节点的next = NULL,标记链表结束。

二、经典题逐题精讲

2.1 移除链表元素(LeetCode 203)

题目描述

给你一个链表的头节点head和一个整数val,请你删除链表中所有满足Node.val == val的节点,并返回新的头节点。

题意分析

这道题是单链表「删除操作」的入门题,核心难点不在删除逻辑本身,而在边界场景:

  • 头节点本身就是要删除的节点,新头节点会变化
  • 链表尾部是要删除的节点
  • 链表所有节点都需要删除,最终返回空链表
  • 链表本身就是空链表

解法一:尾插法构建新链表

这是最符合直觉的思路:不直接在原链表上做剪断删除,而是遍历原链表,把值不等于val的合法节点,按原有顺序重新拼接成一个新链表。

完整代码

struct ListNode* removeElements(struct ListNode* head, int val) { // 新链表的头、尾指针,初始为空 ListNode *newHead, *newTail; newHead = newTail = NULL; ListNode *pcur = head; // 遍历原链表 while(pcur) { if(pcur->val != val) { // 当前节点需要保留,尾插到新链表 if(newHead == NULL) { // 新链表为空,第一个节点同时是头和尾 newHead = newTail = pcur; } else { // 接到尾部,尾指针后移 newTail->next = pcur; newTail = newTail->next; } } pcur = pcur->next; } // 关键收尾:新链表非空时,尾节点next置空 if(newTail != NULL) { newTail->next = NULL; } return newHead; }

思路细节拆解

  1. 天然解决头节点删除问题:第一个被保留的节点自然成为新头,无需单独判断原头节点是否要删。
  2. 尾节点置空是必写步骤:如果原链表尾部是要删除的节点,新链表最后一个节点的next仍会指向原链表的无效节点,必须手动置空标记链表结束。
  3. 空指针防护:如果所有节点都被删除,newTail仍为NULL,必须先判断非空再访问next

解法二:虚拟头节点原地删除

这是链表删除题的工业级标准写法,核心是在原链表头部加一个「哨兵节点(dummy)」,让所有待删除节点都成为 “某个节点的后继”,删除逻辑完全统一。

完整代码

struct ListNode* removeElements(struct ListNode* head, int val) { // 创建虚拟头节点,next指向原链表头 ListNode dummy; dummy.next = head; ListNode* cur = &dummy; while (cur->next != NULL) { if (cur->next->val == val) { // 下一个节点要删除:直接跳过该节点 cur->next = cur->next->next; } else { // 下一个节点保留:指针后移 cur = cur->next; } } return dummy.next; }

这种写法的优势非常明显:无需单独处理头节点,无需手动置空尾节点,代码更简洁,边界场景全部自动兼容。


2.2 反转链表(LeetCode 206)

题目描述

给你单链表的头节点head,请你反转链表,并返回反转后的链表。

题意分析

反转链表的本质,是把每个节点的next指针从「指向后继」改为「指向前驱」。最大的难点是:直接修改当前节点的next会丢失后继节点,导致链表断裂、遍历无法继续。

解法:三指针迭代原地反转

这是面试的标准最优解,空间复杂度 O (1),全程只通过三个指针配合,逐个翻转节点指向。

完整代码

struct ListNode* reverseList(struct ListNode* head) { if(head == NULL) { return head; } ListNode *n1, *n2, *n3; n1 = NULL; // 前驱节点 n2 = head; // 当前节点 n3 = head->next; // 后继节点(提前保存,防止断链) while(n2) { // 翻转当前节点的指向 n2->next = n1; // 三个指针整体后移 n1 = n2; n2 = n3; if(n3) { n3 = n3->next; } } return n1; }

三个指针的分工

  • n1:记录当前节点的前驱节点,也就是反转后当前节点要指向的目标。初始为NULL,因为原头节点反转后会变成尾节点。
  • n2:当前正在处理的节点,从原头节点开始逐个向后遍历。
  • n3:提前保存当前节点的后继节点,相当于 “备份后路”。修改n2->next后原后继会丢失,必须提前存下来。

执行流程模拟(以 1→2→3→NULL 为例)

  1. 初始:n1=NULLn2=1n3=2
  2. 第一轮:节点 1 的 next 指向 NULL;n1 移到 1,n2 移到 2,n3 移到 3
  3. 第二轮:节点 2 的 next 指向 1;n1 移到 2,n2 移到 3,n3 移到 NULL
  4. 第三轮:节点 3 的 next 指向 2;n1 移到 3,n2 移到 NULL,循环结束
  5. 此时 n1 指向原链表最后一个节点,也就是反转后的新头节点,直接返回即可。

拓展:递归解法

递归写法代码更简洁,核心思想是 “先递归到链表尾部,再从后往前逐个翻转”。

struct ListNode* reverseList(struct ListNode* head) { // 终止条件:空链表或只有一个节点,无需反转 if (head == NULL || head->next == NULL) { return head; } // 先反转子链表,得到子链表的新头 ListNode* newHead = reverseList(head->next); // 把当前节点接到子链表尾部 head->next->next = head; // 当前节点变为尾节点,置空防止成环 head->next = NULL; return newHead; }

2.3 链表的中间结点(LeetCode 876)

题目描述

给你单链表的头结点head,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

题意分析

单链表不能随机访问,朴素做法是「先遍历一遍统计长度,再从头走长度的一半」,但需要两次遍历。 这道题的最优解是快慢指针(龟兔赛跑算法),只需一次遍历就能找到中点,也是单链表最经典的算法思想之一。

核心算法:快慢指针

利用两个指针的速度差实现定位:

  • 慢指针slow:一次走 1 步
  • 快指针fast:一次走 2 步

因为快指针速度是慢指针的 2 倍,当快指针走到链表末尾时,慢指针走过的路程刚好是一半,正好停在中间位置。

完整代码

struct ListNode* middleNode(struct ListNode* head) { ListNode *Fast, *Slow; Fast = Slow = head; while(Fast && Fast->next) { Slow = Slow->next; Fast = Fast->next->next; } return Slow; }

循环条件深度解析

while(Fast && Fast->next)是这道题最核心的细节,有两个关键点:

  1. 顺序不能反:利用 C 语言的短路求值特性,先判断Fast是否为空;如果Fast已经是 NULL,程序不会再访问Fast->next,完美避免空指针崩溃。
  2. 决定了中点落点:这个条件会让快指针最终停在「最后一个节点的下一位(NULL)」,因此偶数长度时,慢指针正好落在第二个中间节点上,完全符合题目要求。

场景验证

  • 奇数长度(5 个节点):快指针停在最后一个节点,慢指针停在第 3 个节点(正中间)
  • 偶数长度(6 个节点):快指针停在 NULL,慢指针停在第 4 个节点(第二个中间节点)

拓展与延伸

如果题目要求返回第一个中间节点,只需修改循环条件:

while(Fast->next && Fast->next->next)

快慢指针的应用远不止找中点,它还是解决「判断链表是否有环」「找环的入口节点」「找链表倒数第 k 个节点」等经典题的核心思想,是单链表必须掌握的算法套路。


三、单链表解题通用方法论

做完这三道题,我们可以提炼出单链表应用题的通用解题框架:

  1. 头节点变化的两种处理方案只要题目可能修改头节点(删除、反转、排序),要么用「新链表 + 新头指针」,要么用「虚拟头节点」,两种思路都能避免单独处理头节点的冗余逻辑。

  2. 指针修改的铁则凡是要修改节点的next指针,先问自己一句:修改之后,原后继节点还找得到吗?如果找不到,就必须先用临时指针保存下来,防止断链。

  3. 边界场景必测清单写完代码后,务必用这几个场景验证:空链表、单个节点、两个节点、目标节点在头部、目标节点在尾部、所有节点都符合删除条件。能覆盖这几个场景,代码基本不会出大问题。

  4. 画图辅助调试链表题的指针跳转很抽象,遇到逻辑混乱时,在纸上画出节点和箭头,手动模拟每一步指针的移动,比盯着代码死想高效得多。

写在最后

这三道题是单链表的「基石题」,覆盖了删除、反转、快慢指针三大核心考点。把这三道题的每一行代码、每一个边界条件都吃透,再去学习合并有序链表、环形链表、相交链表等进阶题目时,就会发现核心逻辑都是相通的。

数据结构的学习没有捷径,多写、多画、多踩坑、多复盘,慢慢就能建立起对指针操作的直觉。

http://www.cnnetsun.cn/news/4023215.html

相关文章:

  • 网站建设所需材料:新手避坑指南与企业必备清单深度解析
  • Axure 全版本汉化指南:9、10、11 中文界面一次到位
  • 成都医院网站建设:打破信息孤岛,重塑医患信任的数字桥梁
  • 从一张照片到一件浮雕:用ImageToSTL完成图片转STL的全过程
  • 怎么建设免费网站从零开始打造个人品牌,低成本启动你的数字梦想
  • 别被忽悠了!揭秘靠谱山西网站建设公司背后的真相与干货
  • 免费搞定桌面分区整理:用 NoFences 把杂乱图标装进“围栏“,30 秒上手
  • 沧州网站建设优化背后的真相与企业如何通过低成本策略实现流量破局
  • 一键提升Illustrator工作效率20倍的免费脚本合集:5分钟从安装到上手
  • Wand-Enhancer开源增强工具全解析:本地补丁、手机远程控制与自定义脚本一次讲透
  • 揭秘外贸购物网站建设背后的真相:如何打造高转化率独立站并突破流量瓶颈
  • 邵阳网站建设公司:从草根逆袭到数字赋能,聊聊咱们本地建站那些真实现状
  • Windows APK安装器上手实操:从下载到跑通第一个安卓应用
  • 旅游网站建设目标:从平庸到卓越的全链路进阶策略
  • 厦门电商网站建设怎么避坑?2024年最新实战指南与企业选对服务商的核心逻辑
  • 潍坊企业网站建设如何避坑:从基础搭建到流量转化的底层逻辑深度解析
  • 凤阳网站建设哪家好?深耕本地市场十年,老张掏心窝子告诉你真相
  • 北京网站建设有哪些:揭秘正规企业建站的核心需求与避坑指南
  • 用Qutip绘制布洛赫球
  • 深耕本地市场,揭秘寿光网站建设公司如何选择靠谱合作伙伴
  • 2024高校网站建设方案全解析:打造符合现代教育理念的数字化校园门户
  • 揭秘信阳网站建设公司如何助力实体企业实现数字化腾飞与品牌升级
  • 为什么你的宁波企业网站建设没带来客户?老站长掏心窝子告诉你真相,别再用模板骗自己了
  • 小企业网站建设多少钱,全面解析影响企业官网造价的核心因素与避坑指南
  • 使用开源工具构建面向IoT-边缘-云连续体的低成本网络数字孪生
  • 三步完成QQ空间说说备份:GetQzonehistory 永久保存你的历史说说
  • 佛山市网站建设公司深度解析:中小企业如何在数字化浪潮中突围
  • 南昌手机网站建设:为什么你的企业需要移动端优先的数字化名片
  • 拒绝盲目开工:手把手教你制定一套落地高效的网站建设项目进度表
  • 萧山网站建设公司怎么选?避开这5个坑,让预算花得值