一、题目描述
给定一个包含 0、1、2 的数组 nums,分别表示红色、白色和蓝色。
要求我们将数组原地排序,使得相同颜色的元素相邻,并且按照:
0 -> 1 -> 2
的顺序排列。
注意:题目要求不能使用内置 sort 函数。
示例:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
输入:nums = [2,0,1]
输出:[0,1,2]
二、题目分析
这道题看起来像排序题,但数组里只有三种数字:
0、1、2
所以我们没必要真的用普通排序算法。
比较容易想到两种思路:
- 统计
0、1、2出现的次数,然后重新填充数组。 - 使用双指针,在一次遍历中完成排序。
第一种方法比较简单,但是需要遍历两次数组。
第二种方法更经典,也就是常说的荷兰国旗问题。
三、方法一:计数法
因为数组中只有 0、1、2 三种元素,所以我们可以先统计每个数字出现了多少次。
然后再按照数量,把数组改成:
若干个 0 + 若干个 1 + 若干个 2
Java 代码
class Solution {
public void sortColors(int[] nums) {
int count0 = 0;
int count1 = 0;
int count2 = 0;
for (int num : nums) {
if (num == 0) {
count0++;
} else if (num == 1) {
count1++;
} else {
count2++;
}
}
int index = 0;
while (count0-- > 0) {
nums[index++] = 0;
}
while (count1-- > 0) {
nums[index++] = 1;
}
while (count2-- > 0) {
nums[index++] = 2;
}
}
}
复杂度分析
时间复杂度:O(n)
空间复杂度:O(1)
这个方法很好理解,适合刚开始刷题的时候使用。
不过它不是最优雅的写法,因为它需要先统计,再重新赋值。
四、方法二:双指针法
更推荐掌握的是双指针法。
我们可以维护三个区域:
[0, left - 1] 放 0
[left, i - 1] 放 1
[i, right] 当前还没处理
[right + 1, n - 1] 放 2
定义三个指针:
left 指向下一个应该放 0 的位置
right 指向下一个应该放 2 的位置
i 当前遍历的位置
遍历时分三种情况:
情况一:numsi == 0
说明当前元素应该放到前面。
我们交换 nums[i] 和 nums[left],然后:
left++;
i++;
因为换过来的元素一定已经处理过,或者就是当前位置,所以 i 可以往后走。
情况二:numsi == 1
说明当前元素位置没问题,直接:
i++;
情况三:numsi == 2
说明当前元素应该放到后面。
交换 nums[i] 和 nums[right],然后:
right--;
注意:这里 i 不能马上加一。
因为从 right 换过来的元素还没有处理过,可能是 0、1 或 2,所以需要继续判断当前位置。
五、双指针代码实现
class Solution {
public void sortColors(int[] nums) {
int left = 0;
int right = nums.length - 1;
int i = 0;
while (i <= right) {
if (nums[i] == 0) {
swap(nums, i, left);
left++;
i++;
} else if (nums[i] == 1) {
i++;
} else {
swap(nums, i, right);
right--;
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
六、举个例子理解一下
以数组为例:
nums = [2,0,2,1,1,0]
初始状态:
left = 0
i = 0
right = 5
当前 nums[i] = 2,应该放到右边,所以交换 nums[0] 和 nums[5]:
[0,0,2,1,1,2]
此时:
left = 0
i = 0
right = 4
注意这里 i 不动,因为换过来的 0 还需要继续处理。
当前 nums[i] = 0,应该放到左边,交换 nums[0] 和 nums[0]:
[0,0,2,1,1,2]
然后:
left = 1
i = 1
right = 4
当前 nums[i] = 0,继续放到左边:
[0,0,2,1,1,2]
然后:
left = 2
i = 2
right = 4
当前 nums[i] = 2,交换到右边:
[0,0,1,1,2,2]
然后:
left = 2
i = 2
right = 3
当前 nums[i] = 1,直接跳过:
i = 3
当前 nums[i] = 1,继续跳过:
i = 4
这时 i > right,循环结束,数组排序完成。
最终结果:
[0,0,1,1,2,2]
七、为什么遇到 2 的时候 i 不能加一?
这是这道题最容易出错的地方。
比如:
nums = [1,2,0]
当 i = 1 时,nums[i] = 2,我们把它和右边交换:
[1,0,2]
这时从右边换过来的 0 还没有被处理。
如果我们直接 i++,就会跳过这个 0,导致结果错误。
所以遇到 2 时,只移动 right,不移动 i。
八、复杂度分析
双指针法中,每个元素最多被处理一次,所以:
时间复杂度:
O(n)
空间复杂度:
O(1)
满足题目要求的原地排序。
九、总结
这道题是经典的荷兰国旗问题。
如果只是想快速通过,可以用计数法;
如果想掌握更经典的思路,建议重点理解双指针法。
核心思路就是维护三个区域:
左边放 0
中间放 1
右边放 2
遍历时:
- 遇到
0,交换到左边 - 遇到
1,直接跳过 - 遇到
2,交换到右边,并且当前位置继续判断
这题的关键点是:遇到 2 时,i 不要急着往后走。