题目描述
原地把只含 0、1、2 的数组排成 0 在前、1 在中、2 在后。
思路解析
最直接是「计数排序」:先扫一遍数出 0、1、2 各几个,再扫一遍按数量重新填回去。能对,但要扫两趟。题目只有三种值,其实一趟就能就地分好——这正是三指针的用武之地。
关键招:l 左边全是已就位的 0,r 右边全是已就位的 2,i 是当前检查位。i 遇 0 就和 l 换、l 和 i 都前进;遇 2 就和 r 换、r 后退但 i 不动(换来的数还没看过);遇 1 就 i 直接前进。i 越过 r 就排完——一趟、原地。
三指针就位:三个指针就位:l 和 i 都从最左边开始,r 在最右边。i 负责逐个检查,l 和 r 负责把 0 和 2 归位。
i=0 看到 2 → 换到右边:负例分支:当前是 2,和 r(下标 5)交换,r 退到 4。注意 i 不前进——换过来的是个 0,还没检查过,下一步还得在原地重判它。
i=0 重判 · 看到 0 → 换到左边:重判 i=0:换过来的正是 0,和 l(下标 0)交换(其实是自己换自己),l 和 i 都前进到 1。下标 0 已锁定为 0(变灰)。
i=1 看到 0 → 换到左边:又是 0,和 l(下标 1)交换,l 和 i 都前进到 2。左边已是 [0, 0] 两个就位的红色。
i=2 看到 2 → 换到右边:当前是 2,和 r(下标 4)交换,r 退到 3。i 仍不动,换来的 1 还得再判。
i=2 重判 · 看到 1 → 前进:重判 i=2:换过来是 1,本来就该在中间,不交换,i 直接前进到 3。
i=3 看到 1 → 前进越过 r:又是 1,不交换,i 前进到 4。此时 i 已越过 r(4 大于 3),r 右边的 [2, 2] 早已就位,扫描结束。
完成:一趟扫描完成排序:l 左边全 0、r 右边全 2、中间全 1,得到 [0, 0, 1, 1, 2, 2]。
l 守左、r 守右各管一种值,中间 [l, i) 是已确定的 1、[i, r] 是待处理区——这就是分区思想,快排的 partition 也是同一招。
参考代码(Python)
1l, r, i = 0, len(nums) - 1, 0
2while i <= r: # i 越过 r 就停
3 if nums[i] == 0: # 该放左段
4 nums[l], nums[i] = nums[i], nums[l]; l += 1; i += 1
5 elif nums[i] == 2: # 该放右段
6 nums[r], nums[i] = nums[i], nums[r]; r -= 1 # i 不动!
7 else: # 是 1,留中间
8 i += 1复杂度分析
- 时间复杂度:O(n) —— i 和 r 相向而行,最多扫一趟 n 个元素
- 空间复杂度:O(1) —— 只用 l、i、r 三个下标,原地交换、不开计数数组
套路模板
记住骨架:l 守左、r 守右、i 扫描;换到右边后 i 不动。改一下条件就能复用:快排 partition、把某元素全移到末尾都是这套分区。
1# 例: 0 归左、2 归右、1 留中(荷兰国旗)
2l, i, r = 0, 0, n - 1
3while i <= r:
4 if nums[i] == 0: # 该放左
5 nums[i], nums[l] = nums[l], nums[i]; l += 1; i += 1
6 elif nums[i] == 2: # 该放右
7 nums[i], nums[r] = nums[r], nums[i]; r -= 1 # i 不动!
8 else:
9 i += 1 # 中间值,直接走易错点
- 错误写法:遇到 2 和 r 交换后 i 也一起前进 → 正确写法:遇到 2 交换后只 r 后退,i 留在原地重判(从 r 换来的数还没检查过,可能是 0(本例 i=0 换来的就是 0),i 一前进就漏判,那个 0 永远到不了左段)
- 错误写法:while i < len(nums) → 正确写法:while i <= r(r 右边已全是就位的 2,i 越过 r 就该停,再扫只会把排好的 2 又换乱)
- 错误写法:遇到 1 也做一次交换 → 正确写法:遇到 1 只 i += 1,不交换(1 本就该在中段,多余交换不出错但白费动作,逻辑上也容易写乱 l/r)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。