力扣hot100-234.回文链表-快慢指针与反转详解
234. 回文链表:快慢指针与反转详解
题目链接:234. 回文链表
算法思路
链表 / 快慢指针 / 反转后半段 / 双指针比较回文的含义是:从左往右读,与从右往左读完全一样。
例如:
1 -> 2 -> 2 -> 1从两端向中间看:
第 1 个节点和倒数第 1 个节点:1 == 1 第 2 个节点和倒数第 2 个节点:2 == 2因此它是回文链表。
本题的核心流程是:
找中点 -> 奇数长度时跳过中点 -> 反转后半段 -> 从两端向中间比较1. 为什么链表需要先反转后半段
如果是数组,可以直接通过下标从两端读取:
nums[left]nums[right]但单链表只能沿着next从前往后走:
1 -> 2 -> 2 -> 1虽然可以从第一个节点走到最后一个节点,却不能从最后一个节点走回前一个节点。
也就是说,链表没有办法像数组一样直接比较:
第一个节点 与 最后一个节点 第二个节点 与 倒数第二个节点解决方式是把后半段反转。
例如:
原链表:1 -> 2 -> 2 -> 1 后半段:2 -> 1 反转后:1 -> 2现在可以同时从前向后读取两段:
左半段:1 -> 2 右半段:1 -> 2原本的“从两端比较”,就转换成了“两个指针从前向后比较”。
2. 如何找到中点:快慢指针
定义两个指针:
ListNodeslow=head;ListNodefast=head;移动规则:
slow 每轮走 1 步 fast 每轮走 2 步因此:
fast 到达末尾时,slow 正好到达链表中间。代码:
while(fast!=null&&fast.next!=null){slow=slow.next;fast=fast.next.next;}循环条件中的两个判断含义分别是:
fast != null:fast 当前仍在链表内。 fast.next != null:fast 还能再走两步,不会访问空指针。3. 偶数长度时,slow在哪里
假设链表为:
1 -> 2 -> 2 -> 1初始:
1 -> 2 -> 2 -> 1 ^ slow、fast第 1 轮后:
1 -> 2 -> 2 -> 1 ^ ^ slow fast第 2 轮后:
1 -> 2 -> 2 -> 1 ^ slow fast = null此时slow指向后半段的第一个节点:
1 -> 2 | 2 -> 1 ^ slow所以偶数长度时,直接从slow开始反转后半段即可。
4. 奇数长度时,为什么要跳过中点
假设链表为:
1 -> 2 -> 3 -> 2 -> 1快慢指针结束时:
1 -> 2 -> 3 -> 2 -> 1 ^ ^ slow fast此时:
slow 指向正中间节点 3 fast 不为 null,且停在最后一个节点 1中间节点3没有需要比较的配对节点:
1 <-> 1 2 <-> 2 3 不需要配对因此应跳过它:
if(fast!=null){slow=slow.next;}跳过后:
1 -> 2 -> 3 | 2 -> 1 ^ slow之后只反转2 -> 1,让它与左半段1 -> 2对齐比较。
为什么用fast != null判断奇数长度?
偶数长度:fast 每次刚好跨过两个节点,最终走到 null。 奇数长度:最后会剩一个节点无法再走两步,fast 停在最后一个节点,不是 null。5. 完整推演:1 -> 2 -> 3 -> 2 -> 1
第一步:找到中点并跳过它
快慢指针结束时:
1 -> 2 -> 3 -> 2 -> 1 ^ slow因为fast != null,这是奇数长度,跳过中间节点3:
1 -> 2 -> 3 -> 2 -> 1 ^ slow第二步:反转后半段
从slow开始的后半段是:
2 -> 1 -> null反转后,得到:
1 -> 2 -> null它代表原链表从右到左读到的节点值。
第三步:逐个比较
此时:
left: 1 -> 2 -> 3 -> ... right: 1 -> 2 -> null比较过程:
left 的 1 == right 的 1,继续。 left 的 2 == right 的 2,继续。 right 到达 null,比较完成。所以该链表是回文链表。
6. Java 代码完整注释
classSolution{publicbooleanisPalindrome(ListNodehead){// 空链表或只有一个节点时,从正反两个方向读都相同。if(head==null||head.next==null){returntrue;}// slow 每轮走一步,fast 每轮走两步。// fast 到末尾时,slow 会到达链表中间位置。ListNodeslow=head;ListNodefast=head;while(fast!=null&&fast.next!=null){slow=slow.next;fast=fast.next.next;}// fast 不为 null,说明节点数为奇数。// slow 此时指向正中间节点;中点无需比较,直接跳过。if(fast!=null){slow=slow.next;}// 反转后半段。// right 的遍历顺序等价于从原链表尾部向中间读取。ListNoderight=reverse(slow);// left 从原链表头部开始读取。ListNodeleft=head;// 后半段长度不会超过前半段。// 右半段全部匹配,就说明整条链表是回文。while(right!=null){if(left.val!=right.val){returnfalse;}left=left.next;right=right.next;}returntrue;}privateListNodereverse(ListNodehead){// pre 指向已经反转完成部分的头节点。ListNodepre=null;// cur 指向当前需要反转的节点。ListNodecur=head;while(cur!=null){// 保存原来的下一个节点。// 因为下一步会覆盖 cur.next,必须先保住后续链表。ListNodenext=cur.next;// 让当前节点指向前一个节点,完成当前节点的反转。cur.next=pre;// 移动指针,继续反转原链表中的下一个节点。pre=cur;cur=next;}// pre 指向反转后链表的新头节点。returnpre;}}7. 为什么只比较到right == null
right是反转后的后半段。
对于偶数长度:
左半段长度 == 右半段长度对于奇数长度,跳过中点后:
左侧可比较节点数 == 右半段长度所以无论奇偶,只要右半段的每个节点都与左侧对应节点相同,就已经完成了所有必要比较:
while(right!=null)不需要等待left走到null;奇数长度时,left还会多经过中间节点以及后半段,而它们都不应该重复参与比较。
8. 这段代码会改变原链表吗
会,且改变发生在这一步:
ListNoderight=reverse(slow);它会把原链表的后半段原地反转。
LeetCode 这题通常只要求返回是否回文,允许这种做法,因为题目不会在方法返回后继续检查原链表的结构。
如果在实际业务代码中,后续还要使用原链表的原始顺序,应当在比较结束后:
再次反转 right,把后半段恢复。这不会改变时间复杂度,仍然是O(n)。
9. 复杂度
假设链表有n个节点。
时间复杂度:
找中点:O(n) 反转后半段:O(n) 比较两半:O(n) 总计:O(n)虽然进行了多个阶段,但它们都是对链表进行常数次遍历,因此总时间复杂度仍是线性的。
额外空间复杂度:
O(1)只使用了有限个指针变量,没有使用数组、栈或HashSet。
一句话记忆:
快慢指针找中点;奇数长度跳过中点;反转后半段;用左右指针从前向后逐个比较。