题目描述

✅ 75. 颜色分类

image-20260928203243145

image-20260928203243146

题意分析

数组只包含 0、1、2 三种值,要求原地排列成所有 0 在前、1 居中、2 在后的顺序。不能调用库排序,进阶要求只扫描一遍并使用常数额外空间。

由于取值只有三种,不需要比较任意两个数来确定大小关系。可以直接把 0 送到左端、把 2 送到右端,让 1 留在中间;关键是区分哪些位置已经分类,哪些位置仍需检查,避免交换后漏掉未知元素。

解法:三指针原地分区

核心思路

[!blue]

用 zero 指向下一个应放 0 的位置,用 two 指向下一个应放 2 的位置,用 i 检查当前未知元素。始终维护四段:[0, zero) 全是 0,[zero, i) 全是 1,[i, two] 尚未分类,(two, n) 全是 2。初始三个已分类区都为空,整个数组属于未知区。

遇到 0,把它与 nums[zero] 交换,再同时增加 zero 和 i。如果 zero < i,换回来的值来自已经确定的 1 区,不需要再检查;如果 zero == i,只是原地交换。两种情况下,新增的 0 都被纳入左区,未知区从左侧缩小一格。

遇到 1,它正好可以成为中间区域的新元素,只增加 i 即可。遇到 2,则与 nums[two] 交换并减小 two,把这个 2 纳入右区。但右端原本也是未知区,换回来的值可能属于任意一类,所以 i 不能前进,下一轮必须继续检查它。

每轮不是推进 i,就是缩小 two,未知区都会减少一个位置。即使 i == two,仍有一个未知元素需要处理,因此循环条件是 i <= two。两者交错时未知区为空,剩下的三段自然按 0、1、2 排好,交换也始终保留了全部元素。

解题步骤

  1. 初始化 zero = 0、i = 0、two = n - 1。
  2. 当 i <= two 时,检查 nums[i]。
  3. 若为 0,与 nums[zero] 交换,然后同时推进 zero、i。
  4. 若为 2,与 nums[two] 交换,只让 two 减一,保留 i 继续检查换入值。
  5. 否则当前值为 1,只让 i 加一;未知区为空后结束。

代码实现

class Solution {
    public void sortColors(int[] nums) {
        int zero = 0;
        int i = 0;
        int two = nums.length - 1;

        while (i <= two) {
            if (nums[i] == 0) {
                swap(nums, zero, i);
                zero++;
                i++;
            } else if (nums[i] == 2) {
                // 从右侧换来的元素还没检查,i 不能前进。
                swap(nums, i, two);
                two--;
            } else {
                i++;
            }
        }
    }

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

        nums[i] = nums[j];
        nums[j] = value;
    }
}
func sortColors(nums []int) {
    zero := 0
    i := 0
    two := len(nums) - 1

    for i <= two {
        if nums[i] == 0 {
            nums[zero], nums[i] = nums[i], nums[zero]
            zero++
            i++
        } else if nums[i] == 2 {
            // 右侧交换回来的值属于未知区,需要继续检查。
            nums[i], nums[two] = nums[two], nums[i]
            two--
        } else {
            i++
        }
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,未知区初始有 n 个位置,每轮恰好缩小一个位置,交换与判断都是常数操作。
  • 空间复杂度:$O(1)$,只使用三个指针和交换临时变量。

关键点总结

[!green]

  • 先写出四段区间的不变量,再推导指针移动,比背代码可靠。
  • 从已处理区换回的元素可以跳过;从未知区换回的元素必须重新检查。
  • 未知区是闭区间 [i, two],所以循环条件必须是 i <= two。
  • 该方法就是荷兰国旗算法,也是快速排序三路划分的核心过程。

易错点总结

[!yellow]

  • 处理 2 后推进 i:会跳过从未知右端换回的元素,它可能仍需移到左侧或右侧。
  • 循环写成 i < two:两者相等时还有一个未知元素,不能提前结束。
  • two 初始化为数组长度:它表示可写入 2 的实际下标,应初始化为 n - 1。
  • 处理 0 时只移动一个指针:这个 0 已经归位,换回的值也已知,因此 zero 和 i 都要前进,才能维持四段范围。

相似题目

题目 难度 关联与区别
324. 摆动排序 II 中等 同样可以利用三路划分隔开小于、等于、大于枢轴的元素,原题还需虚拟下标控制摆动位置。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/76545281
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!