75. 颜色分类(Sort Colors)

频次 ★★★★ · 难度 🟡 · 高频:字节/腾讯

题目

数组只含 0、1、2(红白蓝),原地一趟排序,不许用库函数。

示例

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

思路

荷兰国旗问题,三指针分区:

  • p0:0 区右边界(下一个 0 该放的位置)
  • p2:2 区左边界(下一个 2 该放的位置)
  • i:当前扫描位置

nums[i] 为 0 → 与 p0 交换,p0++、i++;为 2 → 与 p2 交换,p2—,i 不动(换来的元素还没检查);为 1 → i++。当 i > p2 时三区闭合。

代码

public void sortColors(int[] nums) {
    int p0 = 0, i = 0, p2 = nums.length - 1;
    while (i <= p2) {
        if (nums[i] == 0) swap(nums, i++, p0++);      // 换来的必是 1(i 走过的区域),可前进
        else if (nums[i] == 2) swap(nums, i, p2--);   // 换来的未检查,i 停住
        else i++;
    }
}

复杂度

  • 时间:O(n) 一趟
  • 空间:O(1)

边界条件

  • 全同色:与自身交换或直接扫过,正常结束
  • 长度 1:循环一次即出
  • 已有序:0 与 p0 自交换,i 与 p0 同步走

变式

  • 计数排序两趟版:数 0/1/2 个数再回填——两趟且需改写值,一趟三指针是本题考点
  • 推广到 k 种颜色:三指针不再够用,退回计数排序 O(n+k)

易错点

  • 与 p2 交换后 i 不能前进——换回来的可能是 0 或 2,没检查过;与 p0 交换后 i 可以前进,因为 p0 ≤ i,p0 位置的值是 i 已经扫过的(只能是 1)
  • 循环条件 i <= p2 不是 i < n——p2 右边已是成品区,多扫会把 2 再换回来
  • 三个分支必须互斥完整,别把 else if 写成两个 if(交换后值变了会连做两个分支)

面试追问

  • 为什么与 p0 换完 i 能走、与 p2 换完不能? 这就是本题唯一的坑:p0 一侧换来的元素来自”i 已扫过区”,必为 1;p2 一侧来自”未扫描区”。答清楚这条不变量,这题就过了
  • 和快排的关系? 三指针分区就是快排的三路划分(partition3),处理大量重复键时避免退化,见数据结构与算法排序部分

关联题

  • 同套路:283. 移动零27. 移除元素 —— 二分区版本,本题升级为三分区
  • 进阶:三路快排 partition(215/912 中处理重复元素的优化)
  • 知识点:循环不变量的定义与维护——分区类题目答题时先讲清每个指针的语义