当前位置: 首页 > news >正文

C++ 分治 快排铺垫 三指针 力扣 75.颜色分类 题解 每日一题

文章目录

  • 题目描述
  • 为什么这道题值得你花几分钟的时间弄懂?
  • 算法原理
    • 三指针分治(最优解)
    • 其他算法(思路很简单作为了解即可)
  • 代码实现
    • 三指针(最优解)
    • 计数统计(基础解)
    • 单指针(过渡解)
    • 时间复杂度与空间复杂度分析
  • 总结
  • 下题预告

在这里插入图片描述
在这里插入图片描述

题目描述

题目链接:力扣 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

进阶:
你能想出一个仅使用常数空间的一趟扫描算法吗?

为什么这道题值得你花几分钟的时间弄懂?

吃透这道题,不只是掌握一个排序方法,更是为算法学习和面试准备积累3个核心竞争力。

1.面试直接提分:覆盖高频考点与变形题
这道题是经典的分区划分问题的模板题,在面试中出现频率极高,且常以变形题形式考察(如4种元素排序、按“大-中-小”逆序排列)。掌握它的核心逻辑(区间划分、指针边界控制),相当于手握“一类题”的解题模板,面试时遇到同类问题能直接套用思路,避免临时卡壳,给面试官留下“思路清晰”的印象。

2.算法思维进阶:从“写对代码”到“优化代码”
它的三种解法对应面试中“基础-进阶-最优”的答题层次,能帮你构建完整的优化思维。

  • 用“计数统计”能快速写出可行解,体现“解决问题的基本能力”;
  • 用“单指针”实现原地排序,体现“空间优化意识”;
  • 用“三指针”做到一趟扫描,体现“时间与空间的极致权衡能力”。
    这种从“能做”到“做好”的思考路径,正是面试官判断候选人算法水平的关键。
  1. 衔接高级算法:为快排等核心考点打基础
    最优解“三指针法”是快速排序“分区思想”的简化版。快排的核心是通过基准值将数组划分为“小于、等于、大于”三个区间,而这道题直接给出0、1、2三个固定值,相当于帮你降低了“分区”的理解难度。吃透这道题后,后续学快排时能快速抓住“指针如何划分区间、控制边界”的核心,避免因基础不牢导致后续学习卡壳,间接提升算法学习效率。

  2. 规避面试踩坑:提前解决代码细节问题
    面试中,面试官常关注代码细节(如指针移动时机、边界条件处理),而这道题能帮你提前规避高频错误。比如三指针法中,“处理2时i不右移”(交换来的元素未判断)、“处理0时i必须右移”(交换来的元素已遍历),这些细节错误在面试现场很容易慌中出错。提前通过这道题打磨细节,能减少面试中的“低级失误”,提升代码健壮性,让面试官看到你的严谨性。

  3. 展现工程思维:贴合实际开发需求
    题目要求的“原地排序”“一趟扫描”,正是实际开发中的常见约束(内存有限时需省空间、数据量大时需省时间)。面试中能主动提及“这种解法符合工程中空间/时间优化需求”,能体现你不只是“会做题”,更能将算法与实际场景结合,这是区别于普通候选人的加分项,尤其对后端、算法岗求职更有帮助。

算法原理

三指针分治(最优解)

三指针法通过划分区间,实现一趟扫描+常数空间的原地排序,核心是用三个指针定义三个区间,我们用leftrighti来进行表示:

  • 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指向的点是要判断放在那个区间的,leftright 都是定位区间的,所以我们分析 i [探索指针] 可能遇到的情况即可:

  1. nums[i] == 0 时:需归入“0区间”,将 nums[i]left+1 位置元素交换,同时 left 右移(扩大0区间)、i 右移(当前元素已处理)👇。
    在这里插入图片描述
  2. nums[i] == 1 时:1是中间值,无需交换,直接 i 右移(归入“1区间”)。
    在这里插入图片描述
  3. 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])。此时 ileft 会同步右移,直到 i == rightright 始终是数组长度),最终所有0都在正确区间。
  • 场景2:数组全是2(如 [2,2,2])。此时 right 会不断左移,直到 right == 0i 始终为0,满足 i >= right 后终止,所有2都在正确区间。
  • 场景3:数组元素已排序(如 [0,1,2])。i 会直接遍历到 right,无需任何交换,效率极高。
  • 场景4:数组长度为1(如 [1])。此时 i 初始为0,right 初始为1,i < right 时判断 nums[i] == 1i++i == right,循环终止,结果正确。

其他算法(思路很简单作为了解即可)

