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

二分查找专题总结:从数组越界到掌握“两段性“

二分查找专题总结:从数组越界到掌握"两段性"

在这里插入图片描述

目录

  • 刷题记录
  • 刷题过程
  • 二分查找的核心概念
    • 二分查找的前提
    • 两种二分模板
  • 我踩的坑总结
    • 坑1:while条件搞错
    • 坑2:mid计算整型溢出
    • 坑3:搜索条件写错
    • 坑4:数组越界
    • 坑5:边界判断遗漏
  • 典型题目分类
    • 类型1:标准二分
    • 类型2:边界二分
    • 类型3:旋转数组
  • 二分查找通用框架
  • 我的理解

刷题记录

  • 刷题周期: 3天深度理解(10.10 - 10.12)
  • 完成题量: 9题全部AC
  • 题目分布: 标准二分3题 + 边界二分3题 + 旋转数组3题

刷题过程

Day09(10.10): 标准二分(3题)

  • LeetCode 704 - 二分查找
  • LeetCode 35 - 搜索插入位置
  • LeetCode 69 - x的平方根
  • 第一题就错了5次!主要是理解 while(left <= right) 的必要性

Day10(10.11): 边界二分(3题)

  • LeetCode 34 - 查找第一个和最后一个位置
  • LeetCode 852 - 山脉数组的峰顶
  • LeetCode 162 - 寻找峰值
  • 核心突破:"找第一个"和"找最后一个"的模板区别(向下/向上取整)

Day11(10.12): 旋转数组(3题)⭐⭐⭐

  • LeetCode 33 - 搜索旋转排序数组(错了4次,花了近2小时)
  • LeetCode 153 - 寻找旋转排序数组中的最小值(10分钟AC!)
  • LeetCode 81 - 搜索旋转排序数组 II(有重复元素)
  • 第一题死磕"两段性"概念,后续两题秒杀!

难度曲线:

  • 标准二分:基础模板(3题)
  • 边界二分:理解取整方向(3题)
  • 旋转数组:理解"两段性"(3题,最难但最有价值)

二分查找的核心概念(我的理解)

什么是二分查找?

我的理解就是:在有序数组中,每次排除一半的搜索空间,从而快速找到目标值。

关键是要搞清楚:

  • 什么时候能用二分?(有序 or “两段性”)
  • left 和 right 怎么初始化?
  • 什么时候用 left <= right?什么时候用 left < right
  • mid 怎么算?什么时候 +1

二分查找的两种模板

通过这8道题,我发现二分查找主要有2种:

1. 标准二分(查找某个值)

特点: 找到了就返回,找不到返回-1

循环条件: while(left <= right) ⚠️ 必须有等号!

模板:

int left = 0, right = n - 1;
while(left <= right) {  // 注意:<=int mid = left + (right - left) / 2;if(nums[mid] < target) {left = mid + 1;} else if(nums[mid] > target) {right = mid - 1;} else {return mid;  // 找到了}
}
return -1;  // 没找到

我做过的题:

  • LeetCode 704 - 二分查找
  • LeetCode 69 - x的平方根
2. 边界二分(找第一个/最后一个)

特点: 找边界位置(第一个/最后一个满足条件的)

循环条件: while(left < right) ⚠️ 没有等号!

两种情况:

找第一个满足条件的(左边界):

int left = 0, right = n - 1;
while(left < right) {  // 注意:<int mid = left + (right - left) / 2;  // 向下取整if(nums[mid] < target) {left = mid + 1;} else {right = mid;  // 不是mid-1!}
}
return left;  // 或right,此时left==right

找最后一个满足条件的(右边界):

int left = 0, right = n - 1;
while(left < right) {int mid = left + (right - left + 1) / 2;  // 向上取整⚠️if(nums[mid] > target) {right = mid - 1;} else {left = mid;  // 不是mid+1!}
}
return left;

我做过的题:

  • LeetCode 35 - 搜索插入位置
  • LeetCode 34 - 查找元素第一个和最后一个位置
  • LeetCode 852 - 山脉数组的峰顶索引
  • LeetCode 162 - 寻找峰值

