引言
作为前端开发者,刷 LeetCode 是提升算法能力绕不开的一环。而在众多题型中,二分查找绝对是一个“看着简单,写起来要命”的存在——它不仅是时间复杂度优化的经典代表,更是大厂面试中的高频考点。
于是我拿出了两道二分查找的题目:
本以为轻车熟路,结果连交三版都报错,甚至一度怀疑自己是不是把“二分”理解成了“随机分”。但正是这些反复调试、不断踩坑的过程,让我真正吃透了二分的本质。
今天就想用这两道题为引子,带你一起梳理出一套通用、可靠、不易出错的二分思维框架。
一、初识二分:你以为你会了,其实只是背过
先来看最经典的题目:
这不就是教科书级别的二分吗?不是简简单单?于是我完全没有思考,飞快的在键盘上敲了起来
var search = function(nums, target) {
let left = 0;
let right = nums[nums.length - 1]; // ❌ 错误1:右边界是值,不是索引!
while (left < right) { // ❌ 错误2:漏了等于
let middle = Math.floor((left + right) / 2);
if (nums[middle] === target) return middle;
else {
if (nums[middle] <= target)
left = middle; // ❌ 错误3:没+1,死循环预警
else
right = middle;
}
}
return -1;
};
提交后……测试样例都直接全挂红。我当时一脸懵:“明明思路是对的啊?”
🔍 回头一看,全是低级错误?
| 错误点 | 问题分析 |
|---|---|
right = nums[length-1] | 把元素值当成了索引边界,完全误解了二分的操作对象 |
while(left < right) | 漏掉 left === right 的情况,导致最后一个元素无法判断 |
left = middle | 没有跳过已检查的 middle,可能造成无限循环 |
这些问题看似“粗心”,实则是对二分机制的理解还不够系统。
大于还是小于?要不要带=号呢,又是怎么判断呢?
感觉自己就跟没学过二分一样,模糊不清
于是我决定不再靠记忆模板,而是重新拆解它的核心逻辑。
✅ 正确姿势:建立清晰的“二分思维模型”
var search = function(nums, target) {
let left = 0;
let right = nums.length - 1; // ✅ 索引边界
while (left <= right) { // ✅ 包含相等情况
const middle = Math.floor((left + right) / 2);
if (nums[middle] === target) {
return middle; // 找到直接返回
} else if (nums[middle] < target) {
left = middle + 1; // 跳过middle,往右走
} else {
right = middle - 1; // 跳过middle,往左走
}
}
return -1; // 未找到
};
这次终于 AC 了。但我开始思考:为什么这么简单的逻辑,会频频出错?
答案是:我们太依赖“记住代码”,而忽略了“理解行为”。
二、进阶挑战:「寻找峰值」——打破我对二分的认知
接下来是这道让我彻底重构认知的题:
乍一看,这不是得遍历一遍找最大值吗?正准备上手,一看是道中等题,于是我停下来自己的想要飞起来的手,
仔细的查看了文章的条件,要求时间复杂度为O(log n),并且我难得细心了一次,看见了下面的提示
我寻思,O(log n)这不是二分嘛,但是这不是一个无序的数组嘛,怎么用二分???
一脸懵逼的我又傻眼了,这道题还能用二分?生无可恋的我发出了叹息
💡不对!题目告诉了对于所有有效的nums[i] !=nums[i+1],这告诉了我们什么信息呢?
联想到高中数学,我在纸上将其画了出来,再利用题目中给出的条件,可以将两个边界值看作-∞
于是我得出了这样的结论 “上坡必有下坡,下坡必有上坡”;
这一次,我秉持着认真的态度一步一步写下了我对这道题目的理解
✅ 最终正确代码如下:
var findPeakElement = function(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > nums[mid + 1]) {
right = mid; // 当前可能是峰值,保留在搜索区间
} else {
left = mid + 1; // 当前不可能是峰值,跳过
}
}
return left; // 最终 left === right,即峰值索引
};
这一次不仅 AC,而且我对“二分”的理解也完成了跃迁,甚至连无序的数组也可以用二分。
三、提炼方法论:构建属于自己的「二分通用思维框架」
经过这两道题的洗礼,我把二分查找总结成一个三步走模型,无论遇到什么变形都能快速推导出正确逻辑。
✅ 第一步:明确定义「搜索区间」
问自己:我在哪个范围内找答案?
通常使用闭区间 [left, right],初始化为:
let left = 0;
let right = nums.length - 1;
优点:边界清晰,易于理解和维护。除非特殊需求(如开区间处理边界问题),否则优先选闭区间。
✅ 第二步:确定「循环终止条件」
问自己:什么时候停止搜索?
两种常见模式:
| 场景 | 循环条件 | 说明 |
|---|---|---|
| 需要检查单个元素 | while (left <= right) | 如基础二分查找 |
最终 left === right 即为答案 | while (left < right) | 如寻找峰值、旋转数组最小值 |
关键区别:是否需要显式判断 left === right 的情况。
✅ 第三步:精准设计「边界收缩逻辑」
问自己:当前
middle还有可能是答案吗?
这是最容易出错的地方!记住一句话:
收缩时,只排除“绝对不是答案”的部分。
判断标准:
- 若
nums[middle]可能是目标 → 保留它(right = middle或left = middle) - 若
nums[middle]一定不是目标 → 跳过它(left = middle + 1或right = middle - 1)
📌 小技巧:可以用“反证法”验证收缩逻辑是否合理。
四、拓展视野:二分不只是“找数字”
别再以为二分只能用于排序数组查值了!只要满足以下特性,就可以考虑二分:
✅ 问题具有“二段性”或“单调性”
——即可以根据某种判断条件将搜索空间划分为两部分,一部分满足性质 A,另一部分不满足。
你会发现,很多“看起来不像二分”的题,本质都是在一个有序空间里做决策划分。
五、写在最后:从“背模板”到“懂原理”
回顾整个学习过程,我最大的收获不是写出正确的代码,而是意识到:
算法的本质不是记忆,而是建模。
以前我总想着“背下几种二分模板”,结果换道题就不会了。而现在,我学会了从问题本身出发,通过三个问题自问自答:
- 我的搜索区间是什么?
- 什么时候该停下来?
- 当前中间点还能不能是答案?
只要这三个问题想清楚了,代码自然水到渠成。
💡 给读者的小建议
如果你也在准备面试或者刷题:
- 不要急于追求“一次 AC”,更要关注“为什么会错”;
- 多动手画图模拟过程,尤其是边界移动;
- 遇到变体题时,回归本质,不要强行套模板;
- 把每一道错题变成一次思维训练的机会。
🎯 结语:
二分查找就像编程世界里的“九九乘法表”——简单却深刻,基础却致命。它教会我们的不只是如何高效查找,更是如何严谨地思考每一个细节。
愿你在每一次“死循环”之后,都能迎来一次认知的突破。
📌 欢迎点赞 + 收藏 + 关注,我会持续分享前端视角下的算法实战心得。