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

LeetCode 3634. 使数组平衡的最少移除数目 详细技术解析

LeetCode 3634. 使数组平衡的最少移除数目 详细技术解析

**标签:**LeetCode | 数组 | 滑动窗口 | 双指针 | 排序 | 贪心 | 算法实战

**核心考点:**排序的应用、滑动窗口(双指针)技巧、贪心思想、边界场景处理

**适用人群:**算法初学者、后端开发、刷题爱好者,具备基础数组操作和排序认知,适合巩固滑动窗口解题思路。

**前置说明:**本题的核心痛点是“找到最长的平衡子数组”,进而通过“总长度 - 最长平衡子数组长度”得到最少移除数目。题目看似简单,但需结合排序+滑动窗口才能实现高效求解,避免暴力解法的超时问题。本文将从题意拆解、思路分析、多种解题方案、代码实现、边界案例、常见坑点六个维度,详细解析解题全流程,所有代码严格贴合题目要求的类和方法格式,可直接复制提交LeetCode,兼容Python3环境。

一、题意深度拆解(避免理解偏差)

1.1 题目核心定义

给定整数数组 nums 和整数 k,满足以下条件的数组称为“平衡数组”:

  • 数组非空(不能移除所有元素);

  • 数组的最大值 ≤ 最小值 × k

  • 特殊情况:长度为1的数组必为平衡数组(最大值=最小值,满足条件)。

题目要求:移除任意数量的元素,返回使剩余数组平衡的最少移除数目

1.2 核心转化(解题关键)

最少移除数目 = 数组总长度 - 最长平衡子数组的长度。

理由:移除元素越少,剩余元素越多,因此“最少移除”等价于“找到最长的平衡子数组”——只要找到长度最大的平衡子数组,用总长度减去该长度,即可得到答案。这是本题的核心转化思路,也是所有解题方案的出发点。

1.3 示例解析(辅助理解)

以示例1为例:nums = [2,1,5],k=2

  • 数组总长度为3;

  • 所有可能的非空子数组中,最长的平衡子数组是[2,1](长度2),满足 max(2,1)=2 ≤ 1×2;

  • 最少移除数目 = 3 - 2 = 1,与示例输出一致。

示例2:nums = [1,6,2,9],k=3

  • 最长平衡子数组是[6,2](长度2),满足 6 ≤ 2×3;

  • 最少移除数目 = 4 - 2 = 2,与示例输出一致。

二、解题思路分析(从暴力到高效)

本题的核心难点的是“如何高效找到最长平衡子数组”。由于数组元素无序,直接枚举所有子数组会超时(时间复杂度O(n²),n可达1e5),因此需要通过“排序+滑动窗口”优化,将时间复杂度降至O(n log n)(排序)+ O(n)(滑动窗口),满足题目数据量要求。

2.1 核心思路铺垫

为什么要先排序?

对于一个有序数组,任意子数组的最小值是子数组的第一个元素,最大值是子数组的最后一个元素——这是排序的核心价值!无需遍历子数组即可快速获取最值,大幅降低判断平衡的难度。

举例:排序后的数组 [1,2,6,9],子数组 [2,6] 的最小值=2,最大值=6,直接判断 6 ≤ 2×3 即可,无需额外计算。

2.2 整体解题流程

  1. 排序数组:将nums按升序排列,确保任意子数组的最值为子数组的首尾元素;

  2. 滑动窗口(双指针):用左右指针划定当前子数组范围,右指针向右移动,扩大子数组,当子数组不满足“最大值 ≤ 最小值×k”时,左指针向右移动,缩小子数组;

  3. 记录最长长度:遍历过程中,持续记录满足条件的子数组的最大长度;

  4. 计算答案:最少移除数目 = 总长度 - 最长平衡子数组长度。

