判断一个链表是不是回文链表,可以借助栈或者是快慢指针的方法
这些都是根据左程云的算法课程整理出来的代码
1.借助栈结构,先进后出的特性
将链表遍历并且一次入栈,再将链表重新遍历,并将栈弹出,依次作对比,如果完全相同则表示是回文链表,如果不同,则不是
private static boolean isPalindrome(Node head){
Stack<Node> stack = new Stack<>();
Node cur = head;//将链表拷贝一份
//遍历链表并且入栈
while (null != cur){
stack.push(cur);
cur = cur.next;
}
//遍历链表并且出站,依次作对比
while (null != head){
if (head.value != stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
2.仍然借助栈,同时利用快慢指针
利用快慢指针
得到链表的中间的位置,将后面一半的链表依次入栈,同样利用先入后出的特性,重新遍历链表,并依次出栈,如果到栈清空,都相同,则表示是回文链表。
快慢指针
,在java语言中,没有指针的概念,我们这里的快慢指针是控制链表遍历的速度,在遍历链表时,同时准备2个变量进行遍历,一个变量一次遍历一个结点,一个变量一次遍历2个节点,如下:
public static class Node{
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
}
public static void test(Node head){
node1 = head;
node2 = head;
while(node2.next != null && node2.next.next != null){
node1 = node1.next;//一次遍历一个结点
node2 = node2.next.next;//一次遍历2个结点
}
}
当node2后面只有一个结点或者是没有结点时,结束,此时对node2来说已经没有可以遍历的节点了,此时node1处于中间位置,node1后面的节点正好是整个链表的后一半。而找到链表中间位置的这种方法,我们称之为快慢指针。
判断回文的具体实现代码如下
private static boolean isPalindrome(Node head){
if (null == head || null == head.next){
return true;
}
Stack<Node> stack = new Stack<>();
Node right = head.next;//慢指针
Node cur = head;//快指针
//利用快慢指针获得中间结点
while (cur.next != null && cur.next.next != null){
right = right.next;
cur = cur.next.next;
}
//右半边结点入栈
while (null != right){
stack.push(right);
right = right.next;
}
//出栈,与原链表遍历对比
while (!stack.isEmpty()){
if (head.value != stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
这种方式主要是利用了回文链表的特性,前一半的链表数据与后一半翻转后 的链表数据是一样的,所以只要翻转后一半的链表,与前一半的链表对比即可。
这种方式比上一个链表多了一个变量,但是入栈的空间少了一半,即额外空间减少了一半
3.不用栈,利用链表变量,翻转链表进行判断
利用快慢指针
得到链表中间位置,取右半边的链表并进行翻转,利用回文链表的特性,同时遍历原链表和右半边反转后的链表,一次对比,如果完全一致,则是回文链表。这边的代码理解起来有点困难,没有前面2种容易,但是如果理解单链表翻转的话,那么就很容易理解了,对于翻转单链表不太理解的可以网上搜资料或者是直接看我的一篇关于翻转单链表和双链表的代码
java反转单链表和双链表
private static boolean isPalindrome3(Node head){
Node n1 = head;
Node n2 = head;
//快慢指针,得到链表中间位置
while (n2.next != null && n2.next.next != null){
n1 = n1.next;
n2 = n2.next.next;
}
n2 = n1.next;
n1.next = null;
Node n3;
//取链表右半边,并翻转
while (null != n2){
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n2 = head;
n3 = n1;
//将原链表和已经翻转后的链表进行遍历,依次对比
while (n1 != null && n2 != null){
//如果哟不同的数值,则表示不是回文链表
if (n1.value != n2.value){
return false;
}
n1 = n1.next;
n2 = n2.next;
}
return true;
}
总结:
最简单快捷的是第一种方式,也是最好理解的,利用栈的特性,先入后出,相当于把链表进行翻转,再与原链表作对比,只不过需要额外空间较多,需要借用的额外空间是O(n),
第二种方式略有改进,因只入栈一半的链表,与原链表作对比。需要借助的额外空间是只有方式一的一半。
第三种方式代码比较难理解,思路却很简单,和方式一思路类似,只不过取的是链表后一半结点,翻转,与原链表作对比,如果存在不一样的节点数值,那就不是回文链表,这是利用了回文的特性,没有借助栈,只借助了几个变量,所需额外空间是O(1),常数级别的。第三种方式的代码,主要还是要理解链表的翻转,理解这个,就能很容易看懂第三种方式的代码。
后两种方式都有优化,优化的原理都是借助了回文的特性,右侧一半翻转与左侧完全一致。