开篇介绍:
hello 大家,我又来了,本篇博客依旧是为大家带来三道题的解析,那么我们废话少说,直接进入正题,还是老样子,我们先看题目链接:
面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)
https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/description/链表的回文结构_牛客题霸_牛客网
https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=182&&tqId=34762&sourceUrl=https%3A%2F%2Fwww.nowcoder.com%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AC%2594%25E9%259D%25A2%25E8%25AF%2595%25E7%25AF%2587%26topicId%3D295面试题 02.07. 链表相交 - 力扣(LeetCode)
https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/description/接下来,我们直接进入题目解析:
面试题 02.02. 返回倒数第 k 个节点:
这道题,其实难度不大,我们可以直接秒,我们先看题目:

题意分析:
这道题要求我们找出单向链表中倒数第 k 个节点,并返回该节点的值。
关键信息提取
- 数据结构:单向链表,每个节点包含一个值
val和指向下一个节点的指针next。 - 目标:找到链表中倒数第
k个节点的值。 - 条件:给定的
k是有效的,无需处理k无效(如k大于链表长度等)的情况。 - 示例辅助理解:输入链表
1->2->3->4->5,k = 2,输出为4,因为倒数第 2 个节点是值为4的节点。
解析:
对于这道题,咱也不讲什么乱七八糟的解法,我们直接用传奇解法:快慢指针,解决本题,大家有不了解快慢指针的,可以看一下这篇博客:对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析-CSDN博客
只不过在本题中,我们的快指针不是每次走两步。
因为题目要找链表的倒数第k个节点,那么也就是说,我们先看图:
要返回倒一个节点的数据,那么这又和快慢指针有什么关系呢,根据前面所学的知识,我们知道,fast指针在为NULL的时候,终止循环,也就是说fast指针运动到了图中的NULL所示位置,那么我们不妨看看此时fast指针距离倒数第一个节点的距离为多少,巧了,正好是1,即k,那么按照惯例来说,我们的slow指针也就要在那是运动到了倒数第一个节点,那么究竟要怎么实现呢?
其实和上面说的类似,我们要让slow指针在fast指针运行到NULL的时候就运行到倒数第k个节点,那么,换句话来说,此时slow指针和fast指针的距离不正好是k吗诸位,再换句话来说就是,我们要让slow指针和fast指针保持k个节点的距离,如此,才能正好达到我们上述所说。
那么,我们又知道,在初始化的时候,我们是把fast和slow指针同时设置在头结点,那么为了让slow指针和fast指针保持k个节点的距离,我们就得在slow指针不移动的情况下,让fast指针先行k个节点,这一步很简单,我们直接借助while循环:
// 双指针法寻找倒数第k个节点:快指针先移动k步
sl* fast = head; // 快指针,用于先行探路
sl* slow = head; // 慢指针,最终指向倒数第k个节点
// sl* cur = head; // 此变量在当前逻辑中未使用,可注释或删除
// 快指针先行移动k步
while (k > 0) {fast = fast->next; // 快指针向前移动k--; // 计数减1,直到移动k步
}
但是在这里我想给大家强调一下,大家可能会觉得说相隔k个节点就代表说两个节点之间要间隔k个节点,其实这是错的,准确来说,相隔k个节点是代表说两个节点之间要间隔k-1个节点,为什么呢?那是因为我们实际上的相隔k个节点指的是前面的那个节点要走几步才能到后面的那一个节点,就那k=2来说,它的意思是说,前面的那个节点要走2步才能到后面的那一个节点,而是不是当这两个节点之间间隔一个节点的时候,能够达到这个效果(即begin=begin->nex->next),要是中间间隔这着两个节点,那就要走4步,那么就不行,这里希望大家注意一下,这个理解很重要。
接下来,我们就可以让slow指针和fast指针同时一起移动相同的步数了,如此一来,当fast指针运动到NULL的时候,slow指针正好就停在倒数第k个节点,完整代码如下:
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
typedef struct ListNode sl;
int kthToLast(struct ListNode* head, int k) {sl* fast=head;sl* slow=head;sl* cur=head;while(k){fast=fast->next;k--;}//出来循环之后,fast正好在距离头结点k个节点的距离的节点的前一个节点while(fast!=NULL){fast=fast->next;slow=slow->next;}return slow->val;//返回的是节点的数据
}
这个方法的原理就是,假设链表一共有n个节点,然后要返回倒数第k个节点,那么就是说,我们要让slow指针走到倒数第k个节点,换句话说就是,我们要让slow指针走n-k步,那么当我们让fast先走k步,那么此时fast距离链表的尾节点(这里是指NULL指针),
即如上图,距离是3步,也就是n-k步,那么当fast指针走到NULL时,那我们的slow指针也是走了n-k步,而n-k,不正好就对应着倒数第k个节点吗,原理就是这样。这个还是考验大家的数学功底和思维。
再给大家提供一个例子,帮助大家进一步理解:
假设我们有链表:1 -> 2 -> 3 -> 4 -> 5,要查找倒数第 2 个节点(即值为 4 的节点)
执行步骤:
初始化指针:
fast = head,slow = head(都指向节点1)第一个循环(快指针先行 k 步):
- k=2,进入循环:
fast移动到节点2,k=1 - k=1,进入循环:
fast移动到节点3,k=0 - 循环结束,
fast指向节点3
- k=2,进入循环:
第二个循环(快慢指针同时移动):
fast不为空,fast移动到节点4,slow移动到节点2fast不为空,fast移动到节点5,slow移动到节点3fast不为空,fast移动到NULL,slow移动到节点4- 循环结束,
slow指向节点4
返回
slow->val即4,正确找到倒数第 2 个节点
另一个例子:链表 3 -> 7 -> 9 -> 5,查找倒数第 3 个节点
- 第一个循环后,
fast指向9 - 第二个循环后,
slow指向7(倒数第 3 个节点) - 函数返回
7
如此,我们本题就大功告成。
REAL483 链表的回文结构:
这道题是牛客网里的,牛客网也算是我们的老相识了,我们先看题目:
题意分析:
这道题要求我们设计一个算法,判断给定的单向链表是否为回文结构。
关键信息提取
- 数据结构:单向链表。
- 算法要求:时间复杂度为 O(n),额外空间复杂度为 O(1)。
- 任务:判断链表是否为回文结构,即链表正序和逆序遍历得到的序列相同。
- 输入输出:给定链表的头指针
A,返回一个布尔值(true表示是回文结构,false表示不是)。 - 测试样例辅助理解:输入链表
1->2->2->1,输出true,因为正序和逆序遍历得到的序列都是1,2,2,1,是回文结构。
解析:
做这道题之前,我们不妨先来了解了解,什么算是回文链表
回文链表的核心定义是:从前往后遍历链表得到的节点值序列,和从后往前遍历得到的节点值序列完全一致。下面通过更详细的例子来理解:
一、是回文链表的例子
例子 1:简单偶数长度回文
链表结构:1 -> 2 -> 2 -> 1
- 正序遍历(从表头到表尾):依次访问节点值
1、2、2、1,序列为[1, 2, 2, 1]。 - 逆序遍历(从表尾到表头):依次访问节点值
1、2、2、1,序列为[1, 2, 2, 1]。 - 结论:正序和逆序序列完全相同,是回文链表。
例子 2:简单奇数长度回文
链表结构:3 -> 5 -> 8 -> 5 -> 3
- 正序遍历:节点值序列
[3, 5, 8, 5, 3]。 - 逆序遍历:节点值序列
[3, 5, 8, 5, 3]。 - 结论:正序和逆序序列完全相同,是回文链表(中间节点
8是 “对称轴”,两侧对称)。
例子 3:单节点链表
链表结构:只有一个节点,值为 4。
- 正序遍历:序列
[4]。 - 逆序遍历:序列
[4]。 - 结论:是回文链表(单个元素自身对称)。
例子 4:重复元素的回文
链表结构:2 -> 2 -> 2 -> 2
- 正序遍历:序列
[2, 2, 2, 2]。 - 逆序遍历:序列
[2, 2, 2, 2]。 - 结论:是回文链表(所有元素相同,天然对称)。
二、不是回文链表的例子
例子 1:1 -> 2 -> 3 -> 1 -> 2
- 正序遍历:节点值依次为
1、2、3、1、2,序列[1, 2, 3, 1, 2]。 - 逆序遍历:节点值依次为
2、1、3、2、1,序列[2, 1, 3, 2, 1]。 - 结论:正序和逆序序列不同(比如第一个元素
1vs2,最后一个元素2vs1),不是回文链表。
例子 2:非对称偶数长度
链表结构:1 -> 3 -> 2 -> 4
- 正序遍历:序列
[1, 3, 2, 4]。 - 逆序遍历:序列
[4, 2, 3, 1]。 - 结论:序列完全不同,不是回文链表。
例子 3:非对称奇数长度
链表结构:5 -> 6 -> 7 -> 8 -> 9
- 正序遍历:序列
[5, 6, 7, 8, 9]。 - 逆序遍历:序列
[9, 8, 7, 6, 5]。 - 结论:序列完全相反,不是回文链表。
总结来说,回文链表的本质是 “对称”—— 就像回文串(如 “level”“雷达”)一样,从前往后和从后往前读完全一致,即中间劈开,两边能不能对称罢了
知道了什么算是回文链表之后,我们才能进行用代码去判断一个链表是不是回文链表,方法有很多,我在这里就提供一种方法供大家参考。
我们拿回文链表 1 2 3 2 1,作为例子来进行分析,首先,我们经过上述,知道了回文链表的核心其实就是中间的一个节点,无论是奇数链表还是偶数链表,那么我们先抓着中间节点不放一下。
我们可以观察中间节点后的节点和中间节点前的节点的数据,究竟有什么关系,即1 2和2 1,我们能找到什么关系呢?
诶,聪明的你很快就能发现,当我们将中间节点后的2 1两个节点进行翻转一下变为1 2,那它是不是就和中间节点前面的节点变得一模一样了。
是的,上述就是我们解决这个问题的关键所在。
我们要找到中间节点,然后要去将中间节点后的节点进行反转,然后再与中间节点前面的节点进行一一的比较,一但有出现不一样的,就代表该链表不是回文链表,例如 1 2 3 1 2,翻转过后变为1 2 2 1 3,此时的第二个2是链表的中间节点,然后进行对比,第一个1和中间节点的2一对比,不相等,pass。
而1 2 3 2 1,翻转过后:1 2 1 2 3,一一对比,OK没问题,完全一样,过关。
但是我们例子是这样,却还是有一些点需要知道,
- 第一,我们翻转是要把从中间节点到最后一个节点进行翻转,前面的节点是不动的,如1 2 3 2 1 翻转后变为1 2 1 2 3
- 第二:我们要知道,由于我们是不用创建新链表的方法去翻转链表,所以即使翻转了之后,例如 1 2 1 2 3,此时的第一个2它的next指针还是指向3的,而3的next指针指向NULL,第二个1的next指针指向第二个2,第二个2的next指针指向3,即从原本的1->2->3->2->1变为

