【Hot100|23-LeetCode 234. 回文链表 - 完整解法详解】

一、问题理解

问题描述

给你一个单链表的头节点 head,请你判断该链表是否为回文链表。如果是,返回 true;否则,返回 false

回文 的定义:正着读和反着读都一样。

示例

示例 1:

text

复制代码
输入: head = [1,2,2,1]
输出: true

示例 2:

text

复制代码
输入: head = [1,2]
输出: false

示例 3:

text

复制代码
输入: head = [1,2,3,2,1]
输出: true

示例 4:

text

复制代码
输入: head = []
输出: true

要求

  • 时间复杂度: O(n)

  • 空间复杂度: O(1) (进阶要求,不包含递归栈空间)

  • 能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决? 这是本题的核心挑战。

二、核心思路:从中间一分为二,反转后半部分

基本思想

判断回文的核心是比较对称位置的节点值。对于数组,我们可以直接用双指针从两端向中间比较。但对于单向链表,我们无法直接从后往前遍历。

因此,一种经典的 O(1) 空间解法是:

  1. 找到中点: 使用快慢指针找到链表的中间节点。

  2. 反转后半部分: 将链表从中点之后的部分进行反转。

  3. 比较两半: 将前半部分与反转后的后半部分逐一比较节点值。

  4. (可选)恢复链表: 为了不破坏原链表结构,可以在比较完成后将后半部分再反转回来。

双指针与反转的结合

这道题巧妙地结合了 快慢指针链表反转 这两个核心技巧。我们主要依赖三个指针或操作:

  • slowfast 指针: 用于寻找链表的中间节点。

  • 反转操作: 用于反转后半部分链表,以便顺序比较。

三、代码逐行解析

方法一:快慢指针 + 反转后半部分(最优解,O(1) 空间)

Python 解法

python

复制代码
# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        # 1. 使用快慢指针找到链表的中间节点
        #    慢指针 slow 每次走一步,快指针 fast 每次走两步
        #    当 fast 走到末尾时,slow 正好指向中间节点
        slow, fast = head, head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # 2. 反转从 slow 开始的后半部分链表
        #    如果链表长度是奇数,slow 正好指向中间节点,反转中间节点之后的部分即可
        prev = None
        curr = slow
        while curr:
            # 保存下一个节点
            next_node = curr.next
            # 反转指针
            curr.next = prev
            # 移动指针
            prev = curr
            curr = next_node
        # 循环结束后,prev 指向反转后的后半部分链表的头节点

        # 3. 比较前半部分和反转后的后半部分
        left, right = head, prev
        # 只需要比较到 right 遍历完即可
        # 因为如果链表长度是奇数,中间节点会被包含在 right 中,但 left 会比 right 少一个节点?需要确认
        # 实际上,对于奇数长度 [1,2,3,2,1]:
        #   - 找到的 slow 是 3,反转后半部分 [3,2,1] 得到 [1,2,3]
        #   - left 是 [1,2,3], right 是 [1,2,3]?这里需要精确处理
        # 为了统一处理奇偶,我们可以在第二步反转时,从 slow 开始反转,这样对于奇数长度,中间节点会被反转后成为后半部分的最后一个节点。
        # 比较时,循环条件设为 while right: 即可,因为当 right 为 None 时,说明后半部分已遍历完,比较结束。
        # 但这样 left 可能会遍历到中间节点?仔细分析:反转后半部分后,right 是反转后的头。对于奇数长度,原链表 slow 指向中间节点,反转后半部分后,中间节点成为后半部分的尾。
        # 此时 left 从头开始,right 从反转后的头开始,它们遍历的长度恰好是 floor(n/2)。中间节点不会被比较,因为它与自身相等,不影响结果。
        while right:
            if left.val != right.val:
                return False
            left = left.next
            right = right.next

        # 4. (可选)如果需要恢复链表,可以在这里再次反转后半部分
        # restore_head = self.reverseList(prev) # 需要定义反转函数

        return True
Java 解法

java

