• 3.9

    • 因为是递归,所以判断好结束条件:是不是大于等于两个节点
    • fast 先走一步,防止两个节点时死循环
  • 2.11

    • 递归剪断交给 merge
特性回文链表 (Palindrome)归并排序 (Sort List)
操作目的找起点:只为了找到后半段从哪开始,好去反转它。切分:必须把链表切成两段长度严格小于原长度的子链表。
对两个节点的要求只要 slow 能跑到第 2 个节点就行,不管它是不是中点,它就是后半段。必须切成 [1][2]。如果切分失败导致一边是 [1, 2],一边是 [],递归就死循环。
推荐初始化fast = head (简单直观,偶数时 slow 在 n/2 + 1)fast = head.next (偏左,保证偶数时 slow 在 n/2,方便切断,slow.next=null)
2. 微观视角:压栈与出栈(拆解 [4, 2, 1, 3])
为了彻底搞懂,我们来人肉跑一遍 [4, 2, 1, 3] 的排序过程。

第一阶段:“递” —— 层层外包(压栈)
程序会一直执行 sortList,直到把链表切得只剩 1 个节点(因为 1 个节点本身就是有序的,这是递归的终点)。

第一层(大老板):拿到 [4, 2, 1, 3]。

切分:左 [4, 2],右 [1, 3]。

呼叫 A:sortList([4, 2])。(程序暂停,等待 A 的结果)

第二层(外包 A):拿到 [4, 2]。

切分:左 [4],右 [2]。

呼叫 A1:sortList([4])。(程序暂停,等待 A1 的结果)

第三层(外包 A1):拿到 [4]。

触发终止条件:if (head.next == null) return head;

A1 回复:“我干完了,给你 [4]”。

第二阶段:“归” —— 收工合并(出栈)
现在开始往回走了,大家开始兑现承诺,进行 merge。

回到第二层(外包 A):

A1 回来了,左边是 [4]。

A 继续呼叫 A2:sortList([2])。

A2 也是单个节点,直接返回 [2]。

A 开始干活(Merge):A 手里有左 [4] 和右 [2]。合并它们变成 [2, 4]。

A 回复大老板:“我干完了,给你 [2, 4]”。

回到第一层(大老板):

A 回来了,左边是 [2, 4]。

老板继续呼叫 B:sortList([1, 3])。

(... B 的过程和 A 一模一样,会切成 [1] 和 [3],然后合并成 [1, 3] ...)

B 回复大老板:“我干完了,给你 [1, 3]”。

大老板的最后一步:

左手拿着 [2, 4](有序)。

右手拿着 [1, 3](有序)。

老板干活(Merge):合并这两个链表 -> [1, 2, 3, 4]。

返回最终结果。

3. 代码里的“暂停”机制
看这两行代码:

Java
ListNode left = sortList(head);  // <--- 代码停在这里!
ListNode right = sortList(mid);  // <--- 等上一行彻底做完了,才会执行这一行
return merge(left, right);       // <--- 等上面两行都做完了,才会执行这一行
很多同学看不懂递归,是因为以为这三行是“瞬间”执行完的。 其实,当 sortList(head) 被调用时,程序会钻进那个函数里去,原来的函数就**挂起(Suspended)**了,保留现场,等待那个钻进去的函数 return 回来,它才继续往下走。

这就是**栈(Stack)**的概念。
  • 1.20

    • 快慢指针找中点(快的先走一步)+递归后 merge(类似与链表相加那个操作)(需要哨兵节点加尾指针)
  • 11.11

    • 双指针找中点(靠左)时,判断next||next.next,而不是之前
    • 在确认位置时,需要断开中链,slow.next=null
    • 先遍历断开,再放入merge
    • merge需要哨兵节点,判大小,连接
 
class Solution {
    public ListNode sortList(ListNode head) {
        if(head==null||head.next==null){
            return head;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while(fast!=null&&fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
 
 
        // ListNode ptr = head;
        // int count = 0;
        // while(ptr!=null){
        //     ptr = ptr.next;
        //     count++;
        // }
        // ListNode slow = head;
        // for(int i=0;i<count/2-1;i++){
        //     slow = slow.next;
        // }
        这里应该在 count/2-1 这里停止,即中点的前一个
 
        ListNode mid = slow.next;
        slow.next = null;
        
        ListNode left = sortList(head);
        ListNode right = sortList(mid);
        
        return merge(left,right);
    }
 
    public ListNode merge(ListNode l1,ListNode l2){
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        while(l1 != null&&l2 != null){
            if(l1.val<=l2.val){
                tail.next = l1;
                l1 = l1.next;
            }else{
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        tail.next = (l1==null)? l2:l1;
        return dummy.next;
    }
}

找到链表中点(快慢指针法)。 将链表从中间断开,分成左右两个子链表。 递归地对左右子链表排序。 合并两个已排序的子链表(merge 两个有序链表)。