你要是问我为什么不把中间节点后的节点进行反转,不去动中间节点,那我的回答是:其实也可以这样子的,只是我做的时候去将中间节点一起反转了。
但是,说是这么说,其实我还是不推荐啦,因为链表长度为奇数的时候,或许还可行,但是要是为偶数的时候,比如 1 2 2 1,那么此时找到的中间节点,我们找到的是第二个2,那这个时候要是我们把中间节点后的节点进行翻转的话,那是不是就变成了1 2 2 1,没变化~,就是如此悲伤,而要是中间节点也跟着反转的话,就是 1 2 1 2,正好满足我们前面所述,所以,我还是推荐从中间节点开始往后进行反转,而后再按照顺序将第一个节点和新链表的中间节点一一比较,逐渐递增。
那么我们要怎么找到中间节点和进行翻转呢?诶,我之前写喽:对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析-CSDN博客
在这里我们要用快慢指针和三指针迭代法哦。
到了这里,我们离结束本题就不远了,只差比较的处理。
我们知道,我们肯定要借助循环进行比较,那么循环终止的条件要是什么呢?
我们是将新链表的第一个节点和第中间个节点进行比较,所以我们的终止条件,也肯定和这两个有关。

大家看图,其实很简单,我们依旧看例子:翻转后变为1 2 1 2 3,进行比较:1和1相同,head和mid各自进入下一个进行比较,2和2相同,OK,两个再进入下一个,此时的head在第一个2,它的下一个是3.而mid在第二个2,它的下一个也是3,OK,再比较,又相等,再往下走,变为空了,比较无意义,结束比较,所以,我们的循环条件就是当head和mid指针都不为0的时候,进行循环。
偶数时的情况:
步骤 3:双指针比较(前半段 vs 反转后的后半段)
- 前半段头指针:
head(指向1)。 - 反转后半段头指针:mid(指向
1)。
循环条件:head != NULL && mid != NULL(两个指针都未走到末尾时比较)。
| 比较轮次 | head 指向的值 | prev 指向的值 | 是否相等 | 后续操作 |
|---|---|---|---|---|
| 第 1 轮 | 1 | 1 | 是 | head = head->next(指向 2);mid = mid->next(指向 2) |
| 第 2 轮 | 2 | 2 | 是 | head = head->next(指向 2);mid = p】mid->next(指向 NULL) |
| 循环结束 | head 指向 2 | prev 指向 NULL | - | 所有对应位置都相等,判定为回文 |
由此,我们便可以将此题斩于马下。下面看完整代码:
/** 回文链表判断的C语言实现* 回文链表定义:正序遍历与逆序遍历得到的节点值序列完全相同的链表* 例如:1->2->2->1 是回文链表;1->2->3 不是回文链表*/
// 链表节点结构体定义
// 每个节点包含一个整数值和指向下一个节点的指针
struct ListNode {int val; // 节点存储的数值struct ListNode *next; // 指向下一个节点的指针
};
// 为结构体创建别名sl,简化代码书写
typedef struct ListNode sl;
/*** 创建新的链表节点* @param val 节点要存储的数值* @return 成功返回新节点的指针,失败返回NULL*/
sl* createNode(int val) {// 分配内存空间,大小为一个ListNode结构体sl* node = (sl*)malloc(sizeof(sl));// 检查内存分配是否成功if (node == NULL) {return NULL; // 内存分配失败,返回空指针}node->val = val; // 设置节点的数值node->next = NULL; // 新节点默认不指向任何节点return node; // 返回创建的节点
}
/*** 寻找链表的中间节点(快慢指针法)* 算法原理:快指针每次移动2步,慢指针每次移动1步* 当快指针到达链表末尾时,慢指针恰好指向中间节点* @param phead 链表的头节点指针* @return 中间节点的指针*/
sl* midsl(sl* phead) {// 初始化快慢指针,都指向头节点sl* fast = phead; // 快指针:每次走2步sl* slow = phead; // 慢指针:每次走1步// 循环条件:快指针不为空且快指针的下一个节点也不为空// 保证fast->next->next不会访问空指针while (fast && fast->next) {fast = fast->next->next; // 快指针移动2步slow = slow->next; // 慢指针移动1步}// 当循环结束时,slow指向中间节点// 对于偶数长度链表:指向后半段的第一个节点// 对于奇数长度链表:指向正中间的节点return slow;
}
/*** 反转链表(三指针迭代法)* 算法原理:通过三个指针逐步反转节点的指向关系* @param mid 要反转的链表的头节点(原链表的中间节点)* @return 反转后新链表的头节点*/
sl* reversesl(sl* mid) {sl* n1 = NULL; // 指向当前节点的前一个节点(初始为NULL)sl* n2 = mid; // 指向当前正在处理的节点(初始为中间节点)// 循环直到当前节点为空(遍历完所有节点)while (n2) {sl* n3 = n2->next; // 保存当前节点的下一个节点(防止断链)n2->next = n1; // 反转当前节点的指向:指向它的前一个节点n1 = n2; // 前指针向后移动:指向当前节点n2 = n3; // 当前指针向后移动:指向下一个节点}// 循环结束后,n1指向原链表的最后一个节点,即反转后新链表的头节点return n1;
}
/*** 判断链表是否为回文结构* 算法步骤:* 1. 找到链表的中间节点* 2. 反转中间节点之后的后半段链表* 3. 比较前半段链表与反转后的后半段链表* @param A 链表的头节点指针* @return 是回文链表返回true,否则返回false*/
bool chkPalindrome(sl* A) {// 边界条件处理:空链表或只有一个节点的链表都是回文链表if (A == NULL || A->next == NULL) {return true;}// 步骤1:找到链表的中间节点sl* mid = midsl(A);// 步骤2:反转中间节点之后的后半段链表,得到新的头节点sl* newsl = reversesl(mid);// 步骤3:比较前半段与反转后的后半段// 循环条件:两个指针都未到达各自的末尾while (A && newsl) {// 若对应位置的节点值不相等,说明不是回文链表if (A->val != newsl->val) {return false;}// 两个指针分别向后移动一步,继续比较下一个节点A = A->next;newsl = newsl->next;}// 所有对应节点值都相等,是回文链表return true;
}
搞定。
面试题 02.07. 链表相交:
这道题,wos,绝对是单链表的经典中的经典的题目。我们先看题目:



