给定一个包含红色、白色和蓝色、共 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.length1 <= n <= 300nums[i]为0、1或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]全是0nums[left .. cur-1]全是1nums[cur .. right]是未分类的(还没检查)nums[right+1 .. n-1]全是2
三个指针的含义:
left:下一个应该放0的位置(左区的下一个索引)。right:下一个应该放2的位置(右区的下一个索引)。cur:当前正在检查的元素索引(属于未分类区间)。
循环条件:while (cur <= right)——当未分类区间为空时停止。
规则(每次看 nums[cur]):
nums[cur] == 0:swap(nums[cur], nums[left]),然后left++、cur++。
原因:left处要放 0,交换后放到cur的一定是 1(或cur==left自身),所以可以安全地cur++。nums[cur] == 1:cur++(1 已经到了中间区,不动)。nums[cur] == 2:swap(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 ; 说明
- cur=0,
nums[cur]=2
swap(0,5) ->[0,0,2,1,1,2]; left=0, cur=0, right=4
说明:把 2 放到最右边。cur 不动(要重新检查当前位置的新值)。 - cur=0,
nums[cur]=0
swap(0,0) ->[0,0,2,1,1,2]; left=1, cur=1, right=4
说明:把 0 放到左边(这里是自交换),左区扩大,cur 前进。 - cur=1,
nums[cur]=0
swap(1,1) ->[0,0,2,1,1,2]; left=2, cur=2, right=4
说明:把 0 放到左边(自交换),继续前进。 - 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 不动需重新判断。 - cur=2,
nums[cur]=1
不交换 ->[0,0,1,1,2,2]; left=2, cur=3, right=3
说明:1 属于中间区,cur++。 - 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]不会是未处理的0或2以外的未知值,可以安全地cur++(因为即便是 1,我们已把它放到中间区)。 - 当
nums[cur]==2时,你把它放到right,换过来的元素来自未分类区(right处之前未必被检查过),这个新元素可能是0、1或2。如果是0,你需要马上把它移到左边 —— 这就要求你再次处理同一个cur(不能 advance) 。所以遇到2后要right--但保持cur不变,在下一次循环里重新判断nums[cur]。
这是算法正确性的关键点。
复杂度
- 时间复杂度:
O(n)—— 每个元素最多被交换/检查常数次。 - 空间复杂度:
O(1)—— 只使用固定数量的指针。