复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        // 1. 快慢指针找中点
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        // 2. 反转后半部分链表
        ListNode prev = null;
        ListNode curr = slow;
        while (curr != null) {
            ListNode nextNode = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextNode;
        }
        // 此时 prev 是反转后的后半部分的头节点

        // 3. 比较前半部分和后半部分
        ListNode left = head;
        ListNode right = prev;
        while (right != null) {
            if (left.val != right.val) {
                return false;
            }
            left = left.next;
            right = right.next;
        }

        // 4. (可选)恢复链表
        // reverseList(prev);

        return true;
    }
}

方法二:将值复制到数组后用双指针(简单解法,O(n) 空间)

Python 解法

python

复制代码
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        # 1. 将链表节点值复制到数组
        vals = []
        current_node = head
        while current_node:
            vals.append(current_node.val)
            current_node = current_node.next

        # 2. 使用双指针判断数组是否为回文
        left, right = 0, len(vals) - 1
        while left < right:
            if vals[left] != vals[right]:
                return False
            left += 1
            right -= 1

        return True
Java 解法

java

复制代码
class Solution {
    public boolean isPalindrome(ListNode head) {
        // 1. 将值复制到 ArrayList
        List<Integer> vals = new ArrayList<>();
        ListNode curr = head;
        while (curr != null) {
            vals.add(curr.val);
            curr = curr.next;
        }

        // 2. 双指针判断
        int left = 0;
        int right = vals.size() - 1;
        while (left < right) {
            if (!vals.get(left).equals(vals.get(right))) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

四、Java 与 Python 语法对比

(此部分内容与反转链表文章中的语法对比高度相似,在此保留以保持结构完整,但内容上关联回文链表的代码)

操作 Java Python
链表节点类 需要预定义 public class ListNode { ... } 需要预定义 class ListNode: ...
创建节点 new ListNode(val) ListNode(val)
检查 null node == null node is None
访问属性 node.next node.next
修改属性 node.next = otherNode node.next = otherNode
List/数组操作 List<Integer> list = new ArrayList<>(); list.add(val); list.get(index); vals = [] vals.append(val) vals[left]

五、实例演示

示例: head = [1, 2, 3, 2, 1] (奇数长度)

  1. 初始状态:

    • 链表:1 → 2 → 3 → 2 → 1 → null
  2. 找中点(快慢指针):

    • slow = 1, fast = 1

    • 第1步后:slow = 2, fast = 3

    • 第2步后:slow = 3, fast = 1fast 走两步,到达最后一个节点 1

    • fast.nextnull,循环结束。slow 指向中间节点 3

  3. 反转后半部分(从 slow 开始):

    • 待反转部分:3 → 2 → 1 → null

    • 反转后:1 → 2 → 3 → nullprev 指向 1

  4. 比较两半:

    • left 指向原链表头 1

    • right 指向反转后的后半部分头 1

    • 比较 11:相等。移动:left=2, right=2

    • 比较 22:相等。移动:left=3, right=3

    • 此时 right 指向 3left 也指向原链表的 3,但继续比较:

    • 比较 33:相等。移动:left=2(原链表的后半部分), right=null

    • while right: 条件不满足,循环结束。

    • 注意: 实际上我们比较了 n/2 + 1 次?这是因为我们的反转包括了中间节点。对于奇数长度,中间节点与自己比较,不影响结果,但为了确保 leftright 的边界处理正确,循环条件设为 while right: 是安全的,因为它保证只比较后半部分存在的节点。当 rightnull 时,所有不对称的可能性都已检查完毕。由于前半部分多出的中间节点(在 left 中)不需要与任何人比较,因此结果是正确的。

六、关键细节解析

1. 为什么用快慢指针找中点?

因为链表不能随机访问,无法直接通过索引找到中间位置。快慢指针可以在一次遍历中找到中间节点,满足 O(n) 时间复杂度的要求。当 fast 走到末尾时,slow 正好指向中间(偶数长度时指向后半部分的第一个节点)。

2. 如何统一处理链表长度的奇偶性?

  • 偶数长度 [1,2,2,1]slow 会指向第二个 2。反转后半部分 [2,1] 得到 [1,2]。前半部分 [1,2][1,2] 完美一一对应。

  • 奇数长度 [1,2,3,2,1]slow 指向中间的 3。反转后半部分 [3,2,1] 得到 [1,2,3]。前半部分 [1,2,3][1,2,3] 比较。中间的 3 与自己比较了一次,这不会导致错误,因为回文要求它等于自身。

3. 为什么反转后半部分而不是前半部分?

反转后半部分更方便。因为我们有指向原链表头的指针 head,可以顺序遍历前半部分。如果反转前半部分,我们将丢失前半部分的起始点,需要额外处理。反转后半部分后,我们只需将反转后的新头与原来的头进行比较即可。

4. 如何处理空链表或单节点链表?

这两种情况都是回文。在找中点前的循环 while fast and fast.next: 中,空链表 fastNone,不进入循环,slowNone。后续反转和比较循环 while right: 也不会执行,函数直接返回 True。单节点链表 fast.nextNone,同样不进入循环,slow 指向唯一的节点。反转部分只有一个节点,prev 指向该节点。比较循环 while right: 执行一次,比较 left.valright.val(同一个节点),相等,返回 True

5. 空间复杂度 O(1) 的解法为什么是最优的?

虽然将值复制到数组的方法简单易懂,但它需要 O(n) 的额外空间。题目要求能否用 O(1) 空间解决,这迫使我们必须在不使用额外线性空间的情况下操作链表,考察了对链表指针的熟练运用。

七、复杂度分析

方法一:快慢指针 + 反转后半部分

  • 时间复杂度: O(n),其中 n 是链表的长度。需要遍历链表找中点(O(n/2))、反转后半部分(O(n/2))、比较两半(O(n/2)),总体仍是线性时间。

  • 空间复杂度: O(1)。我们只使用了 slowfastprevcurrnext_nodeleftright 等常数个指针变量。

方法二:复制到数组

  • 时间复杂度: O(n),遍历链表复制到数组 O(n),双指针比较 O(n/2)。

  • 空间复杂度: O(n),需要额外一个大小为 n 的数组或列表来存储节点值。

八、其他解法

解法三:递归法(巧妙但空间复杂度高)

利用递归的特性,在递归返回时与链表头节点进行比较。这实际上利用了系统栈,空间复杂度为 O(n)。

python

复制代码
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        self.front_pointer = head

        def recursively_check(current_node):
            if current_node is not None:
                if not recursively_check(current_node.next):
                    return False
                if self.front_pointer.val != current_node.val:
                    return False
                self.front_pointer = self.front_pointer.next
            return True

        return recursively_check(head)

分析: 代码非常简洁,但递归深度等于链表长度,对于长链表有栈溢出的风险,且空间复杂度 O(n),不符合进阶要求。

九、常见问题与解答

Q1: 为什么不能直接反转整个链表然后比较?

A1: 如果直接反转整个链表得到一个新链表 newHead,然后比较原链表 head 和新链表,那么比较过程中原链表已经被反转破坏了。除非我们先复制一份原链表再反转,但这又会引入 O(n) 的空间。因此,比较两半的方案更优。

Q2: 比较两半时的循环条件为什么是 while right: 而不是 while left and right:

A2: 因为经过我们的分割和反转,right 链表(反转后的后半部分)的长度总是小于等于 left 链表(前半部分)的长度(奇数时相等,偶数时 right 短一个)。所以当 right 遍历完时,所有需要比较的节点都已比较完毕,剩余的 left 节点(如果有的话,即奇数长度的中间节点)不需要再比较。这样可以简化循环条件。

Q3: 如何在比较后恢复链表原状?

A3: 可以在比较之前记录下反转部分的起始节点 slow 和反转后的头节点 prev。比较完成后,再次调用反转函数 reverseList(prev),将后半部分反转回原来的顺序,然后再将 slow 节点与反转后的头节点连接(实际上因为反转函数会返回新的头,即原来的 slow,只需将 slow.next 指向 None 即可断开,但为了完全恢复,需要重新连接)。不过本题没有要求恢复,因此通常省略这一步。

Q4: 如果链表很大,快慢指针找中点会有什么风险?

A4: 快慢指针是一种非常安全的 O(1) 空间方法。它只遍历链表一次,没有使用额外内存,因此可以处理任意大小的链表(只要内存能存下链表本身)。相比之下,递归或复制数组的方法在处理超大链表时可能会因为内存不足而失败。

十、相关题目

1. LeetCode 206. 反转链表

核心操作: 本题是回文链表的基础。掌握反转链表是实现 O(1) 空间解法的前提。

python

复制代码
def reverseList(head):
    prev = None
    curr = head
    while curr:
        next_node = curr.next
        curr.next = prev
        prev = curr
        curr = next_node
    return prev

2. LeetCode 876. 链表的中间结点

核心操作: 本题用于寻找链表的中间节点,是回文链表的第一步。

python

复制代码
def middleNode(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

3. LeetCode 143. 重排链表

思路: 这道题结合了找中点、反转链表和合并链表三个操作。先将链表从中间分开,反转后半部分,然后将两个链表交错合并。

python

复制代码
def reorderList(head):
    if not head or not head.next: return
    # 1. 找中点
    slow, fast = head, head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    # 2. 反转后半部分
    prev, curr = None, slow
    while curr:
        next_node = curr.next
        curr.next = prev
        prev = curr
        curr = next_node
    # 3. 合并前半部分和反转后的后半部分
    first, second = head, prev
    while second.next:
        first.next, first = second, first.next
        second.next, second = first, second.next

4. LeetCode 面试题 02.06. 回文链表

相同题目: 与本题完全一致。

十一、总结

核心要点

  • 问题本质: 判断链表是否中心对称。

  • 最优解法: 快慢指针找中点 + 反转后半部分 + 双指针比较。该方法时间复杂度 O(n),空间复杂度 O(1)。

  • 核心技巧: 将回文比较问题转化为两个顺序链表的比较问题,利用链表反转改变遍历方向。

算法步骤(O(1) 空间)

  1. 找中点: 使用快慢指针找到链表的中间节点 slow

  2. 反转后半部分: 反转从 slow 开始的链表,得到新的头节点 prev

  3. 比较: 设置指针 left = head, right = prev,依次比较 left.valright.val,直到 rightnull。若所有值相等,则为回文,否则不是。

  4. (可选)恢复链表: 如果需要,可再次反转 prev 部分。

复杂度对比

解法 时间复杂度 空间复杂度 优点 缺点
复制到数组 O(n) O(n) 思路简单,易于实现 占用额外内存,不符合进阶要求
递归 O(n) O(n) 代码简洁 有栈溢出风险,空间复杂度高
快慢指针+反转 O(n) O(1) 最优解,满足进阶要求 代码稍复杂,需小心指针操作

扩展思考

掌握回文链表的解法,不仅解决了这一道题,更重要的是学会了如何将 "反转""双指针" 这两个操作结合起来解决链表问题。这种组合思路在 重排链表旋转链表 等问题中也会用到。可以说,反转链表是操作"形",而回文链表是应用"意"。通过这道题,你应该能更深刻地理解如何通过修改指针来改变链表的遍历逻辑,从而解决复杂问题。

相关推荐
小冻梨6661 小时前
ABC444 C - Atcoder Riko题解
c++·算法·双指针
菜鸡儿齐1 小时前
leetcode-找到字符串中所有字母异位词
算法·leetcode·职场和发展
不想看见4041 小时前
Combinations -- 回溯法--力扣101算法题解笔记
数据结构·算法
凤年徐1 小时前
优选算法——双指针专题 3.快乐数 4.盛水最多的容器
开发语言·数据结构·c++·算法
隔壁大炮2 小时前
第二章 多层神经网络
人工智能·深度学习·神经网络·算法
流云鹤2 小时前
2026牛客寒假算法基础集训营1(B C E G K L)
c语言·算法
你怎么知道我是队长2 小时前
C语言---排序算法9---堆排序法
c语言·算法·排序算法
若水不如远方2 小时前
分布式一致性原理(四):工程化共识 —— Raft 算法
分布式·后端·算法
小亮✿2 小时前
算法—并查集
数据结构·c++·算法