【LeetCode】75. 颜色分类
文章目录
- 75. 颜色分类
- 题目描述
- 示例 1:
- 示例 2:
- 提示:
- 进阶:
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(三指针)
- 三指针分区详细流程
- 交换策略流程
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:三指针(荷兰国旗,最优)
- 技巧2:计数排序(两次遍历)
- 技巧3:双指针(只处理0和2)
- 技巧4:快速排序变体
- 边界情况处理
- 测试用例设计
- 基础测试
- 简单情况
- 边界测试
- 特殊情况
- 常见错误与陷阱
- 错误1:交换2时cur前进
- 错误2:循环条件错误
- 错误3:指针初始化错误
- 错误4:忘记原地操作
- 实战技巧总结
- 进阶扩展
- 扩展1:四种颜色(0,1,2,3)
- 扩展2:返回排序后的新数组
- 扩展3:统计交换次数
- 应用场景
- 代码实现
- 测试结果
- 核心收获
- 应用拓展
- 完整题解代码
75. 颜色分类
题目描述
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
提示:
- n == nums.length
- 1 <= n <= 300
- nums[i] 为 0、1 或 2
进阶:
- 你能想出一个仅使用常数空间的一趟扫描算法吗?
解题思路
问题深度分析
这是经典的荷兰国旗问题(Dutch National Flag Problem),由计算机科学家Dijkstra提出。核心在于三路快排和双指针技术,要求在O(n)时间、O(1)空间内完成原地排序。
问题本质
给定只包含0、1、2三种元素的数组,要求:
- 原地排序:不使用额外数组
- 顺序排列:0在前,1在中,2在后
- 一趟扫描:时间复杂度O(n)
这是一个受限排序问题,元素只有三种值,可以用计数排序或三指针分区解决。
核心思想
三指针分区(荷兰国旗算法):
left
指针:指向下一个0应该放置的位置right
指针:指向下一个2应该放置的位置cur
指针:当前遍历的位置
分区规则:
[0...left-1]
:全是0[left...cur-1]
:全是1[cur...right]
:待处理区域[right+1...n-1]
:全是2
操作流程:
nums[cur] == 0
:与nums[left]
交换,left++
,cur++
nums[cur] == 1
:不交换,cur++
nums[cur] == 2
:与nums[right]
交换,right--
(cur不动,因为交换来的元素未检查)
关键难点分析
难点1:为什么交换2时cur不动?
- 与
right
交换后,cur
位置的值是从右边来的,尚未检查 - 需要在下一轮循环检查这个值
- 而与
left
交换时,left
左边的都是已处理的0或1,可以安全前进
难点2:循环终止条件
cur <= right
(不是cur < n
)- 当
cur > right
时,所有元素都已分类
难点3:初始指针位置
left = 0
:第一个0的位置right = n-1
:最后一个2的位置cur = 0
:从头开始遍历
典型情况分析
情况1:一般情况
输入: [2,0,2,1,1,0]
初始: left=0, cur=0, right=5步骤1: nums[0]=2 → 交换nums[0]和nums[5] → [0,0,2,1,1,2], right=4
步骤2: nums[0]=0 → 交换nums[0]和nums[0] → [0,0,2,1,1,2], left=1, cur=1
步骤3: nums[1]=0 → 交换nums[1]和nums[1] → [0,0,2,1,1,2], left=2, cur=2
步骤4: nums[2]=2 → 交换nums[2]和nums[4] → [0,0,1,1,2,2], right=3
步骤5: nums[2]=1 → 不交换 → cur=3
步骤6: nums[3]=1 → 不交换 → cur=4
结束: cur > right输出: [0,0,1,1,2,2]
情况2:已排序
输入: [0,1,2]
处理: 0→交换自己, 1→cur++, 2→交换自己
输出: [0,1,2]
情况3:逆序
输入: [2,1,0]
处理: 需要多次交换
输出: [0,1,2]
情况4:全相同
输入: [1,1,1]
处理: 全部cur++
输出: [1,1,1]
情况5:只有两种颜色
输入: [0,0,2,2]
输出: [0,0,2,2]输入: [2,2,0,0]
输出: [0,0,2,2]
算法对比
算法 | 时间复杂度 | 空间复杂度 | 扫描次数 | 特点 |
---|---|---|---|---|
三指针 | O(n) | O(1) | 1次 | 最优解法 |
计数排序 | O(n) | O(1) | 2次 | 简单直观 |
快速排序 | O(nlogn) | O(logn) | 多次 | 通用但不是最优 |
冒泡排序 | O(n²) | O(1) | 多次 | 效率低 |
注:本题因为只有3个值,可以O(n)解决
算法流程图
主算法流程(三指针)
graph TDA[开始: nums数组] --> B[初始化指针]B --> C[left=0, cur=0, right=n-1]C --> D{cur <= right?}D -->|否| E[结束,排序完成]D -->|是| F{nums cur 的值?}F -->|== 0| G[交换nums cur 和nums left]G --> H[left++, cur++]H --> DF -->|== 1| I[cur++]I --> DF -->|== 2| J[交换nums cur 和nums right]J --> K[right--]K --> D
三指针分区详细流程
graph TDA[三指针分区] --> B[区域划分]B --> C[区域1: 0,left-1 全是0]B --> D[区域2: left,cur-1 全是1]B --> E[区域3: cur,right 待处理]B --> F[区域4: right+1,n-1 全是2]A --> G[指针移动规则]G --> H[遇到0: 与left交换, left++, cur++]G --> I[遇到1: cur++]G --> J[遇到2: 与right交换, right--]J --> K[注意: cur不动,因为交换来的值未检查]
交换策略流程
graph TDA[当前元素nums cur] --> B{值是多少?}B -->|0| C[应该在左边]C --> D[与left位置交换]D --> E[left指向下一个0的位置]E --> F[left++]D --> G[cur已处理过left位置的值]G --> H[cur++]B -->|1| I[应该在中间]I --> J[保持原位]J --> K[cur++]B -->|2| L[应该在右边]L --> M[与right位置交换]M --> N[right指向下一个2的位置]N --> O[right--]M --> P[cur位置的值未知]P --> Q[cur不动,下次循环检查]
复杂度分析
时间复杂度详解
三指针算法:O(n)
- 单次遍历:每个元素最多被访问2次
- cur指针从0移动到right,最多n步
- 总时间:O(n)
计数排序:O(n)
- 第一次遍历:统计0、1、2的个数 - O(n)
- 第二次遍历:根据计数重建数组 - O(n)
- 总时间:O(2n) = O(n)
快速排序:O(nlogn)
- 通用排序算法
- 未利用只有3个值的特性
空间复杂度详解
三指针算法:O(1)
- 只用3个指针变量
计数排序:O(1)
- 只需3个计数变量
快速排序:O(logn)
- 递归栈空间
关键优化技巧
技巧1:三指针(荷兰国旗,最优)
func sortColors(nums []int) {left, cur, right := 0, 0, len(nums)-1for cur <= right {if nums[cur] == 0 {// 0应该在左边,与left交换nums[left], nums[cur] = nums[cur], nums[left]left++cur++} else if nums[cur] == 1 {// 1在中间,cur继续前进cur++} else {// 2应该在右边,与right交换nums[cur], nums[right] = nums[right], nums[cur]right--// 注意:cur不动,因为交换来的值还未检查}}
}
优势:
- 一趟扫描:O(n)
- 原地排序:O(1)空间
- 最优解法
技巧2:计数排序(两次遍历)
func sortColors(nums []int) {count0, count1, count2 := 0, 0, 0// 第一次遍历:统计for _, num := range nums {if num == 0 {count0++} else if num == 1 {count1++} else {count2++}}// 第二次遍历:重建i := 0for count0 > 0 {nums[i] = 0i++count0--}for count1 > 0 {nums[i] = 1i++count1--}for count2 > 0 {nums[i] = 2i++count2--}
}
特点:
- 简单直观
- 需要两次遍历
- O(n)时间,O(1)空间
技巧3:双指针(只处理0和2)
func sortColors(nums []int) {n := len(nums)left, right := 0, n-1// 先把所有0移到左边for i := 0; i <= right; {if nums[i] == 0 {nums[i], nums[left] = nums[left], nums[i]left++i++} else if nums[i] == 2 {nums[i], nums[right] = nums[right], nums[i]right--} else {i++}}
}
说明:这本质上就是三指针的另一种写法
技巧4:快速排序变体
func sortColors(nums []int) {// 以1为pivot进行三路快排quickSort(nums, 0, len(nums)-1)
}func quickSort(nums []int, left, right int) {if left >= right {return}// 三路快排,pivot=1lt, gt, i := left, right, leftpivot := 1for i <= gt {if nums[i] < pivot {nums[lt], nums[i] = nums[i], nums[lt]lt++i++} else if nums[i] > pivot {nums[i], nums[gt] = nums[gt], nums[i]gt--} else {i++}}
}
说明:利用三路快排思想
边界情况处理
- 空数组:
nums = []
→ 无需处理 - 单个元素:
nums = [0]
→[0]
nums = [1]
→[1]
nums = [2]
→[2]
- 两个元素:
nums = [1,0]
→[0,1]
nums = [2,1]
→[1,2]
- 全相同:
nums = [0,0,0]
→[0,0,0]
nums = [1,1,1]
→[1,1,1]
nums = [2,2,2]
→[2,2,2]
- 只有两种颜色:
nums = [0,2,0,2]
→[0,0,2,2]
- 已排序:
nums = [0,1,2]
→[0,1,2]
测试用例设计
基础测试
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
说明: 一般情况
简单情况
输入: [2,0,1]
输出: [0,1,2]
说明: 三个不同元素
边界测试
输入: [0]
输出: [0]
说明: 单个元素输入: [1,0]
输出: [0,1]
说明: 两个元素
特殊情况
输入: [1,1,1]
输出: [1,1,1]
说明: 全相同输入: [2,1,0]
输出: [0,1,2]
说明: 逆序
常见错误与陷阱
错误1:交换2时cur前进
// ❌ 错误:交换2时cur++
if nums[cur] == 2 {nums[cur], nums[right] = nums[right], nums[cur]right--cur++ // 错误!交换来的值未检查
}// ✅ 正确:cur不动
if nums[cur] == 2 {nums[cur], nums[right] = nums[right], nums[cur]right--// cur不动,下次循环检查
}
错误2:循环条件错误
// ❌ 错误:应该是cur <= right
for cur < right { // 错误,会漏掉cur==right的情况// ...
}// ✅ 正确:
for cur <= right {// ...
}
错误3:指针初始化错误
// ❌ 错误:right应该是n-1
right := len(nums) // 错误,越界// ✅ 正确:
right := len(nums) - 1
错误4:忘记原地操作
// ❌ 错误:创建新数组
result := make([]int, len(nums))
// ...
return result// ✅ 正确:原地修改
// 直接在nums上操作,不返回值
实战技巧总结
- 三指针核心:left指向0区域末尾,right指向2区域开头,cur遍历
- 交换规则:0与left换,2与right换,1不换
- cur前进条件:只有处理0和1时cur才前进,处理2时cur不动
- 循环条件:
cur <= right
,不是cur < n
- 一趟扫描:每个元素最多被访问两次
- 原地操作:O(1)空间,直接在原数组上修改
进阶扩展
扩展1:四种颜色(0,1,2,3)
func sortFourColors(nums []int) {// 需要分成4个区域// 可以先用三指针分离0,然后对剩余部分分离1// 或者使用计数排序count := make([]int, 4)for _, num := range nums {count[num]++}i := 0for color := 0; color < 4; color++ {for count[color] > 0 {nums[i] = colori++count[color]--}}
}
扩展2:返回排序后的新数组
func sortColorsNew(nums []int) []int {result := make([]int, len(nums))copy(result, nums)left, cur, right := 0, 0, len(result)-1for cur <= right {if result[cur] == 0 {result[left], result[cur] = result[cur], result[left]left++cur++} else if result[cur] == 1 {cur++} else {result[cur], result[right] = result[right], result[cur]right--}}return result
}
扩展3:统计交换次数
func sortColorsWithCount(nums []int) int {left, cur, right := 0, 0, len(nums)-1swapCount := 0for cur <= right {if nums[cur] == 0 {if left != cur {nums[left], nums[cur] = nums[cur], nums[left]swapCount++}left++cur++} else if nums[cur] == 1 {cur++} else {nums[cur], nums[right] = nums[right], nums[cur]swapCount++right--}}return swapCount
}
应用场景
- 数据分类:将数据按特征分成几类
- 快速排序优化:三路快排处理重复元素
- 图像处理:颜色通道分离
- 网络包分类:按优先级分类
- 算法竞赛:荷兰国旗问题的经典应用
代码实现
本题提供了四种不同的解法,重点掌握三指针(荷兰国旗)方法。
测试结果
测试用例 | 三指针 | 计数排序 | 双指针 | 快排变体 |
---|---|---|---|---|
一般情况 | ✅ | ✅ | ✅ | ✅ |
简单情况 | ✅ | ✅ | ✅ | ✅ |
边界测试 | ✅ | ✅ | ✅ | ✅ |
特殊情况 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 荷兰国旗算法:三指针分区的经典应用
- 一趟扫描:O(n)时间,O(1)空间
- 交换策略:处理2时cur不动是关键
- 三路快排:处理重复元素的优化思路
- 原地操作:不使用额外数组的排序
应用拓展
- 快速排序的三路分区优化
- 数据分类和聚类
- 图像颜色分离
- 网络流量分类
完整题解代码
package mainimport "fmt"// =========================== 方法一:三指针(荷兰国旗,最优) ===========================func sortColors(nums []int) {left, cur, right := 0, 0, len(nums)-1for cur <= right {if nums[cur] == 0 {// 0应该在左边,与left交换nums[left], nums[cur] = nums[cur], nums[left]left++cur++} else if nums[cur] == 1 {// 1在中间,cur继续前进cur++} else {// 2应该在右边,与right交换nums[cur], nums[right] = nums[right], nums[cur]right--// 注意:cur不动,因为交换来的值还未检查}}
}// =========================== 方法二:计数排序 ===========================func sortColors2(nums []int) {count0, count1, count2 := 0, 0, 0// 第一次遍历:统计for _, num := range nums {if num == 0 {count0++} else if num == 1 {count1++} else {count2++}}// 第二次遍历:重建i := 0for count0 > 0 {nums[i] = 0i++count0--}for count1 > 0 {nums[i] = 1i++count1--}for count2 > 0 {nums[i] = 2i++count2--}
}// =========================== 方法三:双指针 ===========================func sortColors3(nums []int) {n := len(nums)left, right := 0, n-1for i := 0; i <= right; {if nums[i] == 0 {nums[i], nums[left] = nums[left], nums[i]left++i++} else if nums[i] == 2 {nums[i], nums[right] = nums[right], nums[i]right--} else {i++}}
}// =========================== 方法四:快速排序变体 ===========================func sortColors4(nums []int) {if len(nums) <= 1 {return}quickSort(nums, 0, len(nums)-1)
}func quickSort(nums []int, left, right int) {if left >= right {return}// 三路快排,pivot=1lt, gt, i := left, right, leftpivot := 1for i <= gt {if nums[i] < pivot {nums[lt], nums[i] = nums[i], nums[lt]lt++i++} else if nums[i] > pivot {nums[i], nums[gt] = nums[gt], nums[i]gt--} else {i++}}
}// =========================== 测试代码 ===========================func main() {fmt.Println("=== LeetCode 75: 颜色分类(荷兰国旗问题) ===\n")testCases := []struct {nums []intexpect []int}{{[]int{2, 0, 2, 1, 1, 0},[]int{0, 0, 1, 1, 2, 2},},{[]int{2, 0, 1},[]int{0, 1, 2},},{[]int{0},[]int{0},},{[]int{1, 0},[]int{0, 1},},{[]int{1, 1, 1},[]int{1, 1, 1},},{[]int{2, 1, 0},[]int{0, 1, 2},},{[]int{0, 0, 2, 2},[]int{0, 0, 2, 2},},}fmt.Println("方法一:三指针(荷兰国旗)")runTests(testCases, sortColors)fmt.Println("\n方法二:计数排序")runTests(testCases, sortColors2)fmt.Println("\n方法三:双指针")runTests(testCases, sortColors3)fmt.Println("\n方法四:快速排序变体")runTests(testCases, sortColors4)
}func runTests(testCases []struct {nums []intexpect []int
}, fn func([]int)) {passCount := 0for i, tc := range testCases {nums := make([]int, len(tc.nums))copy(nums, tc.nums)fn(nums)status := "✅"if !equal(nums, tc.expect) {status = "❌"} else {passCount++}fmt.Printf(" 测试%d: %s\n", i+1, status)if status == "❌" {fmt.Printf(" 输入: %v\n", tc.nums)fmt.Printf(" 输出: %v\n", nums)fmt.Printf(" 期望: %v\n", tc.expect)}}fmt.Printf(" 通过: %d/%d\n", passCount, len(testCases))
}func equal(a, b []int) bool {if len(a) != len(b) {return false}for i := range a {if a[i] != b[i] {return false}}return true
}