LEETCODE 234Easy

回文链表

把找中点、反转后半、再对比拼在一起,就能在 O(1) 空间里判断链表是否回文。

问题拆解

判断单链表是不是回文。数组做法很简单:把值复制到列表里,用双指针从两端向中间比。但那是 O(n) 空间。

进阶要求 O(1) 空间。链表没法从尾往前走,所以关键是想办法让“后半段”能被正向遍历——把后半段原地反转,再和前半段逐一对照即可。

三步走:快慢指针找到中点,反转后半段,然后从两头同时向中间比较。整个过程只借用几个指针。

找中点 + 反转后半

public boolean isPalindrome(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    ListNode prev = null;
    while (slow != null) {
        ListNode nxt = slow.next;
        slow.next = prev;
        prev = slow;
        slow = nxt;
    }

    ListNode left = head, right = prev;
    while (right != null) {
        if (left.val != right.val) {
            return false;
        }
        left = left.next;
        right = right.next;
    }
    return true;
}
def isPalindrome(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next

    prev = None
    while slow:
        nxt = slow.next
        slow.next = prev
        prev = slow
        slow = nxt

    left, right = head, prev
    while right:
        if left.val != right.val:
            return False
        left = left.next
        right = right.next
    return True
func isPalindrome(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }

    var prev *ListNode
    for slow != nil {
        nxt := slow.Next
        slow.Next = prev
        prev = slow
        slow = nxt
    }

    left, right := head, prev
    for right != nil {
        if left.Val != right.Val {
            return false
        }
        left = left.Next
        right = right.Next
    }
    return true
}
// Box 链表反转后半段后,前半段的尾巴仍指向旧位置,双向对比会和借用检查冲突;
// 直接把值收集进 Vec 再双指针比较,同样是 O(n) 时间。
pub fn is_palindrome(head: Option<Box<ListNode>>) -> bool {
    let mut vals = Vec::new();
    let mut cur = &head;
    while let Some(node) = cur {
        vals.push(node.val);
        cur = &node.next;
    }
    let (mut i, mut j) = (0, vals.len().saturating_sub(1));
    while i < j {
        if vals[i] != vals[j] {
            return false;
        }
        i += 1;
        j -= 1;
    }
    true
}

快指针一次两步、慢指针一次一步,快指针到头时慢指针正好落在后半段的起点(长度为奇数时落在正中,中间那个节点不影响比较)。接着用三指针把从 slow 开始的后半段反转,prev 成为反转后的新头,也就是原链表的尾节点。

比较时让 left 从头、right 从尾(反转后的头)同时往中间走。因为后半段更短或等长,以 right 走空作为结束条件即可,前半段多出来的中间节点不必比。

复杂度

指标 复杂度 原因
时间 O(n) 找中点、反转、比较各扫描一部分,合计线性
空间 O(1) 只用若干指针,原地反转后半段

可以迁移的模式

  • “从两端向中间比”在链表里做不到,就把后半段反转成可正向遍历;
  • 快慢指针找中点是拆分链表的通用前置步骤;
  • 需要 O(1) 空间时,优先考虑原地改指针,而不是复制数据。

这道题是链表基本操作的组合练习:中点、反转、比较三个小技能各来一次。