分治算法刷题总结

148. 排序链表

两个思路,一种是从顶向下的排序,另外的一种是从低向上的归并排序,归并排序的复杂度是NLogN

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode fast = head.next, slow = head; // 这里,slow最终指向的是前半段的中间,始终如此,但是如果fast=head的话,会有问题,问题是,slow可能是重点,也可能是后半段的起点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode tmp = slow.next;
        slow.next = null;

        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        ListNode h = new ListNode(0);
        ListNode res = h;
        while (left != null && right != null) {
            if (left.val < right.val) {
                h.next = left;
                left = left.next;
            } else {
                h.next = right;
                right = right.next;
            }
            h = h.next;
        }
        h.next = left != null ? left : right;
        return res.next;
    }
}

作者:Krahets
链接:https://leetcode.cn/problems/sort-list/solutions/13728/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

不傻!这段比刚才那段简单多了,它用的是——

“递归版归并排序”,思路跟“二分法”一模一样。


🧠 一句话先背住:

把链表从中间劈成两半左边排好序右边排好序,再合并两个有序链表


🌰 举个例子:

链表还是:

4 -> 2 -> 1 -> 3

🧩 第一步:找中间节点(快慢指针)

ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}

👉 slow 最后会停在中间节点的前一个
上图:

4 -> 2 -> 1 -> 3
↑    ↑
head slow
↑         ↑
         fast

最后 slow 停在 2,fast 停在 null,于是:

slow.next = 1 -> 3

🧩 第二步:从中间“掰断”

ListNode tmp = slow.next;
slow.next = null;

👉 现在链表被分成两半:

  • 左边:4 -> 2
  • 右边:1 -> 3

🧩 第三步:递归排序两边

ListNode left = sortList(head);   // 把左边排好
ListNode right = sortList(tmp);   // 把右边排好

👉 就像老师把全班分成两半,分别找两个班长把各半边排好序。


🧩 第四步:合并两个有序链表(merge)

ListNode h = new ListNode(0);
ListNode res = h;
while (left != null && right != null) {
    if (left.val < right.val) {
        h.next = left;
        left = left.next;
    } else {
        h.next = right;
        right = right.next;
    }
    h = h.next;
}
h.next = left != null ? left : right;

👉 就是谁小谁先上,最后把剩下的直接接上。


🧩 第五步:返回排好序的头

return res.next;

✅ 总结一句话(给你背下来):

这段代码就是先把链表从中间劈开,左右分别排好序,再合并,跟“分糖果”一样:一人一半,先各自理好,再合起来。


🎁 文字版动画:

原始:4 -> 2 -> 1 -> 3

劈开:
  左:4 -> 2
  右:1 -> 3

递归左:
  再劈:4 和 2
  合并:2 -> 4

递归右:
  再劈:1 和 3
  合并:1 -> 3

最后合并:
  2 -> 4
  1 -> 3
→ 1 -> 2 -> 3 -> 4 ✅

🧑‍🏫 送你一句人话:

它就像把一堆乱牌从中间撕开,左右两半分别理好,再合起来,一直撕到只剩一张牌为止。


🎯 小口诀(背下来):

找中间,劈两半,
左右排好再合并。

如果你想,我可以给你画成流程图,或者打印每一步的链表长什么样。要不要?

🎯 一句话先回答:

奇数也不怕,慢指针会停在“前半部分的最后一个节点”,后半部分多一个节点而已。


🧠 举个例子给你看:

假设链表是:

1 -> 2 -> 3 -> 4 -> 5

(5 个节点,奇数)


🔍 快慢指针怎么走:

java复制

ListNode fast = head.next; // 快指针先走一步
ListNode slow = head;      // 慢指针从头开始

表格复制

回合 fast 指向 slow 指向 说明
初始 2 1 快指针先走一步
第1步 4 2 各走一步
第2步 null 3 快指针再走两步,停住

✂️ 此时:

  • slow 停在 3
  • tmp = slow.next 就是 4 -> 5
  • slow.next = null 把链表掰断:

复制

前半:1 -> 2 -> 3
后半:4 -> 5

✅ 结论:

表格复制

节点总数 前半部分 后半部分
偶数 一半 一半
奇数 少一个 多一个

🧑‍🏫 送你一句人话:

慢指针就像“剪刀手”, 总是站在前半部分的尾巴上 ,后半部分哪怕多一个节点,也能稳稳接住。


🎯 小口诀(背下来):

复制

奇偶无所谓,慢针剪中间,
前半少一个,后半全接住。

