LeetCode 234.回文链表

题目传送门:回文链表


请判断一个链表是否为回文链表。

示例1:

输入: 1->2
输出: false

示例2:

输入: 1->2->2->1
输出: true

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?


方法一:将链表的值复制到数组中然后使用双指针法

该方法使用了数组的特性,因为数组查找起来非常简单,而链表只能从前到后查找。首先我们将链表的值复制到数组中,然后使用头尾两个指针,两个指针都往数组中间遍历,如果值都相等的话则链表为回文链表。

  • 时间复杂度:O(n),其中 n 指的是链表的元素个数。
    • 第一步:遍历链表并将值复制到数组中,O(n)
    • 第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)
    • 总的时间复杂度:O(2n) = O(n)
  • 空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        //1.新建ArrayList
        List<Integer> vals = new ArrayList<>();

        //把LinkedList的值赋给ArratList
        while(head != null) {
            vals.add(head.val);
            head = head.next;
        }

        //分别创建头尾两个指针,然后比较两个指针节点的值
        //要注意这里为包装类,不能使用!=来进行比较,需要使用! .equals()来做判断
        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;
    }
}

方法二:切成两半,把后半段反转,然后比较两半是否相等。

因为本题的进阶要求为使用 O(1) 的空间复杂度,所以我们不能使用额外的空间,那么,改怎么去解决这个问题呢?

避免使用 O(n) 额外空间的方法就是改变输入。

我们可以将链表的后半部分反转(修改链表结构),然后比较两半是否相等。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    // 在这个算法中,如果链表为奇数个节点,那么最后比较中经过反转后的链表要比前半段多一个最中间的节点
    public boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null) return true;
        ListNode slow = head;
        ListNode fast = head.next;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        if(fast != null) slow = slow.next; // 如果链表为偶数,则将slow移动到中间位置的下一个节点
        cut(head, slow); // 分割链表
        return isEqual(head, reverse(slow)); // 反转链表的后半段并返回结果
    }

    // 切断链表方法
    private void cut(ListNode head, ListNode cutNode) {
        while(head.next != cutNode) {
            head = head.next;
        }
        head.next = null;
    }

    // 反转链表方法
    private ListNode reverse(ListNode head) {
        ListNode newNode = null;

        while(head != null) {
            ListNode nextNode = head.next;
            head.next = newNode;
            newNode = head;
            head = nextNode;
        }
        return newNode;
    }

    // 比较两个链表的值是否相等的方法
    private boolean isEqual(ListNode l1, ListNode l2) {
        while(l1 != null && l2 != null) {
            if(l1.val != l2.val) return false;
            l1 = l1.next;
            l2 = l2.next;
        }
        return true;
    }
}

自己遇到的坑:我在用第二种方法做这个题的时候遇到了一个很大的坑,找了好久,最终还是写了测试程序 debug 之后,才意识到自己的问题。在第一次写第二种方法的时候 cut 方法中的 while 循环中的判断条件我写的是 head != cutNode ,发现测试算法最后提交时是错误的,通过不了,改成 head.next != cutNode 之后就成功了。没办法,自己空想想不出来,就只好去 debug 一步步看程序的运行结果,在好几遍的 debug 之后,我终于发现了错误原因。因为算法的 cut 方法的目标为切断链表,把链表一分为二后再来对后半段进行翻转和比较的操作。我们的目标为把从链表头部到 slow 指针前面的一个节点和从 slow 到尾节点切开。所以当 head != slow 的话,每次 slow 都会被切断(切的位置在 slow 后面),保留 slow 节点的这个值,所以算法出错。

举个例子:如果测试用例为 [1,0,1],是一个回文链表。如果 cut 方法中 while 的判断条件为 head != slow 的话,经过上面的指针移动 slow 会移动到链表中 0 这个节点。因为判断条件会使 head 移动到 slow 指针这个位置,所以下一步 head.next = null 的时候链表会在 0 这个节点后面切一刀。但是这一刀会影响 slow 这个将要被拿去翻转的链表,也就是 slow 这个链表也会被切断,变为只有一个 0 节点,之后的比较中就会出错。

但是判断条件如果为 head.next != cutNode 的话,切点就在 slow 指针前面,slow 这个后半段链表就不会受影响,而且当链表为奇数个节点时 slow 链表也会多出一个中间结点,但这并不影响后面的比较,所以这样写才是正确的。

如果博客内容有误,请联系我修改指正,非常感谢! 如果觉得这篇博客对你有用的话,就帮我点个小小的赞吧! 一起加油鸭, 越努力,越幸运!!!
原文地址:https://www.cnblogs.com/studywithme/p/13572491.html