1. 问题背景与题目解析
这道题目来自LeetCode热题100中的第75题"颜色分类",在力扣题库中被标记为中等难度。题目要求我们原地对包含红、白、蓝三种颜色的数组进行排序,使得相同颜色的元素相邻,且按照红、白、蓝的顺序排列。
在实际编码面试中,这类数组原地操作问题非常常见。题目中通常会用0、1、2分别代表红、白、蓝三种颜色。例如给定数组[2,0,2,1,1,0],排序后应该得到[0,0,1,1,2,2]。
注意:题目明确要求必须使用原地算法,这意味着我们不能使用额外的存储空间,只能通过交换元素来修改输入数组。
2. 常见解法分析与比较
2.1 直接排序法(不符合要求)
最直观的想法是直接调用语言内置的排序函数:
def sortColors(nums): nums.sort()这种方法虽然简单,但有两个问题:
- 时间复杂度为O(nlogn),不是最优解
- 没有体现出算法设计能力,面试中不会被接受
2.2 计数排序法
另一种思路是统计0、1、2出现的次数,然后重新填充数组:
def sortColors(nums): count = [0, 0, 0] for num in nums: count[num] += 1 i = 0 for num in range(3): for _ in range(count[num]): nums[i] = num i += 1这种方法:
- 时间复杂度:O(n),需要遍历数组两次
- 空间复杂度:O(1),只使用了固定大小的计数数组
- 缺点:不是原地排序,虽然符合题目空间要求但修改方式不够优雅
3. 最优解:三指针法(Dutch National Flag问题)
3.1 算法原理
这个问题实际上是经典的荷兰国旗问题(Dutch National Flag Problem),由Edsger Dijkstra提出。最优解法使用三个指针:
- left指针:指向当前已排序的0的右边界
- right指针:指向当前已排序的2的左边界
- curr指针:当前正在检查的元素
算法过程:
- 当nums[curr] == 0时,与nums[left]交换,left和curr都右移
- 当nums[curr] == 2时,与nums[right]交换,right左移
- 当nums[curr] == 1时,curr右移
3.2 完整实现代码
def sortColors(nums): left, curr, right = 0, 0, len(nums) - 1 while curr <= right: if nums[curr] == 0: nums[left], nums[curr] = nums[curr], nums[left] left += 1 curr += 1 elif nums[curr] == 2: nums[curr], nums[right] = nums[right], nums[curr] right -= 1 else: curr += 13.3 算法分析
- 时间复杂度:O(n),只需一次遍历
- 空间复杂度:O(1),只使用了常数个额外空间
- 稳定性:非稳定排序,因为交换会改变相同元素的相对位置
4. 边界条件与测试用例
4.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] # 空数组 [] → [] # 单元素 [1] → [1]4.2 特殊边界情况
在实际编码中需要特别注意:
- 数组长度为0或1的情况
- 数组中只有两种颜色的情况
- 指针移动时不要越界
- 交换后curr指针的处理(特别是与right交换时curr不能立即增加)
5. 算法优化与变种
5.1 循环不变式验证
为了确保算法正确性,我们可以定义循环不变式:
- [0, left)区间全是0
- [left, curr)区间全是1
- (right, end]区间全是2
- [curr, right]区间是待处理的未知元素
5.2 变种问题
- 四色排序问题(增加一种颜色)
- 将奇数放在偶数前面
- 将0和1分开(只有两种值的情况)
6. 实际应用场景
这类分区算法在实际中有广泛应用:
- 快速排序中的分区操作
- 数据库查询优化中的范围分区
- 图形渲染中的颜色分类
- 数据分析中的数据分桶处理
7. 常见错误与调试技巧
7.1 常见错误
- 与right交换后curr也增加,导致跳过元素检查
- 循环条件写成curr < right,漏掉最后一个元素
- 指针初始化错误,如curr初始化为1
- 交换时使用不正确的索引
7.2 调试建议
- 打印每次交换后的数组状态
- 跟踪三个指针的位置变化
- 使用小规模测试用例手动模拟
- 特别注意边界条件的测试
8. 不同语言实现对比
8.1 Java实现
public void sortColors(int[] nums) { int left = 0, curr = 0; int right = nums.length - 1; while (curr <= right) { if (nums[curr] == 0) { swap(nums, left++, curr++); } else if (nums[curr] == 2) { swap(nums, curr, right--); } else { curr++; } } } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }8.2 C++实现
void sortColors(vector<int>& nums) { int left = 0, curr = 0; int right = nums.size() - 1; while (curr <= right) { if (nums[curr] == 0) { swap(nums[left++], nums[curr++]); } else if (nums[curr] == 2) { swap(nums[curr], nums[right--]); } else { curr++; } } }8.3 JavaScript实现
function sortColors(nums) { let left = 0, curr = 0; let right = nums.length - 1; while (curr <= right) { if (nums[curr] === 0) { [nums[left], nums[curr]] = [nums[curr], nums[left]]; left++; curr++; } else if (nums[curr] === 2) { [nums[curr], nums[right]] = [nums[right], nums[curr]]; right--; } else { curr++; } } }9. 性能优化与进阶思考
9.1 减少交换次数
在某些情况下可以优化交换次数:
- 当left == curr时,不需要交换
- 当curr == right时,可以提前终止
9.2 并行化处理
对于大规模数据,可以考虑:
- 将数组分成多个区间并行处理
- 最后合并各区间结果
9.3 内存访问优化
考虑CPU缓存行优化:
- 顺序访问比随机访问更快
- 减少不必要的内存写入
10. 面试技巧与解题思路
在面试中遇到这类问题时:
- 先明确问题要求和约束条件
- 提出暴力解法并分析复杂度
- 逐步优化,解释每一步的思考过程
- 讨论边界条件和测试用例
- 最后给出最优解并分析复杂度
对于颜色分类问题,可以这样思考:
- 首先想到可能需要分区处理
- 考虑需要几个指针来标记不同区域的边界
- 确定指针移动的条件和顺序
- 验证算法在各种情况下的正确性