题意分析:
这道题要求找出两个单链表相交的起始节点,若不相交则返回 null,且要保证函数返回后链表结构不变,还需设计时间复杂度 O(n)、空间复杂度 O(1) 的解决方案。
关键信息提取
- 数据结构:两个单链表,无环。
- 任务:找到两链表相交的起始节点,无交点则返回
null。 - 约束条件:返回结果后链表需保持原始结构;时间复杂度 O(n),空间复杂度 O(1)。
- 示例辅助理解:如示例 1 中两链表在值为
8的节点相交;示例 3 中两链表无交点,返回null。
解析:
对于这道题,我们同样要进行画图:

就拿题目给的例子吧,根据题目要求,我们首先要判断,这两个链表是不是相交的,说到相交,很多人觉得可能是这样子的相交:
但是是这样子的嘛,其实不是的,如果是这么相交的话,那链表不就变成了这样子:
所以,相交只能是这样子的:
这一点希望大家注意。
那么我们要如何判断两个链表是不是相交的呢?
其实很简单,我们可以看到
两个链表从相交开始到最后一个节点,都是相同的,那么这就给了我们可乘之机。
我们可以分别遍历两个链表,遍历到两个链表的尾节点,然后看一下两个尾节点的地址是否相同,相同就代表两个链表的最后一个节点是同一个节点,这里注意,一定要用地址进行比较,要是用节点中存储的数据的话,可能会有巧合,至于是什么巧合呢?大家不妨自己想想,我在这里不进行赘述。
那么在判断两个链表是否为相交链表之后,题目还要求我们返回出两个链表相交的第一个节点,那么这一步,我们又要怎么解决呢?
按照上图来看,其实就是要我们把c1节点找到,那么我们要怎么找到呢?
这就需要两个指针了,我们让两个指针分别处于两个链表,然后去对比,不同就一起进入下一个节点,一旦两个指针对应的节点相同了,就代表这个节点是两个链表相交的第一个节点。
但是这就有一个要求,那就是两个节点各自所处的链表的位置要是一样的,比如这样子:
这样子才能随着各自的一起移动(即cura走一步,curb也得跟着走一步)去找到相交的第一个节点。
如果是不在距离相交的第一个节点相同的节点上,比如这个:

