二分查找专题总结:从数组越界到掌握“两段性“
二分查找专题总结:从数组越界到掌握"两段性"
目录
- 刷题记录
- 刷题过程
- 二分查找的核心概念
- 二分查找的前提
- 两种二分模板
- 我踩的坑总结
- 坑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这么快? 因为:
- 前面做了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]
}
完整流程:
- 找旋转点(最大值)
- 判断边界情况
- 判断target在哪个区间
- 在相应区间二分查找
这题给我的启发:
做完这题,我做LeetCode 153(找旋转数组最小值)只用了10分钟就AC了!
为什么?因为:
- 理解了"两段性"
- 知道要和
nums[n-1]
比较 - 边界情况处理清楚了
前面花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)
- 前提:数组有序 或 具有"两段性"
"两段性"的理解:
- 不是指数组有两段
- 而是指:数组能按某个性质分成"满足"和"不满足"两部分
- 关键是找到这个"性质"
两种模板的选择:
- 标准二分: 找具体值 →
while(left <= right)
,找到返回 - 边界二分: 找边界 →
while(left < right)
,最后返回left
mid的计算:
- 找第一个:向下取整
(right - left) / 2
- 找最后一个:向上取整
(right - left + 1) / 2
- 原因:避免死循环
什么时候复习:
- 旋转数组要再做一遍(LeetCode 33、153、81)
- 边界二分多练几题
- 答案二分还没学,后面继续
总结于: 2025年10月14日
相关专题: 排序、前缀和