典型题目分类

类型1:标准二分

LeetCode 704. 二分查找

这是我二分的第一题,结果错了很多次!

我的第一次错误:

while(left < right) {  // ❌ 单元素数组进不去循环!int mid = left + (right - left) / 2;if(nums[mid] < target) {left = mid + 1;} else if(nums[mid] > target) {right = mid - 1;} else {return mid;}
}
return -1;

测试用例: nums = [5], target = 5
我的输出: -1
预期输出: 0

问题分析:

  • 当数组只有一个元素时,left = 0, right = 0
  • while(left < right)0 < 0 → false,根本进不去循环!
  • 直接返回-1了

正确写法:

while(left <= right) {  // ✅ 加上等号// ...
}

深入理解:

为什么标准二分要用 <=

情况1:数组有1个元素 [5]
left = 0, right = 0
- while(left < right)  → 0 < 0 → false ❌ 不进循环
- while(left <= right) → 0 <= 0 → true ✅ 进循环情况2:数组有2个元素 [3, 5],找5
第1轮:left = 0, right = 1, mid = 0nums[0] = 3 < 5 → left = 1
第2轮:left = 1, right = 1
- while(left < right)  → 1 < 1 → false ❌ 不进循环
- while(left <= right) → 1 <= 1 → true ✅ 进循环,找到5

教训:标准二分必须用 <=,因为最后可能left==right还需要判断!

这个错误让我理解了2小时!


LeetCode 35. 搜索插入位置

我的第一次错误:

if(nums[left] < target) {  // ❌ 应该是nums[mid]!left = mid + 1;
}

又犯错了! 我把 nums[left]nums[mid] 搞混了!

应该是:

if(nums[mid] < target) {  // ✅ 是mid不是leftleft = mid + 1;
}

教训:在二分中,判断条件用的是 nums[mid],不是 nums[left]nums[right]


LeetCode 69. x的平方根

我的第一次错误:整数溢出!

long long mid = left + (right - left + 1) / 2;  // ❌

运行错误:

Line 9: Char 48: runtime error: signed integer overflow

问题分析:

  • right - left + 1 很大时,比如 right = INT_MAX, left = 0
  • right - left + 1 超过了 INT_MAX,发生溢出!

正确写法:

long long mid = left + ((long long)right - left + 1) / 2;  // ✅

关键: 要先把 right 强制转换成 long long,然后再减!

教训:涉及大数的二分,mid的计算要注意类型转换!


类型2:边界二分

LeetCode 34. 查找元素第一个和最后一个位置

这题要找两个边界:第一个和最后一个。

找第一个满足的(左边界):

// 找第一个 >= target 的位置
while(left < right) {  // 注意:<int mid = left + (right - left) / 2;  // 向下取整if(nums[mid] < target) {left = mid + 1;} else {  // nums[mid] >= targetright = mid;  // 不要-1!}
}

找最后一个满足的(右边界):

// 找最后一个 <= target 的位置
while(left < right) {int mid = left + (right - left + 1) / 2;  // 向上取整⚠️if(nums[mid] > target) {right = mid - 1;} else {  // nums[mid] <= targetleft = mid;  // 不要+1!}
}

为什么要向上取整?

假设:[5, 7, 7, 7, 8],找最后一个7
left = 1, right = 3如果向下取整:
mid = 1 + (3 - 1) / 2 = 2
nums[2] = 7 <= 7 → left = mid = 2
下一轮:left = 2, right = 3, mid = 2
nums[2] = 7 <= 7 → left = mid = 2
死循环了!❌如果向上取整:
mid = 1 + (3 - 1 + 1) / 2 = 3
nums[3] = 7 <= 7 → left = mid = 3
下一轮:left = 3, right = 3 → 退出循环 ✅

教训:找最后一个时,mid要向上取整,否则会死循环!


LeetCode 162. 寻找峰值

这题AC得很快,用时10分钟,因为前面的题做多了。

核心思路: 峰值左边递增,右边递减,具有"两段性"

