☰
LeetCode颜色分类问题:三指针法最优解
2026/11/3 6:45:13 网站建设 项目流程

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()

这种方法虽然简单,但有两个问题:

  1. 时间复杂度为O(nlogn),不是最优解
  2. 没有体现出算法设计能力,面试中不会被接受

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提出。最优解法使用三个指针:

  1. left指针:指向当前已排序的0的右边界
  2. right指针:指向当前已排序的2的左边界
  3. 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 += 1

3.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 特殊边界情况

在实际编码中需要特别注意:

  1. 数组长度为0或1的情况
  2. 数组中只有两种颜色的情况
  3. 指针移动时不要越界
  4. 交换后curr指针的处理(特别是与right交换时curr不能立即增加)

5. 算法优化与变种

5.1 循环不变式验证

为了确保算法正确性,我们可以定义循环不变式:

  1. [0, left)区间全是0
  2. [left, curr)区间全是1
  3. (right, end]区间全是2
  4. [curr, right]区间是待处理的未知元素

5.2 变种问题

  1. 四色排序问题(增加一种颜色)
  2. 将奇数放在偶数前面
  3. 将0和1分开(只有两种值的情况)

6. 实际应用场景

这类分区算法在实际中有广泛应用:

  1. 快速排序中的分区操作
  2. 数据库查询优化中的范围分区
  3. 图形渲染中的颜色分类
  4. 数据分析中的数据分桶处理

7. 常见错误与调试技巧

7.1 常见错误

  1. 与right交换后curr也增加,导致跳过元素检查
  2. 循环条件写成curr < right,漏掉最后一个元素
  3. 指针初始化错误,如curr初始化为1
  4. 交换时使用不正确的索引

7.2 调试建议

  1. 打印每次交换后的数组状态
  2. 跟踪三个指针的位置变化
  3. 使用小规模测试用例手动模拟
  4. 特别注意边界条件的测试

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 减少交换次数

在某些情况下可以优化交换次数:

  1. 当left == curr时,不需要交换
  2. 当curr == right时,可以提前终止

9.2 并行化处理

对于大规模数据,可以考虑:

  1. 将数组分成多个区间并行处理
  2. 最后合并各区间结果

9.3 内存访问优化

考虑CPU缓存行优化:

  1. 顺序访问比随机访问更快
  2. 减少不必要的内存写入

10. 面试技巧与解题思路

在面试中遇到这类问题时:

  1. 先明确问题要求和约束条件
  2. 提出暴力解法并分析复杂度
  3. 逐步优化,解释每一步的思考过程
  4. 讨论边界条件和测试用例
  5. 最后给出最优解并分析复杂度

对于颜色分类问题,可以这样思考:

  1. 首先想到可能需要分区处理
  2. 考虑需要几个指针来标记不同区域的边界
  3. 确定指针移动的条件和顺序
  4. 验证算法在各种情况下的正确性

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询