如果你想,我可以给你 画成动画流程图 ,或者 打印每一步指针的位置 。要不要?

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null) {
            return head;
        }
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        ListNode dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode prev = dummyHead, curr = dummyHead.next;
            while (curr != null) {
                ListNode head1 = curr;
                for (int i = 1; i < subLength && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode head2 = curr.next;
                curr.next = null;
                curr = head2;
                for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode next = null;
                if (curr != null) {
                    next = curr.next;
                    curr.next = null;
                }
                ListNode merged = merge(head1, head2);
                prev.next = merged;
                while (prev.next != null) {
                    prev = prev.next;
                }
                curr = next;
            }
        }
        return dummyHead.next;
    }

    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }
        if (temp1 != null) {
            temp.next = temp1;
        } else if (temp2 != null) {
            temp.next = temp2;
        }
        return dummyHead.next;
    }
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/sort-list/solutions/492301/pai-xu-lian-biao-by-leetcode-solution/ 
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这短短挨骂好难理解, 请帮助我理解,我是个傻瓜

你一点都不傻,这段代码确实信息密度很高,而且用了一些“技巧”让人第一眼很难看懂。
我来用最通俗的方式给你讲清楚它到底在干嘛,一句一句拆给你看,就像讲故事一样。


🧠 先记住一句话:

这段代码是在用“归并排序”的思想,给链表排序,但没用递归,而是从下到上,一步一步合并


🌰 举个例子:

假设链表是:

4 -> 2 -> 1 -> 3

我们要把它排成:

1 -> 2 -> 3 -> 4

🧩 第一步:先数长度(就是数数)

int length = 0;
ListNode node = head;
while (node != null) {
    length++;
    node = node.next;
}

👉 就是从头走到尾,看看有几个节点。
比如 4->2->1->3,长度就是 4。


🧩 第二步:准备“假头”

ListNode dummyHead = new ListNode(0, head);

👉 这是链表的“万能技巧”:
弄个假的头节点,这样不管怎么变,真头都不会丢


🧩 第三步:开始“从小到大”合并

for (int subLength = 1; subLength < length; subLength <<= 1)

👉 这句话是核心!

  • subLength 一开始是 1,每次翻倍(<<=1 就是乘以 2)
  • 意思是:先两两合并,再四四合并,再八八合并……直到整个链表有序

🧩 第四步:每次“切两段”出来合并

ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
    curr = curr.next;
}
ListNode head2 = curr.next;
curr.next = null;

👉 这就是把链表切成两段,每段长度最多是 subLength。比如第一次 subLength = 1,那就是:

  • head1 是 4
  • head2 是 2
    然后把它俩合并成一段有序的小链表

🧩 第五步:合并两段(merge函数)

ListNode merged = merge(head1, head2);

👉 这个 merge 函数就是你熟悉的合并两个有序链表,谁小谁先上。


🧩 第六步:把合并好的接回去

prev.next = merged;
while (prev.next != null) {
    prev = prev.next;
}

👉 把合并好的那段接回主链表,然后 prev 走到最后,准备下一次合并。


🧩 第七步:继续下一轮

curr = next;

👉 刚才切完两段后,可能后面还有,就继续切继续合并。


✅ 总结一句话(给你背下来):

这段代码就是把链表先切成小段,小段排好序,再慢慢合并成大段,直到整个链表有序。
只不过它没用递归,而是从下到上,一步一步来,所以看起来有点绕。


🎁 我给你画个图(文字版):

初始:4 -> 2 -> 1 -> 3

subLength = 1:
  合并 [4] 和 [2] → 2 -> 4
  合并 [1] 和 [3] → 1 -> 3
结果:2 -> 4 -> 1 -> 3

subLength = 2:
  合并 [2->4] 和 [1->3] → 1 -> 2 -> 3 -> 4
结果:1 -> 2 -> 3 -> 4 ✅

🧑‍🏫 最后送你一句人话:

它就像打牌时理牌:先一张一张理好,再两张两张理好,再四张四张……直到全部理完。


如果你愿意,我可以给你重写成超级傻瓜版代码,每一步都带注释,甚至打印出来那种。你要吗?

问题定位:找中点的逻辑错误

在归并排序中,需要将链表 从中间节点的前一个位置断开 ,但你的代码中:

ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}

这段逻辑会让 slow 最终指向 链表的后半段起点 (而非前半段的终点),导致:

  1. 分割链表时,slow.next = null 会让前半段包含中点,后半段从 slow.next 开始(实际是空);
  2. 递归时会反复处理同一个子链表,触发 无限递归 ,最终抛出 StackOverflowError

反例验证:

假设链表是 1->2

  • fast 初始为 1fast.next2,进入循环;
  • slow 走到 2fast 走到 2.next(null),循环结束;
  • tmp = slow.next(null),slow.next = null 后,前半段是 1->2,后半段是 null
  • 递归 sortList(head) 时又会重复上述过程,无限递归。

427. 建立四叉树

题目比较难看明白,实际上连接了该能很快的写出来,说白了,就是一个递归的处理,先处理最左上交的元素,然后划分格子去处理,得到四个格子的数据

posted @ 2025-12-14 20:06  coder江  阅读(3)  评论(0)    收藏  举报