题目描述:
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
算法思想:
方法一:双指针法
我们知道,判断一个数组的回文是非常简单的,只需要一头一尾两个指针,边走边检查即可,但是链表怎么办呢?
直接暴力,把链表存入数组,用数组的方法判断。
class Solution {
public boolean isPalindrome(ListNode head) {
//辅助数组部分 把链表的值全部存入数组
List<Integer> list = new ArrayList<>();
ListNode p = head;
while(p != null) {
list.add(p.val);
p = p.next;
}
//数组查找回文部分
int front = 0;
int back = list.size() - 1;
while(front < back) {
if(list.get(front) != list.get(back)) {
return false;
}
front++;
back--;
}
return true;
}
}
复杂度分析:
时间复杂度:辅助数组部分为O(n),数组查找回文部分为O(n/2),总的来说就是O(n)
空间复杂度:用到一个大小为n的辅助数组,为O(n)
方法二:递归
顺着方法一的思路,我们可以不用辅助数组就利用双指针去遍历吗?
其实,借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表。
先来看看二叉树后序遍历是怎么写的:
void traverse(TreeNode root) {
traverse(root.left);
traverse(root.right);
// 后序遍历代码
}
其实,树结构不过是链表的衍生,二叉树能实现后序遍历,我们链表也能!
/* 倒序打印单链表中的元素值 */
void traverse(ListNode head) {
if (head == null) return;
traverse(head.next);
// 后序遍历代码
print(head.val);
}
借助这个思想,right指针借助后序遍历抵达最后的节点,再通过弹出栈访问它的前一个节点(注:这里的递归实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已。
class Solution {
ListNode left;
public boolean isPalindrome(ListNode head) {
left = head;
return traverse(head);
}
public boolean traverse(ListNode right) {
if(right == null) return true;
boolean res = traverse(right.next);
res = res && (left.val == right.val);
left = left.next;
return res;
}
}
递归工作栈如图所示
复杂度分析
时间复杂度:需要遍历一遍链表,故为O(n)
空间复杂度:利用一个递归工作栈,O(n)
方法三:反转部分链表
方法一二都是O(n)的空间复杂度,我们找寻一下更节省空间的办法。
我们先设想这样的方法,把链表反转过后(参考之前的题目:反转链表I)存入一个新的链表,然后再与目标链表比较,判断是否相同,但这样空间复杂度还是O(n)。
借助这个思路,我们可以仅仅反转链表的一部分吗,这样不就只需要O(1)的空间复杂度了!
也就是说,我们只需要反转链表的后半部分,再与前半部分比较。
到底哪里才是后半部分呢,如果链表节点为奇数,那么中间节点(不包括中间节点)以后的部分就是后半部分,如果链表节点为偶数,那么靠后那个中间节点以后就是后半部分,如图所示
下面来看具体的反转过程,我们使用快慢指针来找中间节点
//使用快慢指针找到中间值
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//如果是奇数链表 那么中间值不参与比较
if(fast!=null) slow = slow.next;
代码执行效果如图所示
接下来,就是反转链表的工作了
ListNode left = head;
ListNode right = reverseList(slow);
最后,我们就把链表当作数组,从“两边向中间比较”
while(right != null) {
if(left.val != right.val){
return false;
}
left = left.next;
right = right.next;
}
return true;
注意这里的终止条件,很巧妙。
整体代码如下
class Solution {
public boolean isPalindrome(ListNode head) {
//使用快慢指针找到中间值
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//如果是奇数链表 那么中间值不参与比较
if(fast!=null) slow = slow.next;
//开始比较
ListNode left = head;
ListNode right = reverseList(slow);
while(right != null) {
if(left.val != right.val){
return false;
}
left = left.next;
right = right.next;
}
return true;
}
public ListNode reverseList(ListNode head) {
if(head == null) return null;
if(head.next==null) return head;
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}
复杂的分析
时间复杂度:需要遍历一遍链表,故为O(n)
空间复杂度:O(1)