面试知识库

75. 颜色分类#

大家好!今天我们要探讨一个既直观又充满挑战的问题——颜色分类(Color Sort)。这个问题虽然表面上看起来简单,但它背后隐藏着重要的算法思想,在数组处理、原地算法和荷兰国旗问题中都有特殊地位。让我们一起深入这个问题的本质,探索多种巧妙解法!

🌟 颜色分类:把混乱变成秩序#

什么是”颜色分类”?想象你有一堆红白蓝三种颜色的球,现在需要将它们按照红、白、蓝的顺序排列整齐。这个过程就像是整理一面混乱的国旗,让它恢复应有的秩序与美观。

问题描述#

题目目标#

给定一个包含红色、白色和蓝色共 n 个元素的数组 nums ,请你原地对它们进行排序,使相同颜色的元素相邻,并按照红、白、蓝的顺序排列。我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。

示例 1#

输入: nums = [2,0,2,1,1,0] 输出: [0,0,1,1,2,2] 说明: 把输入和输出放在一起看,会更容易直接抓住题目要求的变化。

🎯 问题描述:整理三色数组#

给定一个包含红色、白色和蓝色的数组,把它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色的顺序排列。

这里,我们使用整数0、1和2分别表示红色、白色和蓝色。

注意:不能使用标准库的排序函数来解决这个问题,必须仅使用常数空间的一趟扫描算法。

例如:

输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
plaintext

🚶‍♀️ 解法一:计数排序(简单但并非最优)#

最直接的方法是统计数组中0、1、2的个数,然后重新填充数组:

时间复杂度:O(n),需要遍历数组两次
空间复杂度:O(1),只使用了常数级别的额外空间

这个解法符合题目要求的常数空间,但它需要两次遍历。我们能做得更好吗?

🏇 解法二:双指针法(更优但不是最佳)#

我们可以使用两个指针,分别跟踪应该放置0和1的位置:

时间复杂度:O(n),只需遍历数组一次
空间复杂度:O(1),只使用了常数额外空间

这个解法虽然只需要一次遍历,但代码相对复杂,不够直观。

🏆 解法三:荷兰国旗问题(最优解)#

这个问题实际上是由计算机科学家Edsger W. Dijkstra提出的”荷兰国旗问题”的一个变种。荷兰国旗恰好是由红、白、蓝三色组成的,与我们的问题一致。

解法的核心思想是使用三个指针将数组分成四个区域:

  • [0, p0):全部是0(红色)
  • [p0, p1):全部是1(白色)
  • [p1, p2]:待分类区域
  • (p2, n-1]:全部是2(蓝色)

时间复杂度:O(n),只需遍历数组一次
空间复杂度:O(1),只使用了常数额外空间

🎭 荷兰国旗算法的直观理解:拔河游戏#

想象这样一个场景:有三队人(0队、1队、2队)在玩拔河游戏。0队站在左边,2队站在右边,1队站在中间。游戏开始时,大家都混在一起,现在我们要把他们分开。

我们从左向右检查每个人:

  • 如果是0队的,就把他与左边区域的边界交换,然后左边区域向右扩展一步
  • 如果是2队的,就把他与右边区域的边界交换,然后右边区域向左扩展一步
  • 如果是1队的,就保持原位,继续检查下一个人

最终,0队都在最左边,2队都在最右边,1队自然就在中间了。

🎬 图解荷兰国旗算法#

以输入数组 [2,0,2,1,1,0] 为例:

  1. 初始状态:p0 = 0, curr = 0, p2 = 5 [2,0,2,1,1,0] ^ ^ p0 p2 curr

  2. nums[curr] = 2,交换nums[curr]和nums[p2],p2— [0,0,2,1,1,2] ^ ^ p0 p2 curr

  3. nums[curr] = 0,交换nums[curr]和nums[p0],p0++,curr++ [0,0,2,1,1,2] ^ ^ p0 p2 curr

  4. nums[curr] = 2,交换nums[curr]和nums[p2],p2— [0,0,1,1,2,2] ^ ^ p0 p2 curr

  5. nums[curr] = 1,curr++ [0,0,1,1,2,2] ^ ^ p0 p2 curr

  6. nums[curr] = 1,curr++ [0,0,1,1,2,2] ^ ^ p0 p2 curr

最终结果为 [0,0,1,1,2,2],完成排序!

🔍 为什么荷兰国旗算法有效?#

荷兰国旗算法的关键在于维护三个指针,分别表示三个颜色的边界。通过不断调整这些边界,我们可以确保:

  • 所有0都被移到数组左侧
  • 所有2都被移到数组右侧
  • 所有1自然就在中间

这个算法只需要一次遍历,而且空间复杂度是O(1),达到了时间和空间的双重最优。

🌱 扩展思考:如果有更多种颜色?#

如果有k种颜色(而不仅仅是3种),我们可能需要回到计数排序的思路,或者使用快速排序的partition思想进行多次划分。

🌐 实际应用:从排序到分区#

颜色分类问题的思想在现实中有广泛应用:

  • 数据处理:将数据分成若干类别进行处理
  • 图像处理:图像分割和颜色量化
  • 内存管理:垃圾收集算法中的对象分类
  • 数据库索引:数据分区提高查询效率

💯 解题心得:特殊问题的特殊解法#

颜色分类问题展示了一个重要的算法设计思想:针对特定问题的特殊性质设计最优算法。

  • 当我们知道数据只有少量几种取值时,可以设计比通用排序更高效的解法
  • 有时候,问题可以转化为经典问题(如荷兰国旗问题),利用已有的算法思想
  • 指针技巧在数组处理中非常有用,特别是需要原地修改数组时

🎓 面试小贴士#

面试中遇到颜色分类问题,建议按以下步骤回答:

  1. 先提出计数排序的方法,说明这是最直观的两遍扫描法
  2. 然后介绍双指针的方法,表明你在优化第一个解法
  3. 最后讲解荷兰国旗算法,展示你对经典算法问题的理解
  4. 讨论如何将这个思想应用到其他问题,如快速排序中的三路划分

🤔 思考题#

如果数组中有四种颜色(用0,1,2,3表示),如何使用类似荷兰国旗的算法,在一次遍历中完成排序?


颜色分类问题虽然简单,但它是理解分区算法和多指针技巧的绝佳例子。通过这个问题,我们不仅学会了解决特定问题的技巧,还触及了算法设计的一般思路。希望这次探讨能帮助你在数组处理和排序算法方面有更深的理解!

💪 练习建议#

想要巩固对这个问题的理解,不妨尝试:

  1. 手动模拟荷兰国旗算法的过程,理解三个指针的移动规律
  2. 尝试实现处理四种颜色的分类算法
  3. 将这种思想应用到快速排序中,实现三路快排

记住,算法的精髓不在于记忆代码,而在于理解思想。当你能够自然地推导出荷兰国旗算法,你就真正掌握了这个问题背后的核心思想!