769. 最多能完成排序的块
问题描述
给定一个长度为 n 的整数数组 arr
,其中 arr
是 [0, 1, ..., n-1]
的一种排列。
我们将数组分割成若干个"块"(分区),并对每个块单独进行排序,然后按顺序连接这些块,使得整个数组有序。
问最多能将数组分成多少个块?
示例:
输入: arr = [4,3,2,1,0]
输出: 1
解释: 分成 [4,3,2,1,0] 一个块,排序后得到 [0,1,2,3,4]
如果分成多个块,无法得到有序数组
输入: arr = [1,0,2,3,4]
输出: 4
解释: 可以分成 [1,0], [2], [3], [4] 四个块
分别排序后连接得到 [0,1,2,3,4]
算法思路
贪心算法 + 最大值检查:
- 遍历数组,维护当前遍历到的最大值
- 当最大值等于当前索引时,说明可以在这里分割
- 因为如果
max(arr[0..i]) == i
,则子数组arr[0..i]
包含了[0..i]
的所有数字 - 这样排序后一定能得到正确的顺序
核心思想:一个块能独立排序的充要条件是:该块包含的数字恰好是 [0..k]
的连续整数。
代码实现
方法一:贪心分割法(推荐解法)
class Solution {
/**
* 计算最多能将数组分成多少个可排序的块
*
* @param arr 长度为n的数组,是[0,1,...,n-1]的排列
* @return 最多的块数
*/
public int maxChunksToSorted(int[] arr) {
// 输入校验
if (arr == null || arr.length == 0) {
return 0;
}
int n = arr.length;
// 特殊情况:单元素
if (n == 1) {
return 1;
}
int chunks = 0; // 记录块的数量
int maxSoFar = 0; // 记录遍历到的最大值
// 遍历数组
for (int i = 0; i < n; i++) {
// 更新当前最大值
maxSoFar = Math.max(maxSoFar, arr[i]);
// 关键:如果最大值等于当前索引
// 说明arr[0..i]包含了[0..i]的所有数字
// 可以在这里分割
if (maxSoFar == i) {
chunks++;
}
}
return chunks;
}
}
方法二:优化(提前终止)
class Solution {
/**
* 优化:在某些情况下可以提前判断
*
* @param arr 输入数组
* @return 块的数量
*/
public int maxChunksToSorted(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int n = arr.length;
// 如果数组已经有序,每个元素都可以单独成块
boolean sorted = true;
for (int i = 0; i < n; i++) {
if (arr[i] != i) {
sorted = false;
break;
}
}
if (sorted) {
return n;
}
int chunks = 0;
int maxSoFar = 0;
for (int i = 0; i < n; i++) {
maxSoFar = Math.max(maxSoFar, arr[i]);
if (maxSoFar == i) {
chunks++;
}
}
return chunks;
}
}
方法三:栈模拟法
import java.util.*;
class Solution {
/**
* 使用栈模拟分割过程
* 逻辑更直观,便于理解
*
* @param arr 输入数组
* @return 块的数量
*/
public int maxChunksToSorted(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
// 使用栈存储每个块的最大值
Stack<Integer> stack = new Stack<>();
for (int num : arr) {
// 如果当前数字大于栈顶,可以开始新块
if (stack.isEmpty() || num > stack.peek()) {
stack.push(num);
} else {
// 当前数字小于等于某个块的最大值
// 需要合并块
int currentMax = stack.peek();
while (!stack.isEmpty() && num <= stack.peek()) {
stack.pop();
}
// 合并后的块的最大值
stack.push(currentMax);
}
}
return stack.size();
}
}
算法分析
-
时间复杂度:O(n)
- 只需要遍历数组一次
- 每个元素的处理是 O(1)
-
空间复杂度:
- 方法一:O(1),只使用常数额外空间
- 方法二:O(1),同上
- 方法三:O(n),栈空间
-
算法特点:
- 贪心策略:尽可能多地分割
- 关键:
max(arr[0..i]) == i
是分割点的充要条件 - 一次遍历解决问题
算法过程
arr = [1,0,2,3,4]
:
- i=0:
maxSoFar=max(0,1)=1
,1≠0
,不分割 - i=1:
maxSoFar=max(1,0)=1
,1==1
,chunks=1
- i=2:
maxSoFar=max(1,2)=2
,2==2
,chunks=2
- i=3:
maxSoFar=max(2,3)=3
,3==3
,chunks=3
- i=4:
maxSoFar=max(3,4)=4
,4==4
,chunks=4
- 返回:4
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:只能一个块
int[] arr1 = {4,3,2,1,0};
System.out.println("Test 1: " + solution.maxChunksToSorted(arr1)); // 1
// 测试用例2:多个块
int[] arr2 = {1,0,2,3,4};
System.out.println("Test 2: " + solution.maxChunksToSorted(arr2)); // 4
// 测试用例3:已排序
int[] arr3 = {0,1,2,3,4};
System.out.println("Test 3: " + solution.maxChunksToSorted(arr3)); // 5
// 测试用例4:单元素
int[] arr4 = {0};
System.out.println("Test 4: " + solution.maxChunksToSorted(arr4)); // 1
// 测试用例5:两个元素逆序
int[] arr5 = {1,0};
System.out.println("Test 5: " + solution.maxChunksToSorted(arr5)); // 1
// 测试用例6:两个元素有序
int[] arr6 = {0,1};
System.out.println("Test 6: " + solution.maxChunksToSorted(arr6)); // 2
// 测试用例7:复杂情况
int[] arr7 = {2,0,1};
System.out.println("Test 7: " + solution.maxChunksToSorted(arr7)); // 1
// 测试用例8:空数组
int[] arr8 = {};
System.out.println("Test 8: " + solution.maxChunksToSorted(arr8)); // 0
}
关键点
-
关键:
- 块能独立排序的充要条件是:包含
[0..k]
的所有数字 - 等价于
max(arr[0..i]) == i
- 块能独立排序的充要条件是:包含
-
贪心策略:
- 只要满足条件就分割
- 这样能获得最多的块数
-
边界处理:
- 空数组
- 单元素
- 已排序数组
-
算法正确性:
- 每个分割点都保证了前面的数字能正确排序
- 后面的分割不影响前面的结果
常见问题
-
为什么max==i就是分割点?
- 因为这意味着
[0..i]
的所有数字都在前i+1
个位置 - 排序后一定能得到
[0,1,...,i]
- 因为这意味着
-
如何处理重复元素?
- 本题保证是排列,无重复
-
能否用动态规划?
- 可以,但过于复杂
-
最大可能的块数?
- 最多
n
个(已排序时)
- 最多
-
如何返回具体分割方案?
- 记录分割点的位置