【链表】回文链表

题目

在leetcode上有两个题
234. 回文链表
面试题 02.06. 回文链表

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

示例 1:

输入: 1->2
输出: false
示例 2:

输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

解法

遍历链表得到数组

  1. 遍历一遍链表得到值的数组
  2. 判断数组是否是回文的
class Solution {
    
    
    public boolean isPalindrome(ListNode head) {
    
    
        ListNode cur = head;
        ArrayList<Integer> list = new ArrayList<>();
        while (cur != null) {
    
    
            list.add(cur.val);
            cur = cur.next;
        }
        int n = list.size();
        // 使用stream得到int数组
        int[] array = list.stream().mapToInt(i -> i).toArray();
        for (int i = 0; i < n / 2; i++) {
    
    
            if (array[i] != array[n - i - 1]) {
    
    
                return false;
            }
        }
        return true;
    }
}

时间复杂度:O(n),其中 n指的是链表的元素个数。
空间复杂度:O(n),其中 n指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。

递归

为了想出使用空间复杂度为 O(1)的算法,你可能想过使用递归来解决,但是这仍然需要 O(n)的空间复杂度。

递归为我们提供了一种优雅的方式来方向遍历节点。

function print_values_in_reverse(ListNode head)
    if head is NOT null
        print_values_in_reverse(head.next)
        print head.val

如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。

算法
currentNode 指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer 是递归函数外的指针。若 currentNode.val != frontPointer.val 则返回 false。反之,frontPointer 向前移动并返回 true。

算法的正确性在于递归处理节点的顺序是相反的(回顾上面打印的算法),而我们在函数外又记录了一个变量,因此从本质上,我们同时在正向和逆向迭代匹配。

class Solution {
    
    
   ListNode temp;

public boolean isPalindrome(ListNode head) {
    
    
    temp = head;
    return check(head);
}

private boolean check(ListNode head) {
    
    
    if (head == null)
        return true;
    boolean res = check(head.next) && (temp.val == head.val);
    temp = temp.next;
    return res;
}
}

时间复杂度:O(n,其中 n指的是链表的大小。
空间复杂度:O(n),其中 n指的是链表的大小。我们要理解计算机如何运行递归函数,在一个函数中调用一个函数时,计算机需要在进入被调用函数之前跟踪它在当前函数中的位置(以及任何局部变量的值),通过运行时存放在堆栈中来实现(堆栈帧)。在堆栈中存放好了数据后就可以进入被调用的函数。在完成被调用函数之后,他会弹出堆栈顶部元素,以恢复在进行函数调用之前所在的函数。在进行回文检查之前,递归函数将在堆栈中创建 nn 个堆栈帧,计算机会逐个弹出进行处理。所以在使用递归时空间复杂度要考虑堆栈的使用情况。

这种方法不仅使用了 O(n) 的空间,且比第一种方法更差,因为在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个节点创建堆栈帧极大的限制了算法能够处理的最大链表大小。

反转前半部分链表

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

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

该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。

算法

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

  1. 找到前半部分链表的尾节点。 【快慢指针】
  2. 反转后半部分链表。 【反转链表】
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。
class Solution {
    
    
    public boolean isPalindrome(ListNode head) {
    
    
        if (head == null) {
    
    
            return true;
        }
         // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = getFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);
		
		 // 判断是否回文
        ListNode firstHalfCur = head;
        ListNode secondHalfCur = secondHalfStart;
        boolean result = true;
        // 注意这里是secondHalfCur不为空,不能是firstHalfCur。
        while (result && secondHalfCur != null) {
    
    
            if (firstHalfCur.val != secondHalfCur.val) {
    
    
                result = false;
            }
            firstHalfCur = firstHalfCur.next;
            secondHalfCur = secondHalfCur.next;
        }
        firstHalfEnd.next = reverseList(secondHalfStart);
        return result;
    }

    public ListNode reverseList(ListNode head) {
    
    
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
    
    
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

    public ListNode getFirstHalf(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;
    }
}

时间复杂度:O(n),其中 n指的是链表的大小。

空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。

我们知道栈是先进后出的一种数据结构,这里还可以使用栈先把链表的节点全部存放到栈中。
其实我们只需要拿链表的后半部分和前半部分比较即可,没必要全部比较,所以这里可以优化一下

public boolean isPalindrome(ListNode head) {
    
    
    if (head == null) {
    
    
        return true;
    }
    Stack<Integer> stack = new Stack<>();
    ListNode cur = head;
    while (cur != null) {
    
    
        stack.push(cur.val);
        cur = cur.next;
    }
    int size = stack.size();
    size >>= 1;
    while (size-- >= 0) {
    
    
        if (head.val != stack.pop()) {
    
    
            return false;
        }
        head = head.next;
    }
    return true;
}

猜你喜欢

转载自blog.csdn.net/qq_17677907/article/details/112962287