C++ 分治 快排铺垫 三指针 力扣 75.颜色分类 题解 每日一题
文章目录
- 题目描述
- 为什么这道题值得你花几分钟的时间弄懂?
- 算法原理
- 三指针分治(最优解)
- 其他算法(思路很简单作为了解即可)
- 代码实现
- 三指针(最优解)
- 计数统计(基础解)
- 单指针(过渡解)
- 时间复杂度与空间复杂度分析
- 总结
- 下题预告


题目描述
题目链接:力扣 75. 颜色分类
题目描述:
示例 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
进阶:
你能想出一个仅使用常数空间的一趟扫描算法吗?
为什么这道题值得你花几分钟的时间弄懂?
吃透这道题,不只是掌握一个排序方法,更是为算法学习和面试准备积累3个核心竞争力。
1.面试直接提分:覆盖高频考点与变形题
这道题是经典的分区划分问题的模板题,在面试中出现频率极高,且常以变形题形式考察(如4种元素排序、按“大-中-小”逆序排列)。掌握它的核心逻辑(区间划分、指针边界控制),相当于手握“一类题”的解题模板,面试时遇到同类问题能直接套用思路,避免临时卡壳,给面试官留下“思路清晰”的印象。
2.算法思维进阶:从“写对代码”到“优化代码”
它的三种解法对应面试中“基础-进阶-最优”的答题层次,能帮你构建完整的优化思维。
- 用“计数统计”能快速写出可行解,体现“解决问题的基本能力”;
- 用“单指针”实现原地排序,体现“空间优化意识”;
- 用“三指针”做到一趟扫描,体现“时间与空间的极致权衡能力”。
这种从“能做”到“做好”的思考路径,正是面试官判断候选人算法水平的关键。
-
衔接高级算法:为快排等核心考点打基础
最优解“三指针法”是快速排序“分区思想”的简化版。快排的核心是通过基准值将数组划分为“小于、等于、大于”三个区间,而这道题直接给出0、1、2三个固定值,相当于帮你降低了“分区”的理解难度。吃透这道题后,后续学快排时能快速抓住“指针如何划分区间、控制边界”的核心,避免因基础不牢导致后续学习卡壳,间接提升算法学习效率。 -
规避面试踩坑:提前解决代码细节问题
面试中,面试官常关注代码细节(如指针移动时机、边界条件处理),而这道题能帮你提前规避高频错误。比如三指针法中,“处理2时i不右移”(交换来的元素未判断)、“处理0时i必须右移”(交换来的元素已遍历),这些细节错误在面试现场很容易慌中出错。提前通过这道题打磨细节,能减少面试中的“低级失误”,提升代码健壮性,让面试官看到你的严谨性。 -
展现工程思维:贴合实际开发需求
题目要求的“原地排序”“一趟扫描”,正是实际开发中的常见约束(内存有限时需省空间、数据量大时需省时间)。面试中能主动提及“这种解法符合工程中空间/时间优化需求”,能体现你不只是“会做题”,更能将算法与实际场景结合,这是区别于普通候选人的加分项,尤其对后端、算法岗求职更有帮助。
算法原理
三指针分治(最优解)
三指针法通过划分区间,实现一趟扫描+常数空间的原地排序,核心是用三个指针定义三个区间,我们用left
,right
,i
来进行表示:
- left 指针:指向已排序完成的“0区间”的右边界(初始为 -1,代表暂无0)。
- i 指针:指向当前正在遍历、待判断的元素(初始为 0)。
- right 指针:指向已排序完成的“2区间”的左边界(初始为 nums.size(),代表暂无2)。
当我们这样做区分后我们可以得到四个区间如下图👇:
我们已知题目最后让我们按照红(0)白(1)蓝(2)进行排列,那么我们分析每个区间的作用:
- 1.
[0,left]
: 已排序完成的“0区间”,存的是0。 - 2.
[left+1,i-1]
: 已排序完成的“1区间”,存的是1。 - 3.
[i,right-1]
: 未排序的区间。 - 4.
[right,nums.size()-1]
: 已排序完成的“2区间”,存的是2.
执行逻辑分析
三指针中i
指向的点是要判断放在那个区间的,left
和 right
都是定位区间的,所以我们分析 i
[探索指针] 可能遇到的情况即可:
- 当
nums[i] == 0
时:需归入“0区间”,将nums[i]
与left+1
位置元素交换,同时left
右移(扩大0区间)、i
右移(当前元素已处理)👇。
- 当
nums[i] == 1
时:1是中间值,无需交换,直接i
右移(归入“1区间”)。
- 当
nums[i] == 2
时:需归入“2区间”,将nums[i]
与right-1
位置元素交换,同时right
左移(扩大2区间);注意 i 不右移,因为交换过来的新元素未判断。
细节处理
这三个细节可以自己动手在纸上画一画,理解更深刻
1. 三个指针的初始位置
初始位置的设定不是随意的,而是为了明确“初始时无已确认区间”:
left = -1
:因为初始时没有任何0被确认,left
需指向“0区间”的虚拟右边界(即数组第一个元素左侧),这样++left
后才能准确指向第一个0的存放位置。right = nums.size()
:同理,初始时没有任何2被确认,right
需指向“2区间”的虚拟左边界(即数组最后一个元素右侧),这样--right
后才能准确指向第一个2的存放位置。i = 0
:从数组第一个元素开始遍历,确保所有待处理元素都能被覆盖。
2. 循环终止条件
终止条件 i >= right
的本质是“待处理区间为空”:
- 待处理区间是
[i, right-1]
,当i >= right
时,这个区间的左右边界交叉,意味着所有元素都已归入“0区间”或“2区间”,无需再处理。 - 注意不能用
i == nums.size()
作为终止条件,因为right
会左移(比如数组尾部全是2时,right
会提前左移到中间位置),此时i
达到right
就已处理完所有元素,无需遍历到数组末尾。
4. 边界校验:特殊场景下的逻辑正确性
需确保算法在极端场景下仍能正常运行,这也是面试中面试官可能追问的点:
- 场景1:数组全是0(如
[0,0,0]
)。此时i
和left
会同步右移,直到i == right
(right
始终是数组长度),最终所有0都在正确区间。 - 场景2:数组全是2(如
[2,2,2]
)。此时right
会不断左移,直到right == 0
,i
始终为0,满足i >= right
后终止,所有2都在正确区间。 - 场景3:数组元素已排序(如
[0,1,2]
)。i
会直接遍历到right
,无需任何交换,效率极高。 - 场景4:数组长度为1(如
[1]
)。此时i
初始为0,right
初始为1,i < right
时判断nums[i] == 1
,i++
后i == right
,循环终止,结果正确。
其他算法(思路很简单作为了解即可)
1.计数
计数法是最直观的思路,通过两次遍历实现排序,核心是“先统计数量,再填充数组”。
- 第一趟遍历:用三个变量分别统计数组中 0、1、2 的出现次数(如 count0、count1、count2)。
- 第二趟遍历:按“0→1→2”的顺序,根据统计次数依次填充数组(前 count0 个位置填0,接下来 count1 个位置填1,最后 count2 个位置填2)。
优点:逻辑简单,易理解;缺点:需两次遍历,无法满足“一趟扫描”的进阶要求。
2.单指针
单指针法是计数法的“原地优化版”,通过两次遍历实现,核心是“先排0,再排1,剩下的就是2”。
- 第一趟遍历(排0):用指针 p 指向数组起始位置,遍历数组,遇到 0 就与 p 位置元素交换,交换后 p 右移(确保 p 左侧全是0)。
- 第二趟遍历(排1):从 p 位置开始继续遍历,遇到 1 就与 p 位置元素交换,交换后 p 右移(确保 p 左侧全是0和1)。
优点:无需额外计数变量,原地排序;缺点:仍需两次遍历,效率低于三指针法。
代码实现
三指针(最优解)
class Solution {
public:void sortColors(vector<int>& nums) {// left:0区间右边界,i:当前遍历元素,right:2区间左边界int left = -1, i = 0, right = nums.size();// 遍历终止条件:i进入2区间(所有元素处理完毕)while(i < right){if(nums[i] == 0) {// 0归入0区间,left右移,i右移(当前元素已处理)swap(nums[++left], nums[i++]);} else if(nums[i] == 1) {// 1无需交换,直接i右移(归入1区间)i++;} else {// 2归入2区间,right左移,i不右移(新元素待判断)swap(nums[--right], nums[i]);}}}
};
代码走读(以示例1 [2,0,2,1,1,0] 为例)
初始状态设定
逐轮循环执行
循环终止条件为 i >= right
,以下按每轮 i
指向元素的判断结果,拆解执行步骤:
📌第1轮:nums[i] = nums[0] = 2
判断:当前元素是2,需归入“2区间”。
执行操作:
1.right
先左移1位(right = 6-1 = 5
),指向“2区间”的新左边界(索引5)。
2.交换 nums[i]
(索引0的2)和 nums[right]
(索引5的0)。
结果:
📌第2轮:nums[i] = nums[0] = 0
判断:当前元素是0,需归入“0区间”。
执行操作:
1.left
先右移1位(left = -1+1 = 0
),指向“0区间”的新右边界(索引0)。
2.交换 nums[i]
(索引0的0)和 nums[left]
(索引0的0,自身交换,无变化)。
3.i
右移1位(i = 0+1 = 1
),处理下一个待判断元素。
结果:
📌第3轮:nums[i] = nums[1] = 0
判断:当前元素是0,需归入“0区间”。
执行操作:
1.left
右移1位(left = 0+1 = 1
),指向“0区间”新右边界(索引1)。
2.交换 nums[i]
(索引1的0)和 nums[left]
(索引1的0,自身交换)。
3.i
右移1位(i = 1+1 = 2
),处理下一个元素。
结果:
📌第4轮:nums[i] = nums[2] = 2
判断:当前元素是2,需归入“2区间”。
执行操作:
1.right
左移1位(right = 5-1 = 4
),指向“2区间”新左边界(索引4)。
2.交换 nums[i]
(索引2的2)和 nums[right]
(索引4的1)。
结果:
📌第5轮:nums[i] = nums[2] = 1
判断:当前元素是1,无需交换(1属于“1区间”,直接归入)。
执行操作:i
右移1位(i = 2+1 = 3
),处理下一个元素。
结果:
📌第6轮:nums[i] = nums[3] = 1
判断:当前元素是1,无需交换,直接归入“1区间”。
执行操作:i
右移1位(i = 3+1 = 4
),处理下一个元素。
结果:
📌循环终止与最终结果
当 i=4
时,right=4
,将不满足 i < right
,循环终止。此时:
各区间状态:
- “0区间”:
[0, left] = [0,1]
(索引0-1为0)。 - “1区间”:
[left+1, i-1] = [2,3]
(索引2-3为1)。 - “2区间”:
[right, 5] = [4,5]
(索引4-5为2)。
最终数组:
计数统计(基础解)
class Solution {
public:void sortColors(vector<int>& nums) {int count0 = 0, count1 = 0, count2 = 0;// 第一趟:统计0、1、2的数量for(int num : nums) {if(num == 0) count0++;else if(num == 1) count1++;else count2++;}// 第二趟:按顺序填充数组int index = 0;// 填充0while(count0--) nums[index++] = 0;// 填充1while(count1--) nums[index++] = 1;// 填充2while(count2--) nums[index++] = 2;}
};
单指针(过渡解)
class Solution {
public:void sortColors(vector<int>& nums) {int p = 0;int n = nums.size();// 第一趟:将所有0移到数组左侧for(int i = 0; i < n; i++) {if(nums[i] == 0) {swap(nums[p], nums[i]);p++;}}// 第二趟:从p开始,将所有1移到0的右侧for(int i = p; i < n; i++) {if(nums[i] == 1) {swap(nums[p], nums[i]);p++;}}// 剩余元素默认是2,无需处理}
};
时间复杂度与空间复杂度分析
算法 | 时间复杂度 | 空间复杂度 | 核心优势 |
---|---|---|---|
三指针分治 | O(n) [整体只遍历一遍] | O(1) | 一趟扫描、常数空间、最优 |
计数统计 | O(n) [整体要遍历两遍] | O(1) | 逻辑简单、易实现 |
单指针 | O(n) [整体要遍历两遍] | O(1) | 原地排序、过渡思路 |
注:三者时间复杂度均为 O(n)(需遍历数组1-2次),空间复杂度均为 O(1)(仅用有限变量,无额外数组),但三指针法在“遍历次数”上更优,且满足进阶要求。
总结
- 解法选择:若面试要求“最优解”,优先用三指针法;若追求“逻辑简单”,可先用计数法或单指针法打底,再优化到三指针。
- 核心思维:三指针法的关键是“区间划分”,通过指针定义明确的边界,将问题拆解为“归位0、1、2”的子问题,这种思想可迁移到多区间排序问题。
- 易错点:三指针法中,处理
nums[i] == 2
时,i
不能右移,因为交换过来的元素可能是0或1,需重新判断;处理nums[i] == 0
时,i
必须右移,因为交换过来的元素是已遍历过的(要么是1,要么是0),无需再判断。
下题预告
力扣 912. 排序数组
这道题是通用排序问题的延伸,会覆盖快速排序、归并排序、堆排序等经典排序算法,可进一步巩固“分治”“堆”等数据结构与算法思想,同时对比不同排序算法的时间、空间复杂度差异,为解决更复杂的排序问题打基础。
大家有没有跟克拉拉一样,把博主写的颜色分类题解看明白啦?之前克拉拉总搞不清三指针怎么移动,可博主把区间划分讲得好清楚,连初始位置为什么那样设都说明白了,跟着走一遍示例,一下子就懂了呢~
博主花了这么多心思整理解法,还告诉我们要注意的细节,真的好用心呀!所以…… 所以克拉拉想跟大家一起,给博主点个赞好不好?把这篇题解收藏起来,以后忘了三指针的用法,翻出来看就很方便啦~要是能关注博主,以后还能看到更多像这样清楚的讲解,说不定下次学排序数组的时候,又能收获新知识呢~