那么当cura运动到c1的时候,curb才移动到b3,那么这两个就会一直错过~~~。
所以呢,我对上面总结一下就是,我们要让cura和curb处于相同的起跑线,因为一般来说,两个链表肯定一长一短,我们要是让cura和curb都从头结点出发,那肯定会嘎嘣脆,不行。
所以针对较短的,我们就让它从头结点出发,对于较长的,我们就不能让它从头结点出发,而是要移动它,让它这个链表的指针和较短链表的头结点处于同一起跑线上,即如下图:
那么我们要实现这一步呢?
大家看上图,我们想要让较长和较短的链表的各自指针处于同一起跑线上,我们就得让较长的那个链表的指针移动(较长较短链表节点的数量差)步,这就是关键。
那么你要是问我我是怎么得出的,很简单,画图,我们先得到了
这一副图,然后再去死命找关系即可。
所以,我们在一开始的遍历时,就要去各自统计两个链表的节点数量,然后再判断a和b哪个链表比较长,用长的那个减去短的那个,然后再让长的链表的指针从长链表的头结点移动(差)步,然后再让较短链表的指针从其头结点出发,这么一来,两个指针就在同一起跑线上了。
随后再进行比较并移动指针,当两个指针对应的节点的地址相同,就代表找到了两个链表相交的第一个节点,这个时候我们把两个指针中的随意一个返回即可。
具体代码如下:
typedef struct ListNode sl;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {sl* cura=headA;sl* curb=headB;int counta=1;int countb=1;while(cura){cura=cura->next;counta++;}while(curb){curb=curb->next;countb++;}//找到尾节点,比较尾节点的地址是否相同,相同就代表有相交//要记得用地址比较,不能用值,不难可能会发生巧合if(curb!=cura)//直接用指针比较{return NULL;}else{if(counta>countb){int cha=counta-countb;while(cha){headA=headA->next;cha--;}while(headA!=headB){headA=headA->next;headB=headB->next;}return headA;}else{int cha=countb-counta;while(cha){headB=headB->next;cha--;}while(headA!=headB){headA=headA->next;headB=headB->next;}return headA;}}
}
但是对于上面这么长一串代码,大家有没有感觉麻烦,那么,为了简洁性,我在这里给大家提供一个假设法:
其实这个方法思路很简单,就是我们假设是A链表长,并创建新节点longsl去接收A链表的头结点,假设B链表短,再创建新节点shortsl去接收B链表的头结点。
然后再借用if语句判断一下,如果真的是B链表的长度大于A链表,我们就将longsl和shortsl调换,简简单单,而后我们再借助abs函数去接收两个链表长度之差,接着让我们所创建的longsl指针(此时它就是长链表(A长他就是A的头结点,B长它就是B的头结点))移动(差)步,如此一来,longsl和shortsl便处于同一起跑线(即距离相交的第一个节点相同),由此,我们就可以大幅减少代码长度,具体如下:
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
typedef struct ListNode sl;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {sl* cura=headA;sl* curb=headB;int counta=1;int countb=1;while(cura){cura=cura->next;counta++;}while(curb){curb=curb->next;countb++;}//找到尾节点,比较尾节点的地址是否相同,相同就代表有相交//要记得用地址比较,不能用值,不难可能会发生巧合// if(curb!=cura)//直接用指针比较// {// return NULL;// }// else// {// if(counta>countb)// {// int cha=counta-countb;// while(cha)// {// headA=headA->next;// cha--;// }// while(headA!=headB)// {// headA=headA->next;// headB=headB->next;// }// return headA;// }// else// {// int cha=countb-counta;// while(cha)// {// headB=headB->next;// cha--;// }// while(headA!=headB)// {// headA=headA->next;// headB=headB->next;// }// return headA;// }// }//假设法int gap=abs(counta-countb);sl* longsl=headA;sl* shortsl=headB;if(countb>counta){longsl=headB;shortsl=headA;}while(gap--){longsl=longsl->next;}while(longsl!=shortsl){longsl=longsl->next;shortsl=shortsl->next;}return longsl;
}
依旧是简简单单,那么到这里,我们本题也就结束喽。
结语:
当我们逐行拆解完这三道单链表经典题目,会发现看似独立的问题背后,其实藏着一套贯穿始终的 “解题逻辑”—— 从对链表遍历节奏的把控(快慢指针),到对链表结构的改造(局部反转),再到对两个链表关系的精准分析(长度对齐与交点定位),每一步都是对单链表 “指针指向” 这一核心特性的深度运用。这些题目或许不是算法领域中最难的,但却是理解 “如何用最少的资源解决问题” 的绝佳案例,尤其是在时间复杂度要求 O (n)、空间复杂度要求 O (1) 的约束下,更能倒逼我们跳出 “用额外空间存储数据” 的惯性思维,转而聚焦于链表本身的结构特性与指针操作的灵活性。
先看 “返回倒数第 k 个节点”,这道题的关键在于 “如何让一个指针精准停在倒数第 k 个位置”。我们没有选择先遍历链表统计长度、再回溯 k 步(虽然可行,但不够优雅),而是用快慢指针的 “距离差” 实现了 “一次遍历即定位”—— 让快指针先跑 k 步,再让两个指针同步前进,当快指针触达链表末尾时,慢指针恰好落在目标节点上。这种思路的巧妙之处在于,它把 “倒数第 k 个” 的抽象需求,转化为 “两个指针保持 k 步距离” 的具象操作,既避免了额外的长度统计步骤,也让代码逻辑更简洁。相信大家在后续遇到 “定位链表特定位置” 的问题时,比如找中间节点、找倒数第 n 个节点,都能想到用类似的 “指针距离控制” 思路解决。
再到 “判断链表的回文结构”,这道题则是对 “链表操作组合” 的考验。回文的核心是 “前后对称”,但单链表的单向性决定了我们无法直接从后往前遍历,于是我们拆解出 “找中间节点→反转后半段→前后对比” 三个步骤:先用快慢指针找到中间节点,把链表分成 “前半段” 和 “后半段”;再用三指针法反转后半段,让原本单向的后半段变成 “反向可遍历” 的结构;最后用两个指针分别从原链表头部和反转后的后半段头部同步前进,逐一对比节点值。整个过程没有用任何额外的数组或栈存储数据,完全依赖原地指针操作,既满足了空间复杂度 O (1) 的要求,也让我们更熟悉链表反转的细节 —— 比如反转时如何用 n1、n2、n3 三个指针避免断链,反转后哪个指针才是新的头节点。更重要的是,这道题教会我们:当遇到 “无法直接双向遍历” 的限制时,“改造链表局部结构” 是一种高效的破局思路。
最后是 “链表相交”,这道题的难点在于 “如何找到两个链表的第一个公共节点”。首先我们要明确一个关键前提:若两个链表相交,那么从相交节点开始到末尾的所有节点都是共享的,也就是说它们的尾节点必然是同一个(地址相同,而非值相同)。基于这个前提,我们先通过遍历统计两个链表的长度,判断尾节点是否一致(不一致则直接返回 null);再通过 “长度差对齐” 让两个链表的指针站在 “同一起跑线”—— 让长链表的指针先移动长度差的步数,确保后续同步前进时,两个指针能同时触达相交节点(若存在)。后来我们还优化了代码,用 “假设法” 简化了长链表与短链表的判断逻辑,避免了大量的 if-else 分支。这道题的启示在于,解决 “两个链表关系” 的问题时,首先要找到它们的共性特征(如尾节点是否相同),再通过 “补全长度差”“同步遍历” 等操作,把复杂的双链表问题转化为单链表的遍历问题,这种 “化繁为简” 的思路在算法题中尤为重要。
回顾这三道题,我们会发现单链表的算法题本质上是 “指针操作的艺术”—— 如何用最少的指针,通过最合理的移动逻辑,实现目标需求。快慢指针、三指针反转、长度对齐,这些技巧不是孤立的,而是可以组合使用的:比如在更复杂的 “带环链表” 问题中,我们会用到快慢指针判断环的存在,再用类似 “链表相交” 的思路找环的入口。因此,大家在学习这些题目时,不要只记住代码本身,更要理解每一步操作的逻辑的本质 —— 比如为什么快慢指针能找到中间节点?为什么反转后半段后对比就能判断回文?为什么长度对齐后同步遍历能找到相交节点?只有把这些 “为什么” 想清楚,才能在遇到新问题时举一反三,灵活运用。
算法学习从来不是一蹴而就的,可能一开始会对指针的移动逻辑感到困惑,会忘记反转链表时的指针顺序,会在长度统计时少算一个节点 —— 但这些都是正常的。建议大家在学习时多画图:用笔画出链表的结构,标注每个指针的位置,一步步模拟代码的执行过程。比如在 “链表相交” 中,画两个长度不同的链表,标出它们的长度差,模拟长链表指针移动对齐的过程;在 “回文链表” 中,画出反转前后的后半段结构,看对比时指针如何同步移动。画图不仅能帮我们理清思路,还能让我们快速发现代码中的错误(比如反转后返回了错误的头节点,或者对比时循环条件写错)。
最后,希望大家能把这三道题的思路沉淀下来,作为单链表算法的 “基础库”。后续再遇到链表相关的题目时,不妨先回想这些题目的解题逻辑,看看是否能复用或改造已有思路。算法的提升没有捷径,唯有多思考、多练习、多总结,才能在面对复杂问题时游刃有余。相信只要坚持下去,大家都能在数据结构与算法的学习之路上,走得更稳、更远!