🎯 算法精讲:二分查找(二)—— 变形技巧 🔍

友情提示::本小节含高能代码片段 🥤

  1. 阅读前请确保已掌握基础二分原理与实现
  2. 代码片段可能包含不同程度的变形,请根据实际情况选择阅读

作者:无限大

推荐阅读时间:30min


📚 内容回顾与引言

在上一篇《算法精讲:二分查找(一)—— 基础原理与实现》中,我们剖析了二分查找的核心原理:依赖有序数组的单调性,通过不断折半搜索快速定位目标值 📈。

重点讲解了两大区间定义方式(左闭右闭 [left, right] 与左闭右开 [left, right))及其代码实现细节,并强调了循环不变量原则的重要性。

如果说基础二分是少林长拳,那各种变形就是奇门遁甲!本篇我们将解锁二分查找的 “高阶技能”,解锁二分查找的隐藏皮肤 🧥!从基础的边界值变形(如找第一个/最后一个等于目标值的位置)到复杂场景应用(如旋转数组、珂珂吃香蕉问题)。


🧩 一、常见二分变形及应用场景

1. 查找第一个等于目标值的元素位置

思路:在非递减序列中查找第一个等于目标值的元素。当 arr[mid] == target 时,不立即返回,而是继续向左查找(即 right = mid - 1),以确保找到的是第一个出现的位置

代码实现

/*** 在含重复元素的有序数组中,找到目标值第一次出现的位置* @param nums 有序数组(可重复)* @param target 目标值* @return 首个等于target的索引,未找到返回-1*/
public int findFirstEqual(int[] nums, int target) {int left = 0, right = nums.length - 1;int result = -1; // 初始化结果while (left <= right) {int mid = left + (right - left) / 2; // 防溢出计算中点if (nums[mid] == target) {result = mid;    // 记录位置right = mid - 1; //  👉 关键!继续向左搜索更早出现的目标} else if (nums[mid] < target) {left = mid + 1;  // 目标在右半区} else {right = mid - 1; // 目标在左半区}}return result; // 返回首个位置
}

