算法题 最多能完成排序的块

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]

算法思路

贪心算法 + 最大值检查

  1. 遍历数组,维护当前遍历到的最大值
  2. 当最大值等于当前索引时,说明可以在这里分割
  3. 因为如果 max(arr[0..i]) == i,则子数组 arr[0..i] 包含了 [0..i] 的所有数字
  4. 这样排序后一定能得到正确的顺序

核心思想:一个块能独立排序的充要条件是:该块包含的数字恰好是 [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]

  1. i=0: maxSoFar=max(0,1)=1, 1≠0,不分割
  2. i=1: maxSoFar=max(1,0)=1, 1==1chunks=1
  3. i=2: maxSoFar=max(1,2)=2, 2==2chunks=2
  4. i=3: maxSoFar=max(2,3)=3, 3==3chunks=3
  5. i=4: maxSoFar=max(3,4)=4, 4==4chunks=4
  6. 返回: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
}

关键点

  1. 关键

    • 块能独立排序的充要条件是:包含 [0..k] 的所有数字
    • 等价于 max(arr[0..i]) == i
  2. 贪心策略

    • 只要满足条件就分割
    • 这样能获得最多的块数
  3. 边界处理

    • 空数组
    • 单元素
    • 已排序数组
  4. 算法正确性

    • 每个分割点都保证了前面的数字能正确排序
    • 后面的分割不影响前面的结果

常见问题

  1. 为什么max==i就是分割点?

    • 因为这意味着 [0..i] 的所有数字都在前 i+1 个位置
    • 排序后一定能得到 [0,1,...,i]
  2. 如何处理重复元素?

    • 本题保证是排列,无重复
  3. 能否用动态规划?

    • 可以,但过于复杂
  4. 最大可能的块数?

    • 最多 n 个(已排序时)
  5. 如何返回具体分割方案?

    • 记录分割点的位置
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值