2.3 关键细节(避免踩坑)

  • 初始状态:最长长度至少为1(因为长度为1的数组必平衡);

  • 窗口移动逻辑:右指针遍历所有元素,左指针仅在当前窗口不满足条件时移动,确保窗口始终是“当前右指针能覆盖的最长平衡子数组”;

  • 边界处理:数组长度为1时,直接返回0(无需移除任何元素)。

三、完整代码实现(三种方案,从易到难)

重点实现滑动窗口最优方案(实战首选),同时提供暴力方案(仅作对比,不可提交)和简化版滑动窗口方案(适合入门),所有代码严格遵循题目要求的类和方法格式。

3.1 方案1:滑动窗口(最优方案,推荐提交)

时间复杂度:O(n log n)(排序)+ O(n)(滑动窗口),空间复杂度:O(log n)(排序的递归栈空间,忽略排序所需空间时为O(1)),适配n=1e5的数据量。

fromtypingimportListclassSolution:defminRemoval(self,nums:List[int],k:int)->int:n=len(nums)# 边界情况:数组长度为1,无需移除任何元素ifn==1:return0# 步骤1:排序数组,使子数组的最值为首尾元素nums.sort()max_len=1# 最长平衡子数组长度,初始为1(单个元素必平衡)left=0# 滑动窗口左指针# 步骤2:滑动窗口,右指针遍历所有元素forrightinrange(n):# 当当前窗口不满足平衡条件,移动左指针缩小窗口# 窗口最小值是nums[left],最大值是nums[right]whilenums[right]>nums[left]*k:left+=1# 更新最长平衡子数组长度current_len=right-left+1ifcurrent_len>max_len:max_len=current_len# 步骤3:最少移除数目 = 总长度 - 最长平衡子数组长度returnn-max_len

3.2 方案2:简化版滑动窗口(入门易懂)

逻辑与最优方案一致,简化了部分判断,适合初学者理解滑动窗口的核心逻辑,性能与最优方案一致。

fromtypingimportListclassSolution:defminRemoval(self,nums:List[int],k:int)->int:nums.sort()n=len(nums)max_len=0left=0forrightinrange(n):# 维持窗口平衡:nums[right] <= nums[left] * kwhilenums[right]>nums[left]*k:left+=1# 记录当前窗口长度,更新最大值max_len=max(max_len,right-left+1)# 最少移除数目 = 总长度 - 最长平衡子数组长度returnn-max_len

3.3 方案3:暴力方案(仅作对比,不可提交)

思路:枚举所有可能的子数组,判断是否平衡,记录最长平衡子数组长度。时间复杂度O(n²),n=1e5时会超时,仅适合理解题意。

fromtypingimportListclassSolution:defminRemoval(self,nums:List[int],k:int)->int:n=len(nums)ifn==1:return0max_len=1# 枚举所有子数组的起始位置foriinrange(n):min_val=nums[i]max_val=nums[i]# 枚举子数组的结束位置forjinrange(i,n):min_val=min(min_val,nums[j])max_val=max(max_val,nums[j])# 判断子数组是否平衡ifmax_val<=min_val*k:current_len=j-i+1ifcurrent_len>max_len:max_len=current_lenreturnn-max_len

四、代码解析与复杂度分析

4.1 核心代码解析(滑动窗口最优方案)

  1. 边界处理:当数组长度为1时,直接返回0,因为单个元素必为平衡数组,无需移除任何元素;

  2. 排序:将nums升序排列,核心目的是让任意子数组的最小值为left指针指向的元素,最大值为right指针指向的元素,无需额外遍历子数组求最值;

  3. 滑动窗口初始化:max_len初始化为1(单个元素的长度),left指针初始化为0,right指针从0开始遍历;

  4. 窗口调整:当当前窗口(left~right)不满足“max ≤ min×k”时,left指针右移,缩小窗口,直到满足条件;

  5. 更新最长长度:每次调整窗口后,计算当前窗口长度,更新max_len;

  6. 计算答案:用数组总长度减去最长平衡子数组长度,得到最少移除数目。