适用场景:统计数字 4 在数组 [1,2,4,4,4,5] 中的起始位置(返回 2


2. 查找第一个大于等于目标值的元素位置

思路
目标是找到第一个大于或等于目标值的元素。当 arr[mid] >= target 时,缩小右边界(right = mid - 1),否则缩小左边界(left = mid + 1)。循环结束后,直接返回 left,因为它指向第一个大于等于目标值的位置。

代码实现

/*** 找到有序数组中首个≥target的元素位置* @param nums 有序数组* @param target 目标值* @return 首个≥target的索引(若target超最大值返回-1)*/
public int findFirstGreaterOrEqual(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) {right = mid - 1; //  👉 向左收缩,尝试找到更小的满足条件的值} else {left = mid + 1;  // 向右扩大搜索}}// 结束时left指向首个≥target的位置return (left < nums.length) ? left : -1;
}

典型应用:将数字 3 插入有序数组 [1,2,4,5] 的正确位置(返回 2


3. 查找第一个大于目标值的元素位置

思路
寻找第一个大于目标值的元素。当 arr[mid] <= target 时,缩小左边界(left = mid + 1),否则缩小右边界(right = mid - 1)。循环结束后,left 指向第一个大于目标值的元素。

/*** 找到有序数组中首个>target的元素位置* @param nums 有序数组* @param target 目标值* @return 首个>target的索引(若target超最大值返回-1)*/
public int findFirstGreater(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) { //  👉 注意条件包含等于left = mid + 1;  // 目标在右半区} else {right = mid - 1; // 向左收缩}}return (left < nums.length) ? left : -1;
}

💡 关键点:当 nums[mid] == target 时仍需右移,确保定位到严格大于的位置


4. 查找最后一个等于目标值的元素位置

思路
在非递减序列中查找最后一个等于目标值的元素。当 arr[mid] == target 时,不立即返回,而是继续向右查找(即 left = mid + 1),以确保找到的是最后一个出现的位置。循环结束后,检查 right 是否越界以及 arr[right] 是否等于目标值。

/*** 在含重复元素的有序数组中,找到目标值最后一次出现的位置* @param nums 有序数组* @param target 目标值* @return 最后一个等于target的索引,未找到返回-1*/
public int findLastEqual(int[] nums, int target) {int left = 0, right = nums.length - 1;int result = -1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {result = mid;   // 记录位置left = mid + 1; //  👉 关键!继续向右搜索更晚出现的目标} else if (nums[mid] < target) {left = mid + 1; // 目标在右半区} else {right = mid - 1;// 目标在左半区}}return result;
}

用例:确定 4[1,2,4,4,4,5] 中的结束位置(返回 4


5. 查找最后一个小于等于目标值的元素位置

思路
目标是找到最后一个小于等于目标值的元素。当 arr[mid] <= target 时,缩小左边界(left = mid + 1),否则缩小右边界(right = mid - 1)。循环结束后,right 指向最后一个小于等于目标值的元素。

/*** 找到有序数组中最后一个≤target的元素位置* @param nums 有序数组* @param target 目标值* @return 最后一个≤target的索引(若target小于最小值返回-1)*/
public int findLastLessOrEqual(int[] nums, int target) {int left = 0, right = nums.length - 1;int result = -1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) {result = mid;     // 记录可能位置left = mid + 1;   // 👉 向右尝试找到更大的满足值} else {right = mid - 1;  // 目标在左半区}}return result;
}

💡 应用场景:统计考试成绩 ≤80 分的最高分学生位置


6. 查找最后一个小于目标值的元素位置

思路
寻找最后一个小于目标值的元素。当 arr[mid] < target 时,缩小左边界(left = mid + 1),否则缩小右边界(right = mid - 1)。循环结束后,right 指向最后一个小于目标值的元素。

代码实现

/*** 找到有序数组中最后一个<target的元素位置* @param nums 有序数组* @param target 目标值* @return 最后一个<target的索引(若target小于最小值返回-1)*/
public int findLastLess(int[] nums, int target) {int left = 0, right = nums.length - 1;int result = -1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {result = mid;     // 记录位置left = mid + 1;   //  👉 向右尝试找到更大的满足值} else {right = mid - 1;  // 目标在左半区}}return result;
}

典型场景:在 [10,20,30,40] 中找 <35 的最后位置(返回 2


六种核心变形对比

变形类型 🔍核心特点 ➕典型应用场景 ️
查找第一个等于目标值找到目标后继续向左搜索 👈统计元素出现次数、去重操作
查找第一个大于等于目标值不记录结果,循环后通过 left 返回插入排序位置、边界判断
查找第一个大于目标值严格大于目标值的第一个位置区间划分、排名统计
查找最后一个等于目标值找到目标后继续向右搜索 👉确定重复元素的结束位置
查找最后一个小于等于目标值找到最后一个满足条件的位置分页查询、阈值判断
查找最后一个小于目标值严格小于目标值的最后位置排名统计、区间边界确定

💡 核心区别:普通二分找到目标即返回,而变形算法会继续向特定方向收缩边界,确保定位到最左/最右的目标值


⚠️ 边界处理避坑指南

二分变形代码的 “魔鬼在细节”,90%的 bug 源于边界处理不当!以下是四大核心避坑点:

1. 初始边界设置原则
  • 右边界取值
    • right = length - 1 ➜ 闭区间搜索(如普通二分)
    • right = length ➜ 开区间搜索(如插入位置问题),防止漏检末尾元素

示例

// 闭区间:搜索范围[0, length-1]
int right = nums.length - 1;
// 开区间:搜索范围[0, length)
int right = nums.length;
2. 循环条件选择策略
条件适用场景风险点
while (left <= right)需精确匹配目标值(如 findFirstEqual易死循环(需设退出条件)
while (left < right)找边界位置(如 findFirstGreater可能漏检单元素区间

黄金法则

  • <=必须配合 left=mid+1/right=mid-1
  • <必须配合 left=mid+1/right=mid
3. 越界处理技巧
  • 返回前必校验

    // 检查left是否越界(开区间场景)
    return (left < nums.length) ? left : -1;
    
  • 防 off-by-one 错误

    • left=0right=length-1时,mid计算需防溢出 ➜ mid = left + (right - left) / 2

    • 结束时验证结果有效性:

      // 查找类需验证找到的是否为目标值
      if (left >= nums.length || nums[left] != target) return -1;
      

温馨提示

在编程中,off-by-one(差一错误) 指因边界处理偏差导致的逻辑错误,通常表现为循环、索引或区间计算中意外地少或多一次操作。这种错误在二分查找中尤为常见,可能导致死循环、漏检或越界崩溃。

4. 重复元素特判

当数组含重复值时,额外添加分支:

// 旋转数组场景(解决 [3,1,2,3,3,3,3] 类问题)
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {left++;   // 跳过左侧重复right--;  // 跳过右侧重复
}

💡 边界心法口诀

初值定区间 → 循环控方向 → 退出验边界 → 重复要特判

掌握此四步,二分无坑! 🔥


🚀 二、二分变形之魂:LeetCode 实战 ️

光说不练假把式,来点硬菜!

案例 1:珂珂吃香蕉(LeetCode 875)

题目:传送门

场景:珂珂面前堆着 N堆香蕉,警卫 H小时后回来。她每小时只能选一堆吃 K根,吃不完藏枕头下。求最小吃速 K让她优雅吃完蕉。

场景翻译
当你妈出门时说「H 小时后回来」👩,而你要偷吃掉 N 堆香蕉 🍌…每堆数量随机!每小时只能选一堆吃 K 根(不够还得藏枕头下),求最小吃速 K避免被打 👋

解题思路拆解

1.为什么用二分

  • 吃速 K存在临界点:小于它吃不完(太淑女),大于它浪费(变饭桶)

  • 暴力枚举会超时 → 二分搜索完美匹配"找最小可行解"场景

    2.灵魂三问

  • 搜索区间:[1, 最大香蕉堆](吃速至少为 1,最大不超过最大堆)

  • 判断条件:当前吃速mid能否在H小时内吃完

  • 边界收缩:能吃完就压榨吃速(right=mid-1),吃不完就加速(left=mid+1

二分妙用

public int minEatingSpeed(int[] piles, int h) {int left = 1; // 吃速下限:淑女的矜持(至少1根/小时)int right = 0;for (int pile : piles) right = Math.max(right, pile); //  👉 最大堆即吃速上限// 🔥 二分奥义:在[1,最大堆]区间反复横跳测试,看看哪个速度能吃完香蕉while (left <= right) {int mid = left + (right - left) / 2; // mid=当前测试吃速if (canFinish(piles, mid, h)) {right = mid - 1; //  🙆♀️ 能吃完?再压榨下吃速!} else {left = mid + 1; //  🙅♂️ 吃不完?加速干饭!}}return left; // 返回最小吃速K
}private boolean canFinish(int[] piles, int speed, int h) {int hoursNeeded = 0;for (int pile : piles) {// ✨ 骚操作:整数除法向上取整(pile+speed-1)相当于数学ceil()hoursNeeded += (pile + speed - 1) / speed;if (hoursNeeded > h) return false; //  ⌛ 超时警告}return true;
}

关键点

1.mid 是当前测试的吃香蕉速度

2.canEatAll 返回 true → 还能压榨更小 K!(收缩右边界)

3.搜索区间右边界初始化为 max(piles)而非固定值,更通用

关键技巧

K太小
K太大
K刚好
妈妈出门
测试吃速K
吃不完挨打
浪费香蕉被骂
优雅吃蕉保平安
增加K
减少K
成功

🌀 案例 2:旋转数组搜索(LeetCode 33)🎡

题目:传送门

场景:数组 [0,1,2,4,5,6,7]旋转后变 [4,5,6,7,0,1,2],如何在旋转数组中搜 target

破局点旋转后必有一半有序!记住这个黄金定律

代码的艺术

public int search(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) return mid; //   欧皇附体// 左半边有序情景 [4,5,6,7,...]if (nums[left] <= nums[mid]) {if (nums[left] <= target && target < nums[mid]) {right = mid - 1; //  👈 target在有序左半区} else {left = mid + 1; //  👉 目标在混乱右半区}}// 右半边有序情景 [...,0,1,2]else {if (nums[mid] < target && target <= nums[right]) {left = mid + 1; // 👉 target在有序右半区} else {right = mid - 1; // 👈 目标在混乱左半区}}}return -1; //  😭 非酋结局
}

精髓

  • 先判断哪半边有序 🔍
  • 再判断 target 是否在有序区间内

避坑指南

  • 比较时用<=而不是<,处理重复元素边界

  • 判断有序区间时,nums[left] <= nums[mid]中的=处理单元素情况

  • 乱中有序,二分永不为奴


💎 三、二分终极奥义:抽象建模大法 🌌

你以为二分只能搜数组?Noooo,格局打开!凡是能分成两段性的问题,皆可二分!

万能二分模板(背会秒杀 80%题)✍️

while (left <= right) {int mid = left + (right - left) / 2; // 防溢出if (condition(mid)) {right = mid - 1; // 向左侧探索} else {left = mid + 1; // 向右侧探索}
}
return left; // 魔法值:可能是解/插入位置

四大抽象场景总结 🧩

问题类型二分对象判断条件典型案例
找具体值数组索引arr[mid] == target经典二分查找 ✅
找最值解答案值本身是否满足极值条件珂珂吃香蕉
分段函数求极值函数参数函数增减性变化点寻找峰值(LeetCode 162)📈
隐式数学解数学解空间解的存在性平方根(LeetCode 69)➗

🏁 四、课后作业大礼包 📚

1. 基础篇34. 在排序数组中查找元素的第一个和最后一个位置

题目

给定升序数组`nums`和目标值`target`,返回`target`的起始和终止位置,不存在则返回`[-1,-1]`  
示例:  
输入:`nums = [5,7,7,8,8,10], target = 8`  
输出:`[3,4]`

通关秘籍

  • 组合拳:findFirstEqual + findLastEqual 双剑合璧
  • 注意处理target不存在时的边界检查

2. 进阶篇162. 寻找峰值

烧脑点

  • 数组未经排序 → 利用相邻元素比较确定趋势
  • 关键代码:
if (nums[mid] < nums[mid + 1]) {left = mid + 1; //  📈 上升趋势,峰值在右
} else {right = mid; // 📉 下降趋势,峰值在左
}

3. 地狱篇410. 分割数组的最大值

终极奥义

while (left <= right) {int mid = (left + right) / 2;if (canSplit(nums, mid, m)) { // 判断是否能分成m段且每段和<=midright = mid - 1; // 能分割,尝试更小的最大值} else {left = mid + 1; // 不能分割,增大最大值}
}
return left; // 最小化最大分段和

💡 多语言提示

  • Python:mid = (left + right) // 2(注意整数除法)。
  • C++:使用mid = left + (right - left) / 2防溢出。

灵魂画手

数组: [7,2,5,10,8]  m=2
最小最大和:18 →  [7,2,5] 和 [10,8]

本篇关键收获

  • 变形本质:普通二分找到即返回,变形需向特定方向收缩边界(左/右)。

  • 抽象心法:凡问题具两段性(如可行/不可行分界),皆可二分。

  • 必记技巧:防溢出中点计算、循环不变量维护。

无限大忠告:把代码复制到 IDE,开启调试模式观察边界变化

遇到 bug 时:

  1. 喝口水 ☕

  2. 画边界图 📊

  3. 默念"循环不变量"三遍 🧘

  4. 点这里看解法(别真点,自己思考!)

  5. 欢迎各位在评论区分享你的解题思路!

(未完待续:下一篇预告《二分的时空幻术——复杂度与优化篇》)🔥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91263.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91263.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91263.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

两个程序配合实现了基于共享内存和信号量的进程间通信,具体说明如下:

第一个程序&#xff1a;共享内存读取程序&#xff08;消费者&#xff09;该程序作为消费者&#xff0c;从共享内存中读取数据&#xff0c;通过信号量保证只有当生产者写入数据后才能读取。/*4 - 读共享内存*/ #include<stdio.h> // 标准输入输出库 #inc…

JeecgBoot(1):前后台环境搭建

1 项目介绍 JeecgBoot 是一款基于 Java 的 AI 低代码平台&#xff0c;它采用了 SpringBoot、SpringCloud、Ant Design Vue3、Mybatis 等技术栈&#xff0c;并集成了代码生成器、AI 对话助手、AI 建表、AI 写文章等功能。JeecgBoot 的设计宗旨是实现简单功能零代码开发&#xf…

Nestjs框架: 关于 OOP / FP / FRP 编程

概述 在软件开发过程中&#xff0c;不同的编程范式为我们提供了多样化的思维方式与实现路径它们不仅影响着代码的结构和逻辑组织方式&#xff0c;也深刻影响着项目的可维护性、可扩展性以及团队协作效率 什么是 OOP、FP 和 FRP&#xff1f;首先从三个术语的含义入手 1 &#xf…

elememtor 添加分页功能

各位看官好&#xff0c;最近在忙着使用elementor搭建自己的网站&#xff0c;由于我不是专业的程序员和前端&#xff0c;又没有很多钱去找外包公司实现自己的设计&#xff0c;所以选择了elementor. 总的来说这是一个不错的wordpress 插件&#xff0c;也让我们这种非专业的网站设…

关于“PromptPilot” 之2 -目标系统:Prompt构造器

目标系统&#xff1a;Prompt构造器想法首先&#xff0c;在抽象层对PromptPilot进行封装给出提示词形成过程的全部环节。然后&#xff0c;在 形成一套确定的提示词后再为 小规模试点方案生成一整套开发工具并配套集成开发环境和指南。最后&#xff0c;在小规模试点成功后进行拓展…

短剧小程序系统开发:重塑影视内容消费格局

在数字化浪潮的推动下&#xff0c;影视内容消费正经历着深刻的变革。短剧小程序系统开发作为这一变革的重要力量&#xff0c;正在重塑影视内容消费的格局&#xff0c;为用户带来更加个性化、便捷化的观影体验。传统影视内容消费往往受到时间和空间的限制&#xff0c;用户需要前…

一文掌握最新版本Monocle3单细胞轨迹(拟时序)分析

许多大佬的软件想要构建一个大而美的生态&#xff0c;从 monocle2 开始就能做单细胞的质控、降维、分群、注释这一系列的分析&#xff0c;但不幸的是我们只知道 monocle 系列还是主要做拟时序分析&#xff0c;一方面是因为 Seurat 有先发优势&#xff0c;出名要趁早&#xff0c…

spark入门-helloword

我们学习编程语言的时候&#xff0c;第一个程序就是打印一下 “hello world” &#xff0c;对于大数据领域的第一个任务则是wordcount。那我们就开始我们的第一个spark任务吧&#xff01; 下载spark 官方下载地址&#xff1a;Apache Download Mirrors 下载完毕以后&#xff0c…

雷达系统设计学习:自制6GHz FMCW Radar

国外大神自制6GHZ FMCW Radar开源项目: https://github.com/Ttl/fmcw3 引言 之前我做过一个简单的调频连续波&#xff08;FMCW&#xff09;雷达&#xff0c;能够探测到100米范围内人体大小的物体。虽然它确实能用&#xff0c;但由于预算有限&#xff0c;还有很大的改进空间。 …

系统选择菜单(ubuntu grub)介绍

好的&#xff0c;我们来详细解释一下什么是Ubuntu的GRUB菜单。 简单来说&#xff0c;GRUB菜单是您电脑启动时看到的第一个交互界面&#xff0c;它就像一个“系统选择”菜单&#xff0c;让您决定接下来要启动哪个操作系统或进入哪种模式。详细解释 1. GRUB是什么&#xff1f; GR…

方案C,version2

实现一个简单的Helloworld网页,并通过GitHub Actions自动构建并推送到公开仓库的gh-pages分支。同时,使用PAT进行认证,确保源码在私有仓库中,构建后的静态文件在公开仓库中。 重新设计deploy.yml内容如下(针对纯静态文件,无需构建过程): 步骤: 检出私有仓库源码。 由于…

R 语言科研绘图 --- 其他绘图-汇总1

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

webpack 原理及使用

【点赞收藏加关注,前端技术不迷路~】 一、webpack基础 1.核心概念 1)entry:定义入口,webpack构建的第一步 module.exports ={entry:./src/xxx.js } 2)output:出口(输出) 3)loader:模块加载器,用户将模块的原内容按照需求加载成新内容 比如文本加载器raw-loade…

「日拱一码」039 机器学习-训练时间VS精确度

目录 时间-精度权衡曲线&#xff08;不同模型复杂度&#xff09; 训练与验证损失对比 帕累托前沿分析&#xff08;3D&#xff09; 在机器学习实践中&#xff0c;理解模型收敛所需时间及其与精度的关系至关重要。下面介绍如何分析模型收敛时间与精度之间的权衡&#xff0c;并…

面试刷题平台项目总结

项目简介&#xff1a; 面试刷题平台是一款基于 Spring Boot Redis MySQL Elasticsearch 的 面试刷题平台&#xff0c;运用 Druid HotKey Sa-Token Sentinel 提高了系统的性能和安全性。 第一阶段&#xff0c;开发基础的刷题平台&#xff0c;带大家熟悉项目开发流程&#xff…

负载均衡、算法/策略

负载均衡一、负载均衡层级对比特性四层负载均衡 (L4)七层负载均衡 (L7)工作层级传输层 (TCP/UDP)应用层 (HTTP/HTTPS等)决策依据源/目标IP端口URL路径、Header、Cookie、内容等转发方式IP地址/端口替换重建连接并深度解析报文性能更高吞吐量&#xff0c;更低延迟需内容解析&…

StackingClassifier参数详解与示例

StackingClassifier参数详解与示例 StackingClassifier是一种集成学习方法&#xff0c;通过组合多个基分类器的预测结果作为元分类器的输入特征&#xff0c;从而提高整体模型性能。以下是关键参数的详细说明和示例&#xff1a; 1. classifiers&#xff08;基分类器&#xff09;…

嵌入式中间件-uorb解析

uORB系统详细解析 1. 系统概述 1.1 设计理念 uORB&#xff08;Micro Object Request Broker&#xff09;是一个专为嵌入式实时系统设计的发布-订阅式进程间通信框架。该系统借鉴了ROS中topic的概念&#xff0c;为无人机飞控系统提供了高效、可靠的数据传输机制。 1.2 核心特征 …

HTTP.Client 库对比与选择

HTTP.Client 库对比与选择在 Python 中&#xff0c;除了标准库 http.client&#xff0c;还有许多功能更强大、使用更便捷的 HTTP 库。以下是一些常用的库及其特点&#xff1a;1. Requests&#xff08;最流行&#xff09;特点&#xff1a;高层 API&#xff0c;简单易用&#xff…

RabbitMQ面试精讲 Day 5:Virtual Host与权限控制

【RabbitMQ面试精讲 Day 5】Virtual Host与权限控制 开篇 欢迎来到"RabbitMQ面试精讲"系列的第5天&#xff01;今天我们将深入探讨RabbitMQ中Virtual Host与权限控制的核心机制&#xff0c;这是构建安全、隔离的消息系统必须掌握的重要知识。在面试中&#xff0c;面…