【LeetCode每日一题】234.回文链表

每日一题

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) 空间复杂度解决此题?

总体思路

整体思路

这个算法采用快慢指针+链表反转的组合策略来检测链表是否为回文。核心思想是将链表从中间分成两部分,反转后半部分,然后比较前后两部分是否相同。如果相同,则是回文链表。

算法步骤

  1. 找到链表中点:使用快慢指针技巧定位链表中间节点
  2. 反转后半部分:将链表的后半部分反转
  3. 比较前后部分:逐个节点比较前后两部分的值是否相等
  4. 恢复链表:(可选)如果需要保持原链表结构,可以再次反转恢复

时间复杂度与空间复杂度

  • 时间复杂度 :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
}
相关推荐
不过普通话一乙不改名8 分钟前
第四章:并发编程的基石与高级模式之atomic包与无锁编程
开发语言·golang
秋难降8 分钟前
深入解析快速排序:原理、波动根源与优化之道
算法·排序算法·编程语言
睡不醒的kun15 分钟前
leetcode算法刷题的第二十一天
数据结构·c++·算法·leetcode·职场和发展·回溯算法·回归算法
小欣加油16 分钟前
leetcode 461 汉明距离
c++·算法·leetcode
Amber_371 小时前
深入理解Go 与 PHP 在参数传递上的核心区别
android·golang·php
一起努力啊~2 小时前
算法题打卡力扣第169题:多数元素(easy)
算法·leetcode·哈希算法
水冗水孚2 小时前
通俗易懂地理解深度遍历DFS、和广度遍历BFS
javascript·算法
VT.馒头3 小时前
【力扣】2704. 相等还是不相等
前端·javascript·算法·leetcode·udp
ssshooter3 小时前
上下文工程:为高级大型语言模型构建信息环境
人工智能·算法·设计模式
我也要当昏君4 小时前
5.2 I/O软件
java·网络·算法