颜色分类 图解题解
三种颜色一趟排好,三个指针各司其职,不用计数也不用两遍扫。
像分拣传送带上的红中蓝三色球:l 左边专门堆红球(0),r 右边专门堆蓝球(2),i 是当前检查位。i 遇蓝就和 r 换、r 往左收——换来的球还没看过,i 原地重判;遇红就和 l 换、l 和 i 一起往右走;遇中就 i 直接前进。i 追上 r,三色全就位,一趟完事。
这道题到底在问什么
- 输入
- nums = [2,0,2,1,1,0]
- 输出
- [0,0,1,1,2,2]
最优解:为什么这么做
一句话答案:LeetCode 75 颜色分类的最优解是三指针一趟扫描,即经典的荷兰国旗问题:l 守 0 区右边界、r 守 2 区左边界、cur 居中考察,遇 0 与 l 交换后两者都前进,遇 2 与 r 交换后只收缩 r 而 cur 原地重判,遇 1 直接右移。时间 O(n)、空间 O(1),一趟完成原地排序。
这道题真正在问什么
数组只含 0、1、2 三种值,要求原地按 0、1、2 的顺序排好,不许调库排序,并且最好一趟扫描完成。值域只有三个,套通用排序是杀鸡用牛刀——真正的考点是利用「只有三类」这个信息,把排序退化成「分区」:把每个数直接归进它所属的区域。这就是经典的荷兰国旗问题。
计数排序为什么还不够好
最直观的做法是计数排序:第一趟数出 0、1、2 各有几个,第二趟按个数把数组重新填一遍。它同样是 O(n) 时间、O(1) 空间,逻辑最简单,但要扫两趟。当题目或面试官追问「能不能只扫一趟」时,就需要边扫边把每个数当场放进正确的区域——这正是三指针的用武之地。
三个指针各自守住什么不变量
l 指向 0 区右侧的第一个空位,r 指向 2 区左侧的第一个空位,cur 是当前考察位置。全程保持四段结构:l 左边全是 0,l 到 cur 之间全是 1,cur 到 r 是尚未考察的未知区,r 右边全是 2。cur 每看一个数就把它归位:是 0 就和 l 交换(0 区长一格),是 2 就和 r 交换(2 区长一格),是 1 就正好留在中段、直接右移。当 cur 越过 r,未知区清空,三段区域收拢相接,数组自然有序——正确性全靠这组不变量每一步都不被破坏。
为什么遇 2 交换后 cur 不能前进
这是全题最关键的不对称。和 r 交换时,换过来的数来自右侧的未知区,可能是 0、1、2 中任何一个、还没被考察过,所以 cur 必须留在原地对新来的值再判一次,否则一个 0 可能被漏在中间。而和 l 交换时不同:l 始终不超过 cur,l 位置上的数属于已处理区域,只能是 1(或者 l 与 cur 重合时的自换),换过来不需要重判,cur 可以放心前进。可以这样记:从左边换来的是旧相识,从右边换来的是陌生人,陌生人必须重新过一遍安检。
复杂度与最容易踩的两个坑
cur 与 r 相向而行,合计把数组扫一遍,时间 O(n);只用三个下标变量,原地交换,空间 O(1)。两个高频坑:一是循环条件必须写 cur <= r 而不是 cur < n——r 右边已经是排好的 2 区,越过 r 继续处理会把排好的 2 再换乱;二是遇 0 交换后忘了让 cur 前进——l 与 cur 重合时交换前后值不变,cur 不前进就会永远停在原地死循环。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住这句「0 扔左边(和 l 换)、2 扔右边(和 r 换)、1 留中间;遇 2 换完 cur 别急着走」,下面每帧都在套它。
- 4cur 处看到的是 2。2 该待在最右边,所以准备把它和 r(下标 13,2 区左侧第一个空位)处的值交换。这一帧先点亮 cur 与 r,还没动手。
- 5交换完成:2 落进下标 13,右边的「2 区」又长一格,r 左移收缩。注意 cur 没动——从 r 换过来的值还没看过,下一帧得在原地再判一次它是 0/1/2。这是本题唯一反直觉的点。
- 6cur 处看到的是 2。2 该待在最右边,所以准备把它和 r(下标 12,2 区左侧第一个空位)处的值交换。这一帧先点亮 cur 与 r,还没动手。
- 7交换完成:2 落进下标 12,右边的「2 区」又长一格,r 左移收缩。注意 cur 没动——从 r 换过来的值还没看过,下一帧得在原地再判一次它是 0/1/2。这是本题唯一反直觉的点。
- 8cur 处看到的是 1。1 本来就该待在中间,什么都不用做,cur 直接右移看下一个。l 和 r 都不动。
- 9cur 处看到的是 0。0 该待在最左边,所以准备把它和 l(下标 0,0 区右侧第一个空位)处的值交换。这一帧先点亮 cur 与 l,还没动手。
- 10交换完成:0 落进下标 0,左边的「0 区」又长了一格。因为从 l 换来的值是之前已看过的(必是 1 或就地),所以 l 和 cur 都往右前进一格,继续往下扫。
- 11cur 处看到的是 2。2 该待在最右边,所以准备把它和 r(下标 11,2 区左侧第一个空位)处的值交换。这一帧先点亮 cur 与 r,还没动手。
- 12交换完成:2 落进下标 11,右边的「2 区」又长一格,r 左移收缩。注意 cur 没动——从 r 换过来的值还没看过,下一帧得在原地再判一次它是 0/1/2。这是本题唯一反直觉的点。
- 13cur 处看到的是 0。0 该待在最左边,所以准备把它和 l(下标 1,0 区右侧第一个空位)处的值交换。这一帧先点亮 cur 与 l,还没动手。
- 14交换完成:0 落进下标 1,左边的「0 区」又长了一格。因为从 l 换来的值是之前已看过的(必是 1 或就地),所以 l 和 cur 都往右前进一格,继续往下扫。
- 15cur 处看到的是 1。1 本来就该待在中间,什么都不用做,cur 直接右移看下一个。l 和 r 都不动。
- 16cur 处看到的是 1。1 本来就该待在中间,什么都不用做,cur 直接右移看下一个。l 和 r 都不动。
- 17cur 处看到的是 0。0 该待在最左边,所以准备把它和 l(下标 2,0 区右侧第一个空位)处的值交换。这一帧先点亮 cur 与 l,还没动手。
- 18交换完成:0 落进下标 2,左边的「0 区」又长了一格。因为从 l 换来的值是之前已看过的(必是 1 或就地),所以 l 和 cur 都往右前进一格,继续往下扫。
- 19cur 处看到的是 1。1 本来就该待在中间,什么都不用做,cur 直接右移看下一个。l 和 r 都不动。
- 20cur 处看到的是 2。2 该待在最右边,所以准备把它和 r(下标 10,2 区左侧第一个空位)处的值交换。这一帧先点亮 cur 与 r,还没动手。
- 21交换完成:2 落进下标 10,右边的「2 区」又长一格,r 左移收缩。注意 cur 没动——从 r 换过来的值还没看过,下一帧得在原地再判一次它是 0/1/2。这是本题唯一反直觉的点。
- 22cur 处看到的是 2。2 该待在最右边,所以准备把它和 r(下标 9,2 区左侧第一个空位)处的值交换。这一帧先点亮 cur 与 r,还没动手。
- 23交换完成:2 落进下标 9,右边的「2 区」又长一格,r 左移收缩。注意 cur 没动——从 r 换过来的值还没看过,下一帧得在原地再判一次它是 0/1/2。这是本题唯一反直觉的点。
- 24cur 处看到的是 1。1 本来就该待在中间,什么都不用做,cur 直接右移看下一个。l 和 r 都不动。
- 25cur 处看到的是 0。0 该待在最左边,所以准备把它和 l(下标 3,0 区右侧第一个空位)处的值交换。这一帧先点亮 cur 与 l,还没动手。
- 26交换完成:0 落进下标 3,左边的「0 区」又长了一格。因为从 l 换来的值是之前已看过的(必是 1 或就地),所以 l 和 cur 都往右前进一格,继续往下扫。
- 27cur 越过了 r,一趟扫描结束:左边一段全是 0、中间全是 1、右边全是 2,结果 [0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]。三个指针合起来只把数组走了一遍,O(n) 时间、O(1) 额外空间。
⚠️ 容易写错的地方
✗ 错:遇到 2 交换后让 cur 也前进
✓ 对:遇 2 交换后 cur 不动
从 r 换来的值还没考察过,cur 必须留在原地再判一次,否则会漏看
✗ 错:循环条件写 cur < n
✓ 对:cur <= r
r 右边已是排好的 2 区,cur 只需扫到 r;写成 < n 会越过 r 重复处理 2 区
✗ 错:遇 0 交换后 cur 不前进
✓ 对:遇 0 交换后 cur 前进
从 l 换来的值是已看过的(l<=cur,那段已处理),可直接前进,不前进会死循环
完整代码(Python / C++ / Java)
Python
def sortColors(nums):
l, cur, r = 0, 0, len(nums) - 1 # 0区右界 / 当前 / 2区左界
while cur <= r:
if nums[cur] == 0: # 0 扔左边
nums[l], nums[cur] = nums[cur], nums[l]
l += 1; cur += 1
elif nums[cur] == 2: # 2 扔右边
nums[cur], nums[r] = nums[r], nums[cur]
r -= 1 # cur 不动!
else: # 1 留中间
cur += 1C++
void sortColors(vector<int>& nums){
int l = 0, cur = 0, r = nums.size() - 1;
while(cur <= r){
if(nums[cur] == 0){ // 0 扔左边
swap(nums[l], nums[cur]);
l++; cur++;
}else if(nums[cur] == 2){ // 2 扔右边
swap(nums[cur], nums[r]);
r--; // cur 不动!
}else{ // 1 留中间
cur++;
}
}
}Java
public void sortColors(int[] nums) {
int l = 0, cur = 0, r = nums.length - 1;
while (cur <= r) {
if (nums[cur] == 0) { // 0 扔左边
int t = nums[l]; nums[l] = nums[cur]; nums[cur] = t;
l++; cur++;
} else if (nums[cur] == 2) { // 2 扔右边
int t = nums[cur]; nums[cur] = nums[r]; nums[r] = t;
r--; // cur 不动!
} else { // 1 留中间
cur++;
}
}
}复杂度
时间
O(n)
cur 与 r 相向,合计只扫一遍
空间
O(1)
只用三个指针,原地交换,不开新数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 颜色分类 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用三指针,最直观的解法是什么?有什么缺点?+
「计数排序」两趟:第一趟数出 0、1、2 各有多少个,第二趟按个数依次填回数组。逻辑最简单、也是 O(n),但要扫两趟;面试若强调「一趟扫描」就得用三指针。
为什么遇 0 交换后 cur 能直接前进,遇 2 却不能?+
因为 l<=cur 始终成立,l 指向的位置在 cur 左边、属于已经处理过的区域,从 l 换到 cur 的值必然是已看过的 1(或就是同一格),可以放心前进;而 r 在 cur 右边、是没碰过的区域,从 r 换来的值未知,必须再判一次。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 颜色分类 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。