每日一题
2025.8.29
234.回文链表
题目
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
链表中节点数目在范围[1, 105] 内
0 <= Node.val <= 9
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
总体思路
整体思路
这个算法采用快慢指针+链表反转的组合策略来检测链表是否为回文。核心思想是将链表从中间分成两部分,反转后半部分,然后比较前后两部分是否相同。如果相同,则是回文链表。
算法步骤
- 找到链表中点:使用快慢指针技巧定位链表中间节点
- 反转后半部分:将链表的后半部分反转
- 比较前后部分:逐个节点比较前后两部分的值是否相等
- 恢复链表:(可选)如果需要保持原链表结构,可以再次反转恢复
时间复杂度与空间复杂度
- 时间复杂度 :O(n)
- 找中点:O(n/2)
- 反转后半部分:O(n/2)
- 比较前后部分:O(n/2)
- 总计:O(n)
- 空间复杂度 :O(1)
- 只使用了常数级别的指针变量
- 完全原地操作,不需要额外数据结构
代码
golang
go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func isPalindrome(head *ListNode) bool {
for head == nil || head.Next == nil {
return true
}
fast := head.Next
slow := head
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
second := reverseList(slow.Next)
first := head
for second != nil{
if second.Val != first.Val {
return false
}
second = second.Next
first = first.Next
}
return true
}
func reverseList(head *ListNode) *ListNode {
if head == nil {
return nil
}
var per *ListNode = nil
cur := head
for cur != nil {
tmp := cur.Next
cur.Next = per
per = cur
cur = tmp
}
return per
}
go
/**
* 回文链表:正着读和反着读都一样的链表
* @param head *ListNode 链表的头节点
* @return bool 如果是回文链表返回true,否则返回false
*/
func isPalindrome(head *ListNode) bool {
// 边界条件:
// 1. 空链表被认为是回文(没有元素,正反读都一样)
// 2. 单节点链表肯定是回文(只有一个元素)
if head == nil || head.Next == nil {
return true
}
// 步骤1:使用快慢指针找到链表中点
// slow - 慢指针,每次移动一步,最终会指向中点或中点前一个节点
// fast - 快指针,每次移动两步,用于判断何时到达链表末尾
slow := head
fast := head.Next
// 快指针移动:当fast和fast.Next都不为nil时继续移动
// fast != nil: 确保fast本身不是nil
// fast.Next != nil: 确保可以安全地移动两步
for fast != nil && fast.Next != nil {
slow = slow.Next // 慢指针前进一步
fast = fast.Next.Next // 快指针前进两步
}
// 循环结束后,slow指向中点或中点前一个节点(取决于链表长度的奇偶性)
// 步骤2:反转后半部分链表
// slow.Next开始是后半部分链表的头节点
second := reverseList(slow.Next)
// 断开前后两部分的连接,将链表分成两个独立的部分
slow.Next = nil
// 步骤3:比较前后两部分链表
first := head // 前半部分链表的头节点
// second是反转后的后半部分链表的头节点
for second != nil {
// 如果对应位置的节点值不相等,不是回文
if first.Val != second.Val {
return false
}
// 移动指针到下一个节点继续比较
first = first.Next
second = second.Next
}
// 如果所有对应节点的值都相等,是回文链表
return true
}
/**
* 反转链表的辅助函数
* @param head *ListNode 要反转的链表的头节点
* @return *ListNode 反转后链表的头节点
*/
func reverseList(head *ListNode) *ListNode {
var pre *ListNode = nil // 前驱节点指针,初始为nil
cur := head // 当前节点指针,从头节点开始
// 遍历链表,逐个反转指针方向
for cur != nil {
tmp := cur.Next // 保存下一个节点的引用
cur.Next = pre // 反转指针:当前节点指向前驱节点
pre = cur // 前驱指针前进到当前节点
cur = tmp // 当前指针前进到下一个节点
}
// 返回新的头节点(pre现在指向原链表的最后一个节点)
return pre
}
知识点
什么是快慢指针?
快慢指针(Fast and Slow Pointers)是一种常用的链表处理技巧,使用两个指针以不同的速度遍历链表。通常快指针每次移动两步,慢指针每次移动一步。
基本模式
go
// 基本快慢指针模板
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next // 慢指针移动一步
fast = fast.Next.Next // 快指针移动两步
}
// 循环结束后,slow指向中点或相关位置
主要应用场景
1. 找到链表中点
go
func findMiddle(head *ListNode) *ListNode {
if head == nil {
return nil
}
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
执行过程示例:
链表: 1 → 2 → 3 → 4 → 5
slow: 1 → 2 → 3
fast: 1 → 3 → 5 → nil
结果: slow指向3(中点)
链表: 1 → 2 → 3 → 4
slow: 1 → 2 → 3
fast: 1 → 3 → nil
结果: slow指向3(第二个中间节点)
2. 检测链表中的环
go
func hasCycle(head *ListNode) bool {
if head == nil {
return false
}
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true // 快慢指针相遇,说明有环
}
}
return false // 快指针到达末尾,说明无环
}
数学原理:如果链表有环,快指针最终会追上慢指针(就像两个人在环形跑道上跑步,速度快的人会追上速度慢的人)。
3. 找到环的起始节点
go
func detectCycle(head *ListNode) *ListNode {
if head == nil {
return nil
}
slow := head
fast := head
hasCycle := false
// 第一阶段:检测是否有环
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
hasCycle = true
break
}
}
if !hasCycle {
return nil
}
// 第二阶段:找到环的起点
slow = head
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return slow
}
(1). 为什么这个算法有效?
假设:
- 链表头部到环起点的距离:a
- 环起点到相遇点的距离:b
- 相遇点到环起点的距离:c
- 环的长度:L = b + c
第一阶段(检测环):
- 慢指针走过的距离:a + b
- 快指针走过的距离:a + b + k×L(k是快指针在环中跑的圈数)
- 因为快指针速度是慢指针的2倍:2(a + b) = a + b + k×L
- 简化得:a + b = k×L
- 进一步:a = k×L - b = (k-1)×L + c
第二阶段(找环起点):
- 慢指针从头开始走距离:a
- 快指针从相遇点开始走距离:c + (k-1)×L
- 因为 a = (k-1)×L + c,所以两者会在环起点相遇
(2). 可视化执行过程
示例链表:1 → 2 → 3 → 4 → 5 → 3(形成环)
text
链表结构:
1 → 2 → 3 → 4 → 5
↑_________|
环起点是节点3
第一阶段:检测环
text
步骤0: slow=1, fast=1
步骤1: slow=2, fast=3
步骤2: slow=3, fast=5
步骤3: slow=4, fast=4(fast从5→3→4)
此时slow==fast,检测到环
第二阶段:找环起点
text
重置: slow=1, fast=4(相遇点)
步骤1: slow=2, fast=5
步骤2: slow=3, fast=3(相遇!)
返回节点3(环起点)
4. 找到链表的倒数第K个节点
go
func findKthFromEnd(head *ListNode, k int) *ListNode {
fast := head
slow := head
// 快指针先移动k步
for i := 0; i < k; i++ {
if fast == nil {
return nil // 链表长度小于k
}
fast = fast.Next
}
// 快慢指针同时移动,直到快指针到达末尾
for fast != nil {
slow = slow.Next
fast = fast.Next
}
return slow
}