1. 问题背景与题目解析
这道题目来自力扣(LeetCode)的HOT100系列,编号T.75,题目名为"颜色分类"。题目要求我们将一个包含红色、白色和蓝色元素的数组进行原地排序,使得相同颜色的元素相邻,且按照红色、白色、蓝色的顺序排列。在本题中,我们使用整数0、1和2分别表示红色、白色和蓝色。
这道题看似简单,但它实际上是荷兰国旗问题的一个经典实例,也是快速排序算法中partition过程的一个特例。理解这个问题的解法对于掌握快速排序的核心思想非常有帮助。
注意:题目要求必须在不使用库的sort函数的情况下完成原地排序,这意味着我们需要手动实现排序逻辑。
2. 快速排序与三指针法
2.1 快速排序基础
快速排序的核心思想是分治法,通过选择一个"基准"元素将数组分成两部分,一部分小于基准,一部分大于基准,然后递归地对这两部分进行排序。在标准的快速排序中,partition过程通常使用双指针法:
- 选择一个基准元素(通常是最后一个元素)
- 初始化一个指针i表示小于基准的区域的边界
- 遍历数组,将小于基准的元素交换到i的位置,并递增i
- 最后将基准元素放到正确的位置
2.2 三指针法适应本题
对于颜色分类问题,我们需要将数组分成三个部分(0、1、2),因此标准的双指针partition需要扩展为三指针法:
- left指针:指向当前0元素应该插入的位置
- right指针:指向当前2元素应该插入的位置
- current指针:用于遍历数组
算法流程:
- 初始化left=0, right=n-1, current=0
- 当current <= right时循环: a. 如果nums[current] == 0,交换nums[current]和nums[left],left++, current++ b. 如果nums[current] == 1,current++ c. 如果nums[current] == 2,交换nums[current]和nums[right],right--(注意不增加current)
3. 算法实现与代码解析
3.1 Java实现
class Solution { public void sortColors(int[] nums) { int left = 0, right = nums.length - 1; int current = 0; while (current <= right) { if (nums[current] == 0) { swap(nums, current, left); left++; current++; } else if (nums[current] == 1) { current++; } else { swap(nums, current, right); right--; } } } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }3.2 代码解析
初始化三个指针:
- left始终指向下一个0应该放置的位置
- right始终指向下一个2应该放置的位置
- current用于遍历数组
当current遇到0时:
- 将current位置的0交换到left位置
- 因为left位置之前已经处理过,所以可以安全地递增left和current
当current遇到1时:
- 不做交换,直接跳过(1应该在中间区域)
- 只递增current指针
当current遇到2时:
- 将current位置的2交换到right位置
- 由于从right位置交换过来的元素可能是0或1,所以不能递增current,需要在下一次循环中重新检查
关键点:当交换2时不能增加current指针,因为从right位置交换过来的元素可能还需要处理。
4. 算法复杂度分析
4.1 时间复杂度
该算法只需要一次遍历数组,每个元素最多被访问两次(当交换2时可能被重新检查),因此时间复杂度是O(n),其中n是数组的长度。
4.2 空间复杂度
算法只使用了常数个额外空间(几个指针变量),因此空间复杂度是O(1),满足题目要求的原地排序条件。
5. 边界条件与测试用例
5.1 常见测试用例
基本用例:
- 输入:[2,0,2,1,1,0]
- 输出:[0,0,1,1,2,2]
已排序数组:
- 输入:[0,0,1,1,2,2]
- 输出:[0,0,1,1,2,2]
逆序数组:
- 输入:[2,2,1,1,0,0]
- 输出:[0,0,1,1,2,2]
全0数组:
- 输入:[0,0,0]
- 输出:[0,0,0]
全1数组:
- 输入:[1,1,1]
- 输出:[1,1,1]
全2数组:
- 输入:[2,2,2]
- 输出:[2,2,2]
空数组:
- 输入:[]
- 输出:[]
5.2 特殊边界情况
单元素数组:
- 输入:[1]
- 输出:[1]
只有0和1:
- 输入:[1,0,1,0]
- 输出:[0,0,1,1]
只有1和2:
- 输入:[1,2,2,1]
- 输出:[1,1,2,2]
只有0和2:
- 输入:[2,0,0,2]
- 输出:[0,0,2,2]
6. 常见错误与调试技巧
6.1 常见实现错误
交换2后增加current指针:
- 错误:可能导致未被检查的元素被跳过
- 正确:交换2后不应增加current,因为从right交换过来的元素可能还需要处理
循环条件错误:
- 错误:使用current < nums.length作为循环条件
- 正确:应该是current <= right,因为right之后的元素已经处理过
指针越界:
- 错误:未检查数组为空的情况
- 正确:应首先检查数组长度是否为0
6.2 调试技巧
打印中间状态:
- 在每次交换后打印数组和指针位置,帮助理解算法执行过程
使用小测试用例:
- 从最小可能的输入开始测试(如[2,0,1]),逐步增加复杂度
边界测试:
- 特别注意全0、全1、全2的数组,以及空数组和单元素数组
7. 算法优化与变种
7.1 计数排序法
虽然三指针法是最优解,但也可以使用计数排序的思路:
- 统计0、1、2的个数
- 按照统计结果重写数组
这种方法需要两次遍历,虽然时间复杂度也是O(n),但不如三指针法优雅。
7.2 多颜色分类问题
如果颜色种类不止三种(比如k种),可以考虑:
- 使用k-1次partition将数组分成k部分
- 或者使用计数排序
7.3 快速排序的partition优化
理解本题的三指针法可以帮助优化快速排序的partition过程,特别是当数组中存在大量重复元素时。
8. 实际应用场景
颜色分类问题虽然简单,但其解法在实际中有广泛应用:
- 快速排序优化:处理大量重复元素的数组
- 数据分类:将数据分成几个类别进行处理
- 图像处理:像素值分类
- 系统设计:任务优先级调度
9. 力扣HOT100打卡建议
这道题作为HOT100系列的第14题,是算法学习中的一个重要里程碑。以下是一些打卡建议:
- 理解优先:不要满足于AC,要真正理解三指针法的原理
- 举一反三:思考如何将这种方法应用到其他分区问题上
- 多种实现:尝试用不同语言实现该算法
- 复杂度分析:养成分析时间/空间复杂度的习惯
- 测试驱动:编写自己的测试用例验证算法正确性
我在实际刷题中发现,真正掌握这道题后,对快速排序和各种分区问题的理解会有质的提升。建议在AC后,可以尝试解决类似的partition问题,如移动零、按奇偶排序数组等,巩固这种解题思路。