74.颜色分类

33 阅读4分钟

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地** 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

 

示例 1:

输入: nums = [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]

示例 2:

输入: nums = [2,0,1]
输出: [0,1,2]

 

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i] 为 01 或 2

 

进阶:

  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

class Solution { public void sortColors(int[] nums) { int left = 0; // 左边界:存放 0 的位置 int right = nums.length-1; // 右边界:存放 2 的位置 int cur = 0; // 当前扫描指针

    while (cur <= right) {
        if (nums[cur] == 0) {
            // 把 0 放到左边
            swap(nums, cur, left);
            left++;
            cur++; // 因为交换过来的数在 cur 之前,所以可以放心 cur++
        } 
        else if (nums[cur] == 2) {
            // 把 2 放到右边
            swap(nums, cur, right);
            right--;
            // ⚠️ cur 不++,因为交换过来的数还需要检查
        } 
        else {
            // nums[cur] == 1,保持不动
            cur++;
        }
    }
}

private void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

}

详细讲解三个指针的移动(荷兰国旗问题)

先给出不变式(invariant) ,理解它能让整个过程清清楚楚:在任意时刻,数组被分成 4 个区间(可能有空区间):

  • nums[0 .. left-1] 全是 0
  • nums[left .. cur-1] 全是 1
  • nums[cur .. right]未分类的(还没检查)
  • nums[right+1 .. n-1] 全是 2

三个指针的含义:

  • left:下一个应该放 0 的位置(左区的下一个索引)。
  • right:下一个应该放 2 的位置(右区的下一个索引)。
  • cur:当前正在检查的元素索引(属于未分类区间)。

循环条件:while (cur <= right)——当未分类区间为空时停止。

规则(每次看 nums[cur]):

  1. nums[cur] == 0swap(nums[cur], nums[left]),然后 left++cur++
    原因:left 处要放 0,交换后放到 cur 的一定是 1(或 cur==left 自身),所以可以安全地 cur++
  2. nums[cur] == 1cur++(1 已经到了中间区,不动)。
  3. nums[cur] == 2swap(nums[cur], nums[right]),然后 right--不要 cur++
    原因:从 right 换过来的元素还没分类(可能是 0/1/2),必须在下次循环重新判断 nums[cur]

用例逐步演示(例子:[2,0,2,1,1,0]

初始:nums = [2,0,2,1,1,0],索引 0..5
left = 0, cur = 0, right = 5

我把每步写成: (动作) -> 数组状态 ; left, cur, right ; 说明

  1. cur=0,nums[cur]=2
    swap(0,5) -> [0,0,2,1,1,2] ; left=0, cur=0, right=4
    说明:把 2 放到最右边。cur 不动(要重新检查当前位置的新值)。
  2. cur=0,nums[cur]=0
    swap(0,0) -> [0,0,2,1,1,2] ; left=1, cur=1, right=4
    说明:把 0 放到左边(这里是自交换),左区扩大,cur 前进。
  3. cur=1,nums[cur]=0
    swap(1,1) -> [0,0,2,1,1,2] ; left=2, cur=2, right=4
    说明:把 0 放到左边(自交换),继续前进。
  4. cur=2,nums[cur]=2
    swap(2,4) -> [0,0,1,1,2,2] ; left=2, cur=2, right=3
    说明:把 2 放到右边,换过来的可能是 0/1/2(这里是 1),因此 cur 不动需重新判断。
  5. cur=2,nums[cur]=1
    不交换 -> [0,0,1,1,2,2] ; left=2, cur=3, right=3
    说明:1 属于中间区,cur++。
  6. cur=3,nums[cur]=1
    不交换 -> [0,0,1,1,2,2] ; left=2, cur=4, right=3
    说明:cur++,现在 cur=4 > right=3,循环结束。

结果:[0,0,1,1,2,2](已排序)


为什么 cur 在遇到 2不能自增,而遇到 0 时可以自增?

  • nums[cur]==0 时,你把它放到 left。根据不变式,位置 left 之前或 等于 cur 的区域([left, cur-1])都是已知为 1,所以交换后 nums[cur] 要么变成 1(当 left < cur 时),要么仍然是 0(当 left == cur,自交换)。因此交换之后 nums[cur] 不会是未处理的 02 以外的未知值,可以安全地 cur++ (因为即便是 1,我们已把它放到中间区)。
  • nums[cur]==2 时,你把它放到 right,换过来的元素来自未分类区(right 处之前未必被检查过),这个新元素可能是 012。如果是 0,你需要马上把它移到左边 —— 这就要求你再次处理同一个 cur(不能 advance) 。所以遇到 2 后要 right--保持 cur 不变,在下一次循环里重新判断 nums[cur]

这是算法正确性的关键点。


复杂度

  • 时间复杂度:O(n) —— 每个元素最多被交换/检查常数次。
  • 空间复杂度:O(1) —— 只使用固定数量的指针。