题目:判断一个链表是否为回文结构
给定一个链表的头节点head,请判断该链表是否为回文结构。
例如:1 ->2 -> 1,返回true。1 -> 2 -> 2 -> 1,返回true。15 -> 6 -> 15,返回true。1 -> 2 -> 3,返回 false 。
链表结构:
class Node{
int value;
Node next;
public Node(int val){
this.value = val;
}
}
进阶:如果链表长度为 N ,时间复杂度达到O(N) ,额外空间复杂度达到 O(1)。
法一:借助辅助空间,使用栈
思路:
将链表 List 节点依次压入栈中,再依次弹出节点 node ;将 List 从头节点 head 开始比较,一旦发现不同则返回true;当head 为空时【比较结束】返回true。
图1 使用栈的思路
Code:
public boolean isPalindrome(Node head){
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while(cur != null){ //压栈
stack.push(cur);
cur = cur.next;
}
while(head != null){
if(head.value != stack.pop().value)
return false;
head = head.next;
}
return true;
}
时间复杂度:需要遍历整个 List ,所以 T(n) = O(n) 。
空间复杂度:由于需要保存 List 的所有节点,所以 S(n) = O(n) 。
法二、快慢指针法
看到这个标题,思考一下能否不用保存整个 List 去解决这道题?
根据回文的特性,回文 List 必然是关于中间的对称轴对称的,如果从中间开始比较,是否可以用原先一半的空间来解决它? ——Yes.
思路:
- 快指针 A 一次走 2 步,慢指针 B 一次走 1 步;当 A 走到 List 末尾时, B 刚好到中点;
- B 将后半部分遍历并压入栈中;
- A 再从头开始遍历,每遍历一个,栈 pop 一个进行比较。
- 某一步不相等 => false;遍历结束后且之前未返回 => true 。
Code:
public boolean isPalindrome2(Node head){
if(head == null || head.next == null) //空 or 仅有一个node的List是回文串
return true;
Node fast = head;
Node slow = head;
while(fast.next != null && fast.next.next != null ){
slow = slow.next; //最终到mid处
fast = fast.next.next; //最终到end处
}
Stack<Node> stack = new Stack<Node>();
while(slow != null){
stack.push(slow);
slow = slow.next; //继续从mid遍历到end
}
while(!stack.isEmpty()){
if(head.value != stack.pop().value)
return false;
head = head.next;
}
return true;
}
时间复杂度:需要遍历整个 List ,所以 T(n) = O(n) 。
空间复杂度:仅需要保存 List 的后半部分节点,相比法一少了一半,但是去除系数,S(n) = O(n) 。
法三、快慢指针升级版
法二我们已经用到了快慢指针,能否借助它们,实现彻底不用 O(N) 级别的辅助空间??
—— Yes.
首先我们能找到 List 的中点,让 List 一分为二,并让后半部分逆序;
接着前半部分从前往后,后半部分从后往前遍历,边遍历便比较。
如此,两个 node 的空间即解决了问题。
思路
- 快指针 A 一次走 2 步,慢指针 B 一次走 1 步;当 A 走到 List 末尾时, B 刚好到中点;
- 将中点的 next 指向 null ,右部分逆序;
- 两个 list 都从首开始遍历比较,直到结束。
- 得到 true / false 的结果后,记得将原结构恢复 。
细节
用 length-1 来计算:奇数个节点,中点指向null ;偶数个节点,慢指针会来到中点的前一个位置。如下图:
图2 让中点指向null
code
public boolean isPalindrome3(Node head){
if(head == null || head.next == null)
return true;
Node n1 = head;
Node n2 = head;
while(n2.next != null && n2.next.next!=null){
n1 = n1.next;
n2 = n2.next.next;
}
n2 = n1.next; //n2 -> 右半部分第一个node
n1.next = null; //mid.next -> null
// convert right part 局部结构:n1 -> n2 -> n3 -> ni -> ...
Node n3 = null;
while(n2 != null){
n3 = n2.next; // save n2的 next node
n2.next = n1; // 改变n2指向
n1 = n2; //移动n1
n2 = n3; //移动n2
}
n3 = n1; //此时n2为空,n1为last node ,用n3记录下来便与恢复结构
n2 = head; //n2 为left part first node
boolean res = true; //保存结果,最后才返回
//check 回文
while(n1 != null && n2 != null){
if(n1.value != n2.value){
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
//recover List
n1 = n3.next;
n3.next = null;
while(n1 != null){
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
时间复杂度:需要遍历整个 List ,所以 T(n) = O(n) 。
空间复杂度:仅仅用到了两个额外空间,S(n) = O(1) 。
法三的注意点:不能直接得到结果后立刻返回,要记得把原结构恢复;反转链表的操作要细心。