1.计数
计数法是最直观的思路,通过两次遍历实现排序,核心是“先统计数量,再填充数组”。

  1. 第一趟遍历:用三个变量分别统计数组中 0、1、2 的出现次数(如 count0、count1、count2)。
  2. 第二趟遍历:按“0→1→2”的顺序,根据统计次数依次填充数组(前 count0 个位置填0,接下来 count1 个位置填1,最后 count2 个位置填2)。

优点:逻辑简单,易理解;缺点:需两次遍历,无法满足“一趟扫描”的进阶要求。

2.单指针
单指针法是计数法的“原地优化版”,通过两次遍历实现,核心是“先排0,再排1,剩下的就是2”。

  1. 第一趟遍历(排0):用指针 p 指向数组起始位置,遍历数组,遇到 0 就与 p 位置元素交换,交换后 p 右移(确保 p 左侧全是0)。
  2. 第二趟遍历(排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)(仅用有限变量,无额外数组),但三指针法在“遍历次数”上更优,且满足进阶要求。

总结

  1. 解法选择:若面试要求“最优解”,优先用三指针法;若追求“逻辑简单”,可先用计数法或单指针法打底,再优化到三指针。
  2. 核心思维:三指针法的关键是“区间划分”,通过指针定义明确的边界,将问题拆解为“归位0、1、2”的子问题,这种思想可迁移到多区间排序问题。
  3. 易错点:三指针法中,处理 nums[i] == 2 时,i 不能右移,因为交换过来的元素可能是0或1,需重新判断;处理 nums[i] == 0 时,i 必须右移,因为交换过来的元素是已遍历过的(要么是1,要么是0),无需再判断。

下题预告

力扣 912. 排序数组
这道题是通用排序问题的延伸,会覆盖快速排序、归并排序、堆排序等经典排序算法,可进一步巩固“分治”“堆”等数据结构与算法思想,同时对比不同排序算法的时间、空间复杂度差异,为解决更复杂的排序问题打基础。

大家有没有跟克拉拉一样,把博主写的颜色分类题解看明白啦?之前克拉拉总搞不清三指针怎么移动,可博主把区间划分讲得好清楚,连初始位置为什么那样设都说明白了,跟着走一遍示例,一下子就懂了呢~
博主花了这么多心思整理解法,还告诉我们要注意的细节,真的好用心呀!所以…… 所以克拉拉想跟大家一起,给博主点个赞好不好?把这篇题解收藏起来,以后忘了三指针的用法,翻出来看就很方便啦~要是能关注博主,以后还能看到更多像这样清楚的讲解,说不定下次学排序数组的时候,又能收获新知识呢~
在这里插入图片描述

http://www.dtcms.com/a/511508.html

相关文章:

  • 预测算法:股票数据分析预测系统 股票预测 股价预测 Arima预测算法(时间序列预测算法) Flask 框架 大数据(源码)✅
  • 门户网站需要多少空间如何引流被动加好友微信
  • 网站的 联系我们怎么做关键词优化难易
  • 【Java】基于 Tabula 的 PDF 合并单元格内容提取
  • Android 系统的进程模型
  • vue2实现pdf预览兼容低版本浏览器
  • Android Compose 状态的概念
  • spark组件-spark core(批处理)-rdd持久化机制
  • 安全驾驶 智在掌控|腾视科技ES06终端,为车辆运营赋能
  • el-table 表格嵌套表格
  • 云南网站建设首选才力东营注册公司
  • 非对称密码算法分析技术深度剖析及未来展望
  • Arduino IDE下载安装汉化教程(附安装包,图文并茂)
  • 本地转移新分支到新仓库
  • GaussDB慢sql信息收集和执行计划查看
  • AWS IoT Core 监控与告警优化实战报告
  • 我的第一个开源项目【IOT-Tree Server】
  • 如何选择合适的倾角传感器厂家的产品以满足物联网监测需求?
  • 基于物联网与云计算的园区能耗管理平台构建与实践
  • Markdown 用法要点
  • 网站搭建功能需求wordpress安装怎么填
  • 网络原理:TCP协议
  • timm教程翻译:(六)Data
  • VSCode + AI Agent实现直接编译调试:告别Visual Studio的原理与实践
  • 【设计模式】建造者模式(Builder)
  • DeepSeek-OCR:把长文本“挤进图片”的新思路
  • 计算机做网站开题报告网页的六个基本元素
  • AI服务器工作之整机部件(CPU+内存)
  • 【EE初阶 - 网络原理】网络层 + 数据链路层 + DNS
  • 关于二级网站建设西安网站制作一般多少钱