题目
在leetcode上有两个题
234. 回文链表
面试题 02.06. 回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
解法
遍历链表得到数组
- 遍历一遍链表得到值的数组
- 判断数组是否是回文的
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),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。
算法
整个流程可以分为以下五个步骤:
- 找到前半部分链表的尾节点。 【快慢指针】
- 反转后半部分链表。 【反转链表】
- 判断是否回文。
- 恢复链表。
- 返回结果。
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;
}