最多能完成排序的块 图解题解
这道题到底在问什么
- arr
- [1,0,2,3,4]
- 输出
- 4
- 切法
- [1,0] | [2] | [3] | [4]
最优解:为什么这么做
一句话答案:LeetCode 769 最多能完成排序的块:数组是 0 到 n−1 的排列,从左扫维护前缀最大值,它一追上当前下标就说明这段自成一块、切一刀,时间 O(n)、空间 O(1)。
把 0 到 n−1 的排列切块,最多能切几块
给一个长度为 n 的数组 arr,它恰好是 0 到 n−1 这 n 个数各出现一次的排列。把它切成若干连续的块,每块单独排序后再按原顺序拼起来,要让整个数组变成升序,问最多能切成多少块。题面例子 arr=[1,0,2,3,4],答案是 4,切法是 [1,0] | [2] | [3] | [4]:头两个数排完序变 [0,1],后面三个本来就各就各位,拼起来正好升序。
枚举每个切点验一遍,为什么会慢
能想到的笨办法是枚举所有切法:每两个数之间切或不切,切完把每段排序、拼回去,看整体是否升序。切点组合有 2 的 n−1 次方种,逐段排序验证,n 稍大就跑不动。得找只扫一遍、当场判断某位能不能切的办法。
前缀最大值追上下标,为什么就能放心切一刀
先抓住排列这个条件:数组是 0 到 n−1 的排列,整体排好序后,下标 i 上站的必然是数字 i。所以一个位置能不能当切口,就看它左边这一段排完序能不能刚好填回自己的位置。
维护一个前缀最大值 maxv,也就是从头扫到当前位置为止见过的最大数字。扫到下标 i 时,若 maxv 正好等于 i,说明 0 到 i 这 i+1 个数字已经全部出现在前 i+1 格里、正好填满 0 到 i 这些位置,左边这段排完序就是 0 到 i、和右边彻底分家,这一刀就能落下。贪心在这里落在前缀最大值这把尺子上:maxv 一追平当前下标就立刻封块切一刀、往后再攒下一段,切出的块数自然最多。
一个变量记前缀最大值,一趟扫完数块数
代码就两个变量:maxv 记前缀最大值、初值设 0,chunks 记切出的块数。从左往右扫,每到一格先用当前数字更新 maxv(谁大取谁),再判断 maxv 是否等于当前下标,相等就 chunks 加一。扫到头,chunks 就是答案。初值设 0 是因为排列里一定含 0,第一格若是 0 就能立刻切;更新和判断的先后也不能颠倒,先更新 maxv 再比下标,错一步就会多切或少切一块。
[1,0,2,3,4] 五个位置,maxv 各追到哪
下标 0,数字 1,maxv 更新成 1;maxv 是 1、下标是 0,不相等,数字 0 还没露面,这块封不了。下标 1,数字 0,maxv 还是 1;这回 maxv 等于下标 1,{0,1} 正好填满前两格,[1,0] 自成一块,chunks 记 1。下标 2,数字 2,maxv 变 2、等于下标 2,[2] 单独一块,chunks 记 2。下标 3、下标 4 同理,数字各是 3 和 4,maxv 每次都追平下标,各切一刀。一趟扫完,chunks 停在 4。
光比相邻两格,为什么判不出切点
常见的写错是只看相邻两个数的大小,或者拿最小值去判切点。可 [1,0,2,3,4] 里下标 0 不能切、下标 1 能切,靠相邻比较说不清缘由——真正决定能否封块的是左边整段的最大值有没有落回自己的位置,这是从头累计的全局信息,相邻两格看不出来。另一个坑是 maxv 初值:设成 arr[0] 或负数,再配上先比后更新,下标就会错开一位,多切或漏切。整套逻辑只扫一遍、只用两个变量,时间 O(n)、空间 O(1)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3为什么?排好序后下标 i 上应该是数字 i。如果前 i+1 个数的最大值恰好是 i,说明 0..i 这些数字正好填满了 0..i 这些位置,左边自成一块、和右边再不纠缠,所以能切。
- 4维护前缀最大值 maxv我们让一个指针从左往右扫,一路维护「到目前为止见过的最大值」maxv。每到一格,先更新 maxv,再看 maxv 是否等于当前下标。
- 5maxv = max(0, 1) = 1指针落在下标 0,值是 1。更新前缀最大值:maxv = 1。
- 61 > 0,左边还欠着 0maxv(1) ≠ i(0)。出现了比 0 大的数,说明数字 0 还在后面没遇到,这一块没法封口,不能切。
- 7maxv = max(1, 0) = 1指针挪到下标 1,值是 0。maxv 取较大者还是 1(没变大)。
- 8切出第 1 块 [1,0]maxv(1) == i(1)!前两格里的数 {0,1} 正好填满位置 {0,1},左边 [1,0] 自成一块。切一刀,块数 = 1。
- 9maxv = max(1, 2) = 2指针到下标 2,值是 2。maxv 被刷新成 2。
- 10切出第 2 块 [2]maxv(2) == i(2)!到这里 0..2 的数正好填满 0..2 的位置,[2] 单独成块。块数 = 2。
- 11maxv = max(2, 3) = 3指针到下标 3,值是 3。maxv 刷新成 3。
- 12切出第 3 块 [3]maxv(3) == i(3)![3] 又是独立一块。块数 = 3。
- 13maxv = max(3, 4) = 4指针到最后一格下标 4,值是 4。maxv 刷新成 4。
- 14切出第 4 块 [4]maxv(4) == i(4)!最后一刀,[4] 独立成块。扫完一遍,总共切出 4 块,这就是答案。
- 15[1,0] | [2] | [3] | [4]每个「maxv == i」的位置都是一个切口。整趟一次遍历、一个变量,O(n) 就数完了块数。
- 16下面用倒序数组走一遍,体会「前面只要冒出一个大数,后面就被它牢牢绑住切不开」。
- 17maxv = 0,块数 = 0换成完全倒序的 [4,3,2,1,0],规则不变,看 maxv 怎么一路被 4 卡住。
- 18maxv = max(0, 4) = 4指针落在下标 0,第一格就是最大值 4,maxv 直接被顶到 4。
- 19开头就来个最大值 4maxv(4) ≠ i(0)。这个 4 要等到下标 4 才安顿好,在那之前哪也切不了。
- 20maxv 还卡在 4下标 1,值 3,比 4 小,maxv 不变,仍是 4。4 ≠ 1,切不开。
- 21依旧被 4 压着下标 2,值 2,maxv 还是 4。4 ≠ 2,继续不能切。
- 22马上到头了下标 3,值 1,maxv 仍 4。4 ≠ 3,还差一步。
- 23到末尾才追平,1 块直到最后一格 i=4,maxv 才等于下标。整个数组只能算 1 块——前面的大数把后面全锁死了,这正是贪心判据的威力。
- 27[1,0,2] 误判假如只盯相邻两格的大小关系,就解释不清「为什么 i=0 不能切、i=1 能切」。真正的依据是前缀最大值 maxv 何时追上下标,这是全局信息,相邻比较替代不了。
⚠️ 容易写错的地方
✗ 错:用「最小值」或单纯比较相邻元素判切点
✓ 对:维护「前缀最大值」与下标 i 比较
能否封块取决于左边最大值是否已落位(== i),不是局部相邻关系
✗ 错:max_so_far 初值设成 arr[0] 或 -1 后下标错位
✓ 对:因排列含 0,maxv 初值设 0、循环内先更新再比较 i
下标从 0 起,maxv 初值与比较时机错一位就会多切或少切
完整代码(Python / C++ / Java)
Python
class Solution:
def maxChunksToSorted(self, arr):
max_so_far = 0
chunks = 0
for i in range(len(arr)):
if arr[i] > max_so_far:
max_so_far = arr[i]
if max_so_far == i: # 前缀最大值追平下标 → 切
chunks += 1
return chunksC++
class Solution {
public:
int maxChunksToSorted(vector<int>& arr) {
int maxSoFar = 0, chunks = 0;
for (int i = 0; i < (int)arr.size(); i++) {
maxSoFar = max(maxSoFar, arr[i]);
if (maxSoFar == i) chunks++; // 追平就切
}
return chunks;
}
};Java
class Solution {
public int maxChunksToSorted(int[] arr) {
int maxSoFar = 0, chunks = 0;
for (int i = 0; i < arr.length; i++) {
maxSoFar = Math.max(maxSoFar, arr[i]);
if (maxSoFar == i) chunks++; // 追平就切
}
return chunks;
}
}复杂度
时间复杂度
O(n)
只从左到右扫一遍数组,每格做一次取最大值和一次相等判断,共 n 次 → O(n)
空间复杂度
O(1)
只用了 max_so_far 和 chunks 两个变量,不随数组规模增长 → O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最多能完成排序的块 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么前缀最大值等于下标,就一定能在这里切开?+
因为 arr 是 0 到 n−1 的排列,排好序后第 i 位放的就是数字 i。扫到下标 i 时前缀最大值等于 i,意味着 0 到 i 这 i+1 个数字已经全都出现在前 i+1 格里、不多不少填满 0 到 i 这些位置。这一段排完序恰好是 0 到 i,右边只剩 i+1 及更大的数,两边不会再交换到对方那边,所以在这里落刀一定合法。
如果数组不是排列,而是能重复的任意整数(LC768)怎么办?+
拿前缀最大值比下标就失灵了,因为排序后第 i 位不再必然是 i。改成比较前缀最大值和后缀最小值:在位置 i 能切,当且仅当前 i+1 个数的最大值不超过后面所有数的最小值——左段每个数都比右段小,切开才不会打乱顺序。需要先预处理一遍后缀最小值数组,整体仍是 O(n)。
maxv 初值为什么设 0,先更新还是先比较能换吗?+
初值设 0 是因为排列里必然有数字 0,若第一格就是 0,先更新 maxv 得到 0、再和下标 0 比正好相等,能立刻切出第一块。顺序不能换:必须先用当前数字更新 maxv,再拿它和下标比。要是先比后更新,或者把初值设成 arr[0]、负数,比较的时机就和下标错开一位,切口会整体偏移,答案多一块或少一块。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最多能完成排序的块 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。