题目说明:给你一个单链表的头结点,你需要判断这个链表是否是回文链表。
回文链表指不论从头部开始遍历链表,还是从尾部反过来遍历的结果是一样的,
示例,链表 A -> B -> A ,两种遍历的结果都是A,B,A是回文链表。
反例,链表 A -> B -> C,正序遍历是A,B,C,倒序遍历是C,B,A,因此就不是回文链表。
方法一,利用栈反转进行比较
如何实现呢?
根据回文链表的特性可以看出来以中间节点为分隔左右边的节点是对称的。
跟进我们之前求中间节点的知识,我们稍微分析一下很容易得到回文的解法。
一个非空链表的结点数有两种情况,奇数或偶数
head = A -> B -> C -> B -> A -> null 中间结点为C
head = A -> B -> C -> C -> B -> A -> null 中间结点为第二个C
首先找到链表的中间结点,并判断链表总的结点个数奇数还是偶数,
如果是奇数结点以中间结点为分隔,左边和右边的结点数是一样的,只需要反转左边或右边的链表,再和未反转的链表比较,相等则为回文链表。
如果是偶数结点,以中间结点为起始的右半部分结点数和左半部分的结点数是一样的,同样的我们可以选择反转左/右半部分链表,再和未反转链表进行比较。
具体实现来讲,如果链表总元素个数是奇数,我们选择反转左边链表,即头结点到中间结点之前的元素入栈,再遍历中间节点的后续结点到链尾,依次和栈内元素进行比较;
如果链表总元素个数是偶数,我们选择反转前半部分链表,将头结点到中间结点之前的元素入栈,再遍历中间结点到链尾,依次和栈内元素进行比较;
若遍历完成所有所有元素相等,则该单链表为回文链表。
思考1
我们反转链表破坏了原链表的顺序吗?
import java.util.List;
import java.util.Stack;
import javax.management.ListenerNotFoundException;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public static boolean isPalindrome(ListNode head) {
if(head == null){
return false;
}
// 哑结点,初始快慢指针位置为从头结点的前一个位置开始,便于计数。
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
ListNode midNode = null;
Boolean isEvent = true;
while (fast.next != null){
slow = slow.next;
if(fast.next.next != null) {
fast = fast.next.next;
} else{
// 总结点数为奇数,使快结点遍历到最后一个结点
fast=fast.next;
isEvent=false;
// 此时fast.next为null直接退出循环
break;
}
}
// 总结点数为偶数,会有两个中间节点,我们定义后面的结点为中间节点
midNode = isEvent ? slow.next : slow;
// 反转左边链表
Stack<ListNode> stack = new Stack<>();
ListNode p = dummy.next;
while(p != midNode){
stack.push(p);
p = p.next;
}
// 和右边链表进行比较
ListNode start = isEvent ? midNode : midNode.next;
while(start != null){
ListNode tmp = stack.pop();
if(start.val != tmp.val){
return false;
}
start = start.next;
}
return true;
}
}
时间复杂度是多少?
查找中间节点循环执行n/2次,反转左边链表循环执行n/2次,和右边链表比较循环执行n/2次,因此时间复杂度为O(n)
空间复杂度是多少?
查找中间节点仅需要额外几个临时变量来存储,反转左边链表需要n/2的空间,和右边链表比较也只需要常数的临时变量,因此空间复杂度为O(n)
方法二,利用头插法反转链表进行比较
解题思路其实和方法一类似,只不过我们把反转方法换成头插法反转链表。
需要注意的是我们用头插法插入链表后,链表本身的顺序就被破坏掉了,这个时候需要再比较完成后还原链表。
public static boolean isPalindrome2(ListNode head) {
if(head == null){
return false;
}
// 哑结点,初始快慢指针位置为从头结点的前一个位置开始,便于计数。
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
ListNode midNode = null;
Boolean isEvent = true;
while (fast.next != null){
slow = slow.next;
if(fast.next.next != null) {
fast = fast.next.next;
} else{
// 总结点数为奇数,使快结点遍历到最后一个结点
fast=fast.next;
isEvent=false;
// 此时fast.next为null直接退出循环
break;
}
}
// 总结点数为偶数,会有两个中间节点,我们定义后面的结点为中间节点
midNode = isEvent ? slow.next : slow;
// 头插法反转左边链表
// A -> B -> C
// head = A -> null
// head = B -> A -> null
// head = C -> B -> A -> null
// newNode.next = head;
// head = newNode
ListNode newNode = head;
head = null;
ListNode next = null;
while (newNode != midNode) {
next = newNode.next;
newNode.next = head;
head = newNode;
newNode = next;
}
// 和右边链表进行比较
boolean isPalindrome = true;
ListNode start = isEvent ? midNode : midNode.next;
ListNode p = head;
while (p != null) {
if (p.val != start.val) {
isPalindrome = false;
break;
}
p = p.next;
start = start.next;
}
// 还原链表
newNode = head;
head = null;
ListNode tail = newNode;
while (newNode != null) {
next = newNode.next;
newNode.next = head;
head = newNode;
newNode = next;
}
tail.next = midNode;
return isPalindrome;
}
时间复杂度是多少?
查找中间节点循环执行n/2次,反转左边链表循环执行n/2次,和右边链表比较循环执行n/2次,还原链表n/2次 因此时间复杂度为O(n)
空间复杂度是多少?
查找中间节点仅需要常数临时变量来存储,反转左边链表需要常数的空间,和右边链表比较也只需要常数的临时变量,因此空间复杂度为O(1)
思考1解答
我们反转链表破坏了原链表的顺序吗?
我们利用栈的特性来做的链表局部反转,并未正真修改链表的逻辑关系,只是做了入栈和出栈的操作,因此不会破坏原链表的顺序。