while(left < right) {int mid = left + (right - left) / 2;if(nums[mid] < nums[mid + 1]) {left = mid + 1;  // 峰值在右边} else {right = mid;  // mid可能就是峰值}
}
return left;

为什么AC这么快? 因为:

  1. 前面做了3题边界二分,模板熟了
  2. 理解了"两段性"的概念
  3. 没有犯"索引vs值"的错误

类型3:旋转数组(难点)

LeetCode 33. 搜索旋转排序数组 ⭐⭐⭐⭐⭐

这题是二分的BOSS题!我错了4次,花了近2小时!

什么是旋转数组?

原数组:[0, 1, 2, 4, 5, 6, 7]
旋转后:[4, 5, 6, 7, 0, 1, 2]↑旋转点(最大值)

我的第一次尝试(完全错了):

// 找旋转点
while(left < right) {int mid = left + (right - left + 1) / 2;if(nums[mid] >= nums[mid + 1]) {  // ❌ 数组越界!return mid;}// ...
}

错误1:数组越界

  • mid = n - 1 时,nums[mid + 1] 访问了 nums[n],越界了!

错误2:找的不是"最大值"而是"最后一个递增的"


我的第二次尝试(还是错):

while(left < right) {int mid = left + (right - left + 1) / 2;if(nums[mid] > nums[right]) {  // 比较rightleft = mid;} else {right = mid - 1;}
}
// 找到peak后,没检查peak本身!

错误3:特殊情况没处理

  • 空数组 → 没返回
  • 单元素数组 → left和right越界
  • target就是peak → 没检查

错误4:边界检查不完整

if(nums[peak] < target && nums[n] < target) {return -1;
}

这个逻辑是错的!应该是:

if(nums[peak] < target || nums[n] >= target) {return -1;
}

第三次、第四次也错了… 每次都是边界条件问题。

最后终于AC了,但我意识到这题的核心不是代码,而是理解"两段性"

什么是"两段性"?

老师说的"两段性"不是指数组有两段,而是:数组能按照某个性质分成"满足"和"不满足"两部分。

旋转数组的"两段性":

[4, 5, 6, 7, | 0, 1, 2]满足        | 不满足性质:nums[i] > nums[n-1]
- 左半部分都 > nums[n-1](满足)
- 右半部分都 <= nums[n-1](不满足)

核心代码:

while(left < right) {int mid = left + (right - left + 1) / 2;if(nums[mid] > nums[n - 1]) {  // 和最后一个比!left = mid;  // peak在右边(包括mid)} else {right = mid - 1;  // peak在左边}
}

为什么和 nums[n-1] 比?

因为这样能保证"连续的两段性":

  • 左边的数都比 nums[n-1]
  • 右边的数都比 nums[n-1] 小或等于
  • 中间没有"跳来跳去"的

教训:旋转数组的核心是找到"两段性"的判断标准!


找到peak后还要干什么?

// 1. 检查peak本身
if(nums[peak] == target) return peak;// 2. 判断target在左半还是右半
if(target > nums[peak] || target < nums[n - 1]) {return -1;  // 不在数组范围内
}// 3. 在相应区间二分查找
if(target <= nums[peak] && target >= nums[0]) {// 在左半 [0, peak]
} else {// 在右半 [peak + 1, n - 1]
}

完整流程:

  1. 找旋转点(最大值)
  2. 判断边界情况
  3. 判断target在哪个区间
  4. 在相应区间二分查找

这题给我的启发:

做完这题,我做LeetCode 153(找旋转数组最小值)只用了10分钟就AC了!

为什么?因为:

  1. 理解了"两段性"
  2. 知道要和 nums[n-1] 比较
  3. 边界情况处理清楚了

前面花2小时踩坑,后面10分钟AC,这就是深度学习的价值!


LeetCode 153. 寻找旋转排序数组中的最小值 ⭐⭐⭐

题目: 找到旋转数组中的最小值。

我的思路: 找峰顶,最小值 = 峰顶的下一个。

我的代码(10分53秒,一次AC!):

int findMin(vector<int>& nums) {int left = 0, right = nums.size() - 1, n = nums.size();// 找最大值(峰顶)while(left < right) {int mid = left + (right - left + 1) / 2;if(nums[mid] > nums[n - 1]) left = mid;else right = mid - 1;}// 处理边界if(nums.size() == 0 || nums.size() == 1) return nums[left];if(nums[left] < nums[n - 1]) return nums[left];  // 未旋转return nums[left + 1];  // 最小值 = 峰顶的下一个
}

心得: 直接套用LeetCode 33的框架!提速10倍! 🚀


LeetCode 81. 搜索旋转排序数组 II ⭐⭐⭐(有重复元素)

核心难点: 重复元素破坏了"两段性"!

为什么有重复就不能用"两次二分"?

无重复(LeetCode 33):

[4, 5, 6, 7, 0, 1, 2]
nums[mid] > nums[n] → 左边有峰值 ✅
nums[mid] < nums[n] → 右边有峰值 ✅

有重复(LeetCode 81):

[1, 0, 1, 1, 1]
nums[mid] == nums[n] → 不知道在哪边!❌

解决方案: 当无法判断时,只能 left++, right-- 线性缩小。

我的AC代码:

bool search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while(left <= right) {int mid = left + (right - left) / 2;if(nums[mid] == target) return true;// 关键:处理重复元素if(nums[left] == nums[mid] && nums[mid] == nums[right]) {left++;right--;}// 左半边有序else if(nums[left] <= nums[mid]) {if(nums[left] <= target && target < nums[mid])right = mid - 1;elseleft = mid + 1;}// 右半边有序else {if(nums[mid] < target && target <= nums[right])left = mid + 1;elseright = mid - 1;}}return false;
}

时间复杂度: 平均 O(log n),最坏 O(n)(如 [1,1,1,1,1]


我踩的坑(总结)

坑1:while(left <= right) vs while(left < right)

标准二分:<=

while(left <= right) {if(nums[mid] == target) return mid;else if(...) left = mid + 1;else right = mid - 1;
}
return -1;

边界二分:<

while(left < right) {if(...) left = mid + 1;else right = mid;  // 不要-1
}
return left;

区别:

  • 标准二分找到就返回,所以可以 left == right 时再判断一次
  • 边界二分要找边界,当 left == right 时就是答案了

坑2:向上取整 vs 向下取整

找第一个: 向下取整

int mid = left + (right - left) / 2;

找最后一个: 向上取整

int mid = left + (right - left + 1) / 2;

原因: 避免死循环

left = 2, right = 3向下:mid = 2
如果 left = mid,下一轮还是 left = 2, right = 3 → 死循环向上:mid = 3
如果 left = mid,下一轮 left = 3, right = 3 → 退出

坑3:整数溢出

// ❌ 错误
long long mid = left + (right - left + 1) / 2;// ✅ 正确
long long mid = left + ((long long)right - left + 1) / 2;

原因: right - left + 1 可能超过INT_MAX


坑4:数组越界

// ❌ 错误
if(nums[mid] >= nums[mid + 1]) {  // mid可能是n-1// ...
}// ✅ 正确:和固定位置比
if(nums[mid] > nums[n - 1]) {// ...
}

坑5:判断条件用错变量

// ❌ 错误
if(nums[left] < target) {  // 应该是midleft = mid + 1;
}// ✅ 正确
if(nums[mid] < target) {left = mid + 1;
}

坑6:旋转数组的"两段性"理解错误

错误理解: 以为要和 nums[mid+1]nums[0] 比较

正确理解:nums[n-1] 比较,这样能保证"连续的两段性"

// ✅ 正确
if(nums[mid] > nums[n - 1]) {left = mid;
} else {right = mid - 1;
}

我的薄弱环节

  • 旋转数组的理解:虽然AC了,但还需要再做一遍加深理解
  • 边界情况的处理:空数组、单元素、无旋转等特殊情况
  • "两段性"的识别:什么时候能用二分,需要多练

下一步计划

  • 二分查找基础已经掌握,模板记住了
  • 旋转数组要再复习(尤其是"两段性"的理解)
  • 后面可能有其他二分变种(比如答案二分),继续学习

典型模板总结

标准二分模板

int left = 0, right = n - 1;
while(left <= right) {  // <=int mid = left + (right - left) / 2;if(nums[mid] == target) {return mid;} else if(nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}
}
return -1;

边界二分模板(找第一个)

int left = 0, right = n - 1;
while(left < right) {  // <int mid = left + (right - left) / 2;  // 向下if(nums[mid] < target) {left = mid + 1;} else {right = mid;  // 不要-1}
}
return left;

边界二分模板(找最后一个)

int left = 0, right = n - 1;
while(left < right) {int mid = left + (right - left + 1) / 2;  // 向上⚠️if(nums[mid] > target) {right = mid - 1;} else {left = mid;  // 不要+1}
}
return left;

旋转数组模板(找最大值/旋转点)

int left = 0, right = n - 1, n = nums.size();
while(left < right) {int mid = left + (right - left + 1) / 2;if(nums[mid] > nums[n - 1]) {  // 和最后一个比!left = mid;  // 旋转点在右边(包括mid)} else {right = mid - 1;  // 旋转点在左边}
}
return left;  // 或right

我的理解

二分查找的本质:

  • 每次排除一半搜索空间
  • 时间复杂度:O(log n)
  • 前提:数组有序 或 具有"两段性"

"两段性"的理解:

  • 不是指数组有两段
  • 而是指:数组能按某个性质分成"满足"和"不满足"两部分
  • 关键是找到这个"性质"

两种模板的选择:

  1. 标准二分: 找具体值 → while(left <= right),找到返回
  2. 边界二分: 找边界 → while(left < right),最后返回left

mid的计算:

  • 找第一个:向下取整 (right - left) / 2
  • 找最后一个:向上取整 (right - left + 1) / 2
  • 原因:避免死循环

什么时候复习:

  • 旋转数组要再做一遍(LeetCode 33、153、81)
  • 边界二分多练几题
  • 答案二分还没学,后面继续

总结于: 2025年10月14日
相关专题: 排序、前缀和

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

相关文章:

  • aws ec2防ssh爆破, aws服务器加固, 亚马逊服务器ssh安全,防止ip扫描ssh。 aws安装fail2ban, ec2配置fail2ban
  • F024 CNN+vue+flask电影推荐系统vue+python+mysql+CNN实现
  • 谷歌生成在线网站地图买外链网站
  • Redis Key的设计
  • Redis 的原子性操作
  • 竹子建站免费版七牛云cdn加速wordpress
  • python进阶_Day8
  • 在React中如何应用函数式编程?
  • selenium的css定位方式有哪些
  • RabbitMq快速入门程序
  • Qt模型控件:QTreeView应用
  • selenium常用的等待有哪些?
  • 基于51单片机水位监测控制自动抽水—LCD1602
  • 电脑系统做的好的几个网站wordpress主题很卡
  • 数据结构和算法篇-环形缓冲区
  • iOS 26 性能分析深度指南 包含帧率、渲染、资源瓶颈与 KeyMob 协助策略
  • vs网站建设弹出窗口代码c网页视频下载神器哪种最好
  • Chrome性能优化秘籍
  • 【ProtoBuffer】protobuffer的安装与使用
  • Jmeter+badboy环境搭建
  • ARM 总线技术 —— AMBA 入门
  • 【实战演练】基于VTK的散点凹包计算实战:从代码逻辑到实现思路
  • Flink 状态设计理念(附源码)
  • 23种设计模式——备忘录模式(Memento Pattern)
  • 【LeetCode】73. 矩阵置零
  • 网站开发教材男通网站哪个好用
  • 《3D草原场景技术拆解:植被物理碰撞与多系统协同的6个实战方案》
  • 软件测试—BUG篇
  • OpenAI系列模型介绍、API使用
  • 做网站的可以信吗深圳商城网站建设