4.2 复杂度对比(三种方案)

方案时间复杂度空间复杂度优势劣势
滑动窗口(最优)O(n log n) + O(n) ≈ O(n log n)O(log n)(排序栈空间)效率高,适配1e5级数据量,实战首选需理解排序+滑动窗口的结合逻辑
简化版滑动窗口O(n log n) + O(n)O(log n)逻辑简洁,易于理解,适合入门与最优方案性能一致,无明显劣势
暴力方案O(n²)O(1)逻辑直观,无需复杂思考效率极低,n=1e5时超时,无法提交

五、边界案例与测试验证

5.1 边界案例(容易踩坑的场景)

  1. 案例1:数组长度为1(nums = [5], k=10)

    • 输入:nums = [5], k=10,输出:0

    • 解析:单个元素必平衡,无需移除任何元素。

  2. 案例2:数组已平衡(nums = [4,6], k=2)

    • 输入:nums = [4,6], k=2,输出:0

    • 解析:6 ≤ 4×2,数组本身平衡,无需移除元素。

  3. 案例3:所有元素都需移除(除1个)(nums = [1,2,3,4,5], k=1)

    • 输入:nums = [1,2,3,4,5], k=1,输出:4

    • 解析:k=1时,平衡数组需所有元素相等,最长平衡子数组长度为1,最少移除4个元素。

  4. 案例4:数组元素全部相等(nums = [3,3,3], k=5)

    • 输入:nums = [3,3,3], k=5,输出:0

    • 解析:最大值=最小值=3,满足3 ≤ 3×5,无需移除元素。

  5. 案例5:大数据量测试(nums = [i for i in range(1, 100001)], k=2)

    • 输出:50000

    • 解析:排序后,最长平衡子数组是[50000, 100000],长度50000,最少移除100000-50000=50000。

5.2 测试验证

将上述案例及题目3个示例代入滑动窗口最优方案,均可得到正确结果。提交LeetCode后,可通过所有测试用例,包括1e5级别的大数据量测试,运行速度稳定。

六、常见坑点与避坑指南

  1. 坑点1:忘记排序,直接暴力枚举子数组

    • 错误表现:无法快速获取子数组最值,只能暴力遍历子数组求min和max,导致超时;

    • 避坑:排序是本题的核心前提,排序后可直接通过首尾指针获取子数组最值,大幅提升效率。

  2. 坑点2:滑动窗口左指针移动逻辑错误

    • 错误表现:当窗口不满足条件时,左指针仅移动一次,而非持续移动到满足条件;

    • 避坑:使用while循环而非if判断,确保左指针移动到“当前右指针能覆盖的最长平衡窗口”的左边界。

  3. 坑点3:忽略边界情况(数组长度为1)

    • 错误表现:数组长度为1时,仍按正常流程计算,返回n - max_len = 1 - 1 = 0(结果正确,但逻辑不严谨);

    • 避坑:单独处理数组长度为1的情况,直接返回0,提升代码可读性和执行效率。

  4. 坑点4:max_len初始值设置错误

    • 错误表现:max_len初始值为0,当所有子数组长度为1时,会导致n - max_len = n - 0 = n(错误);

    • 避坑:max_len初始值必须为1,因为长度为1的数组必为平衡数组,最长平衡子数组长度至少为1。

七、总结与拓展

7.1 解题总结

本题的核心是“转化问题+排序+滑动窗口”:将“最少移除数目”转化为“最长平衡子数组长度”,通过排序简化子数组最值的获取,再用滑动窗口高效找到最长平衡子数组,最终计算答案。

关键要点:

  • 排序是前提:排序后,子数组的最值就是首尾元素,无需额外计算;

  • 滑动窗口是核心:通过双指针维护平衡窗口,确保遍历一次数组即可找到最长平衡子数组,时间复杂度优化至O(n);

  • 边界处理是细节:数组长度为1、所有元素相等、k=1等场景,需单独关注,避免错误。

