🚩传送门:牛客题目

题目

给定一个链表的 头节点 head ,请判断其是否为回文链表。

如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。

示例 1:
[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图1

输入: head = [1,2,3,3,2,1] 输出: true

示例 2:
[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图2

输入: head = [1,2] 输出: false

解题思路:将值复制到数组中后用双指针法

有两种常用的列表实现,分别为数组列表和链表。如果我们想在列表中存储值,它们是如何实现的呢 ?

  • 数组列表底层是使用数组存储值,我们可以通过索引在 [NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图3 的时间访问列表任何位置的值,这是由基于内存寻址的方式。
  • 链表存储的是称为节点的对象,每个节点保存一个值和指向下一个节点的指针。访问某个特定索引的节点需要 [NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图4 的时间,因为要通过指针获取到下一个位置的节点。

确定数组列表是否回文很简单,我们可以使用双指针法来比较两端的元素,并向中间移动。

复杂度分析

时间复杂度:[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图5。其中 [NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图6 指的是链表的元素个数。

  1. - 遍历链表并将值复制到数组中,![](https://cdn.nlark.com/yuque/__latex/bf7c2e3ac858e1c3496fd2f47a300139.svg#card=math&code=%5Csmall%20O%28n%29&height=23&id=HEoNk)。
  2. - 双指针判断数组是否为回文,执行了 ![](https://cdn.nlark.com/yuque/__latex/60194e02d36c8670b68317046bc0a4bf.svg#card=math&code=%5Csmall%20O%28%5Cfrac%7Bn%7D%7B2%7D%29&height=23&id=mvWX7) 次的判断,即 ![](https://cdn.nlark.com/yuque/__latex/bf7c2e3ac858e1c3496fd2f47a300139.svg#card=math&code=%5Csmall%20O%28n%29&height=23&id=ASzmC)。

空间复杂度:[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图7。其中 [NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图8 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。

我的代码

class Solution {
    public boolean isPalindrome(ListNode head) {
        List<Integer> vals = new ArrayList<Integer>();

        // 将链表的值复制到数组中
        ListNode currentNode = head;
        while (currentNode != null) {
            vals.add(currentNode.val);
            currentNode = currentNode.next;
        }

        // 使用双指针判断是否回文
        int front = 0;
        int back = vals.size() - 1;
        while (front < back) {
            if (!vals.get(front).equals(vals.get(back))) {
                return false;
            }
            front++;
            back--;
        }
        return true;
    }
}

解题思路:快慢指针

我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。

整个流程可以分为以下五个步骤:

  1. 找到前半部分链表的尾节点
  2. 反转后半部分链表。
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。

我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。

复杂度分析

时间复杂度:[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图9。其中 [NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图10 指的是链表的元素个数。

空间复杂度:[NC]96. 判断一个链表是否为回文结构 【快慢指针】 - 图11,我们只会修改原本链表中节点的指向。

我的代码

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null) return true;
        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);
        // 判断是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (result && p2 != null) {
            if (p1.val != p2.val) {
                result = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }        
        // 还原链表并返回结果
        firstHalfEnd.next = reverseList(secondHalfStart);
        return result;
    }

    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}