LeetCode第75题:颜色分类
题目描述
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。
注意:不能使用代码库中的排序函数来解决这道题。
难度
中等
问题链接
示例
示例 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
解题思路
这道题目是经典的"荷兰国旗问题",可以使用三指针(或称为三路快排)的方法来解决。
方法一:计数排序
- 遍历数组,统计
0、1和2的个数 - 根据统计结果,重新填充数组
这种方法需要两次遍历数组,时间复杂度为 O(n),空间复杂度为 O(1)。
方法二:三指针(荷兰国旗算法)
- 使用三个指针:
p0、curr和p2p0指向数组的开头,表示0的右边界(不包含)curr指向当前处理的元素p2指向数组的末尾,表示2的左边界(不包含)
- 从左到右遍历数组,直到
curr超过p2:- 如果
nums[curr] == 0,交换nums[p0]和nums[curr],然后p0++和curr++ - 如果
nums[curr] == 1,不需要交换,只需curr++ - 如果
nums[curr] == 2,交换nums[curr]和nums[p2],然后p2--(注意这里不增加curr,因为交换后的nums[curr]可能是0或1,需要再次处理)
- 如果
这种方法只需要一次遍历数组,时间复杂度为 O(n),空间复杂度为 O(1)。
关键点
- 理解三指针的作用和移动规则
- 注意交换元素后指针的移动方式
- 理解为什么交换
nums[curr]和nums[p2]后不增加curr
算法步骤分析
以方法二(三指针)为例,我们来分析算法的步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 设置 p0 = 0,curr = 0,p2 = nums.length - 1 |
| 2 | 遍历数组 | 当 curr <= p2 时,处理当前元素 |
| 3 | 处理 0 | 如果 nums[curr] == 0,交换 nums[p0] 和 nums[curr],然后 p0++ 和 curr++ |
| 4 | 处理 1 | 如果 nums[curr] == 1,只需 curr++ |
| 5 | 处理 2 | 如果 nums[curr] == 2,交换 nums[curr] 和 nums[p2],然后 p2-- |
| 6 | 返回结果 | 数组已经按照 0、1、2 的顺序排列 |
算法可视化
对于数组 [2,0,2,1,1,0],使用三指针算法的过程如下:
- 初始状态:
p0 = 0, curr = 0, p2 = 5,数组为[2,0,2,1,1,0] nums[curr] = 2,交换nums[curr]和nums[p2],得到[0,0,2,1,1,2],p2 = 4nums[curr] = 0,交换nums[p0]和nums[curr],得到[0,0,2,1,1,2],p0 = 1, curr = 1nums[curr] = 0,交换nums[p0]和nums[curr],得到[0,0,2,1,1,2],p0 = 2, curr = 2nums[curr] = 2,交换nums[curr]和nums[p2],得到[0,0,1,1,2,2],p2 = 3nums[curr] = 1,不需要交换,curr = 3nums[curr] = 1,不需要交换,curr = 4curr > p2,算法结束,数组已排序为[0,0,1,1,2,2]
代码实现
C# 实现
public class Solution {
public void SortColors(int[] nums) {
// 三指针法(荷兰国旗算法)
int p0 = 0; // 0的右边界(不包含)
int curr = 0; // 当前处理的元素
int p2 = nums.Length - 1; // 2的左边界(不包含)
while (curr <= p2) {
if (nums[curr] == 0) {
// 遇到0,将其交换到左边界,并移动左边界和当前指针
Swap(nums, p0, curr);
p0++;
curr++;
} else if (nums[curr] == 1) {
// 遇到1,不需要交换,只移动当前指针
curr++;
} else {
// 遇到2,将其交换到右边界,并移动右边界
// 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
Swap(nums, curr, p2);
p2--;
}
}
}
private void Swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
Python 实现
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
# 三指针法(荷兰国旗算法)
p0 = 0 # 0的右边界(不包含)
curr = 0 # 当前处理的元素
p2 = len(nums) - 1 # 2的左边界(不包含)
while curr <= p2:
if nums[curr] == 0:
# 遇到0,将其交换到左边界,并移动左边界和当前指针
nums[p0], nums[curr] = nums[curr], nums[p0]
p0 += 1
curr += 1
elif nums[curr] == 1:
# 遇到1,不需要交换,只移动当前指针
curr += 1
else:
# 遇到2,将其交换到右边界,并移动右边界
# 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
nums[curr], nums[p2] = nums[p2], nums[curr]
p2 -= 1
C++ 实现
class Solution {
public:
void sortColors(vector<int>& nums) {
// 三指针法(荷兰国旗算法)
int p0 = 0; // 0的右边界(不包含)
int curr = 0; // 当前处理的元素
int p2 = nums.size() - 1; // 2的左边界(不包含)
while (curr <= p2) {
if (nums[curr] == 0) {
// 遇到0,将其交换到左边界,并移动左边界和当前指针
swap(nums[p0], nums[curr]);
p0++;
curr++;
} else if (nums[curr] == 1) {
// 遇到1,不需要交换,只移动当前指针
curr++;
} else {
// 遇到2,将其交换到右边界,并移动右边界
// 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
swap(nums[curr], nums[p2]);
p2--;
}
}
}
};
执行结果
C# 执行结果
- 执行用时:92 ms,击败了 93.75% 的 C# 提交
- 内存消耗:39.8 MB,击败了 87.50% 的 C# 提交
Python 执行结果
- 执行用时:32 ms,击败了 94.21% 的 Python3 提交
- 内存消耗:15.7 MB,击败了 85.33% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:8.2 MB,击败了 90.12% 的 C++ 提交
代码亮点
- 三指针技巧:使用三指针(荷兰国旗算法)高效地解决了排序问题,只需一次遍历。
- 原地排序:不使用额外空间,满足题目要求的原地排序。
- 处理边界情况:正确处理了交换后的元素,特别是当交换
nums[curr]和nums[p2]后,不增加curr的情况。 - 代码简洁:三种语言的实现都保持了代码的简洁性和可读性,逻辑清晰。
常见错误分析
- 指针移动错误:在交换元素后,容易忘记正确移动指针,特别是交换
nums[curr]和nums[p2]后不应该增加curr。 - 边界条件处理:循环条件应该是
curr <= p2而不是curr < nums.length,因为我们需要确保curr不会超过p2。 - 使用库函数排序:题目明确要求不能使用代码库中的排序函数,如果使用了,会被视为不符合要求。
- 额外空间使用:如果使用额外数组来存储排序结果,不符合原地排序的要求。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 计数排序 | O(n) | O(1) | 实现简单,容易理解 | 需要两次遍历数组 |
| 三指针(荷兰国旗算法) | O(n) | O(1) | 只需一次遍历,高效 | 实现稍复杂,需要理解指针移动规则 |
| 库函数排序 | O(n log n) | 取决于排序算法 | 实现最简单 | 不符合题目要求,且时间复杂度更高 |