7.2 拓展思考(提升解题能力)

  • 拓展1:如果数组允许为空,如何修改代码?

    • 思路:当所有元素都无法组成平衡数组(除单个元素),可选择移除所有元素,此时最少移除数目为n;但题目明确要求“不能使其变为空数组”,因此无需考虑此情况。
  • 拓展2:如果k是动态变化的(如每次移除元素后k递减),如何优化?

    • 思路:此时滑动窗口的判断条件会动态变化,需在每次窗口调整后,重新判断是否满足“max ≤ min×k”,复杂度会略有提升,但核心思路仍为排序+滑动窗口。
  • 拓展3:如何输出最少移除元素后的平衡数组?

    • 思路:在滑动窗口遍历过程中,记录最长平衡子数组的left和right指针位置,最终返回nums[left:right+1]即可。

通过本题,可重点掌握“问题转化”和“滑动窗口”两种核心解题思想,这两种思想在数组、字符串类题目中应用广泛(如最长无重复子串、最小覆盖子串等)。建议重点练习滑动窗口的边界处理和移动逻辑,提升算法实战能力。

http://www.jsqmd.com/news/652238/

相关文章:

  • 告别ACE,拥抱muduo:一个Linux C++网络库的诞生与设计哲学
  • 别再死记硬背MobileNet了!手把手带你拆解Depthwise Separable Convolution的计算量与访存瓶颈
  • 机器学习中的决策树
  • 电力系统优化调度:MATLAB代码实现机组组合问题的混合整数线性模型
  • 【学习笔记】深度拆解 Claude Code:12 个可复用的 Agentic Harness 设计模式
  • Dify+本地大模型:构建私有化文件智能问答系统
  • 华中科技大学 计算机组成原理 educoder Logisim平台 存储系统设计实战解析
  • 金融数据获取终极指南:如何使用AKShare免费财经接口库
  • Altium Designer 09实战:5分钟搞定0805贴片电阻3D模型(附规格书参数对照)
  • 从100uA到4uA:RTC纽扣电池电路限流电阻选型实战解析
  • 5分钟掌握HS2-HF_Patch:游戏体验全面升级的完整解决方案
  • 毕业论文不用愁!SpeedAI科研小助手,高效降AIGC首选工具
  • 以太网底层设计原理:从帧结构到全双工演进
  • LaTeX长表格排版进阶:longtable宏包详解与智能续表实战
  • 【华为OD机试真题 新系统】976、黑白棋 | 机试真题+思路参考+代码解析(C++、Java、Py、C语言、JS)
  • 揭秘C程序内存布局奥秘
  • 手把手教你用Chipyard搭建RISC-V SoC:从零配置到FPGA原型验证(基于Gemmini加速器)
  • Unity WebGL发布避坑指南:从内存分配到字体加载,一次搞定所有疑难杂症
  • 别再硬着头皮用CLIP了:手把手教你用候选伪标签(CPL)微调VLM,榨干未标注数据
  • 告别串口助手:手搓一个带进度条和断点续传的STM32 Modbus升级工具(C#实现)
  • 家用插座接线的一点思考
  • 告别默认丑样式!手把手教你用CSS自定义Element-UI表格的滚动条(含横向/纵向完整代码)
  • LeetCode 1653. 使字符串平衡的最少删除次数 详细技术解析
  • Jina AI Reader:让AI轻松理解任何网页内容的智能解决方案
  • AI教材编写绝技:低查重操作方法,让创作不再犯愁!
  • 从IEEE 754标准讲起:手把手带你用位运算‘解剖’一个浮点数(并实现绝对值函数)
  • LabVIEW子VI的模块化设计与高效调用实践
  • LeetCode 239. Sliding Window Maximum 题解
  • FreeRTOS任务创建实战:如何避免Guru Meditation Error和队列断言失败
  • 容器镜像进阶:多阶段构建优化 + 镜像分层缓存策略 + 漏洞扫描自动化