234. 回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
- 链表中节点数目在范围[1, 105] 内
- 0 <= Node.val <= 9
我常觉得解链表题好比理耳机线,通常链表题都不会涉及太多的算法,要得是细心和耐心,一不小心就可能缠作一团,所以做链表题大家拿笔画一画,就比较清楚了。
“回文”大家应该都知道啥意思,就是镜像,第一个和最后一个一样,第二个和倒数第二个一样……,以此类推。但是,链表没法像数组那样直接按索引随机读取,所以得把后半段反转一下,之后一个指针指向头,一个指针指向中间,依次往后检查。这一个题相融合了三个题:链表反转、寻找链表中点、判断回文。
先写链表反转,没啥好说的。方法也有好多种,记住一种就行了,记的多了还容易乱,我比较习惯头插法,啥叫头插法,就是不断的将“头”的后一个节点移到头的前一个节点,直到最后一个节点,就完成了反转,如下。
private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1, head);
ListNode cur = head;
while (cur.next != null) {
ListNode next = cur.next;
cur.next = next.next;
next.next = dummy.next;
dummy.next = next;
}
return dummy.next;
}
然后找中点,采用快慢指针的方式,快的一次走两步,慢的一次走一步,当快的走到末尾时,慢的正好是链表的一半。思想很简单,就是细节得想明白。比如,快的一次走两步,那是一步一步走走两次还是一次走两步,走到“末尾”是走到最后一个节点,还是走到不足两步就停下来,那如果这时候停下来,慢的还正好在中点这个位置上吗?
先来看是一步一步走,还是一次走两步。
一次走一步,走两次,如下。
private ListNode getMiddle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
if (fast.next != null) {
fast = fast.next;
}
}
return slow;
}
一次走两步。
private ListNode getMiddle(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;
}
是不是看起来差不多?实际上是不一样的,当n为偶数时,前一种会多走一步,n为奇数时两者一样。其实本题用哪个都行,关键你要关注到两者的不同,后面的代码也因之不同。
再来看第二个问题,走到“末尾”是走到最后一个节点,还是走到不足两步就停下来,如果这时候停下来,慢的还正好在中点这个位置上吗?假设链表上有n个节点,也就是能走n-1步,这意味着奇数个节点有偶数步,偶数个节点有奇数步。比如,如果链表有3个节点,那只有2步,有4个节点,有3步,对于快指针来说偶数步正好走到最后一个节点,奇数步走到倒数第二个节点;对于慢指针来说,偶数步走到第(n-1)/2 + 1个节点,如果n为3,那应该是走到第2个节点,正好是中间,奇数步的话,如果是“一次走两步”是走到第(n-1-1)/2 + 1个节点,如果n为4,也是走到第2个节点,链表前半段的末尾。还记得上面说,如果“一步一步走”,会多走一步,也就是会落在第3个节点,即后半段的开头。因此,第二个问题的答案可以笼统的说,慢指针会落在链表的中间节点上,只不过有时候不同走法前后会相差一个。
现在问题就简单了,下面是我写的第一个版本,当时的想法是找到中点,中点的下一个是后半段的头,然后从中间断开,将后半段反转,然后依次判断前半段和后半段反转后的节点值,只有完全一样才会继续往后走,否则直接返回false
,不知道大家看出问题没。
public boolean isPalindrome(ListNode head) {
// 采用一次走两步的方式
ListNode pre = getMiddle(head);
ListNode mid = pre.next;
pre.next = null;
if (mid == null) return true;
ListNode head2 = reverse(mid);
while (head != null || head2 != null) {
if (head != null && head2 != null) {
if (head.val != head2.val) return false;
head = head.next;
head2 = head2.next;
} else {
return false;
}
}
return true;
}
上面的写法,忽略了遇到奇数个节点时,getMiddle
切割的前后两段节点数是不一样的,前半段会多一个,这会导致head != null
而head2 == null
情况出现,进而返回false,比如[1, 2, 1],下面是改正后的。
public boolean isPalindrome(ListNode head) {
// 采用一次走两步的方式
ListNode pre = getMiddle(head);
ListNode mid = pre.next;
pre.next = null;
if (mid == null) return true;
ListNode head2 = reverse(mid);
while (head != null && head2 != null) {
if (head.val != head2.val) return false;
head = head.next;
head2 = head2.next;
}
return true;
}
事实上,head != null && head2 != null
的判断条件可以进一步精简为head2 != null
,因为getMiddle
已经对前后两端的长度做了保证,前半段的长度是不会小于后半段的。前后两段也可以不断开,getMiddle
使用的是“一次走两步”的方式,如果用的是“一次走一步走两次”的方式,返回值直接就是head2
,不用再下一个了。