我以为二分是送分题,结果它是来送我的

108 阅读6分钟

引言


作为前端开发者,刷 LeetCode 是提升算法能力绕不开的一环。而在众多题型中,二分查找绝对是一个“看着简单,写起来要命”的存在——它不仅是时间复杂度优化的经典代表,更是大厂面试中的高频考点。

于是我拿出了两道二分查找的题目:

本以为轻车熟路,结果连交三版都报错,甚至一度怀疑自己是不是把“二分”理解成了“随机分”。但正是这些反复调试、不断踩坑的过程,让我真正吃透了二分的本质。

今天就想用这两道题为引子,带你一起梳理出一套通用、可靠、不易出错的二分思维框架


一、初识二分:你以为你会了,其实只是背过

先来看最经典的题目:

image.png

这不就是教科书级别的二分吗?不是简简单单?于是我完全没有思考,飞快的在键盘上敲了起来

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 了。但我开始思考:为什么这么简单的逻辑,会频频出错?

答案是:我们太依赖“记住代码”,而忽略了“理解行为”


二、进阶挑战:「寻找峰值」——打破我对二分的认知

接下来是这道让我彻底重构认知的题:

image.png 乍一看,这不是得遍历一遍找最大值吗?正准备上手,一看是道中等题,于是我停下来自己的想要飞起来的手, 仔细的查看了文章的条件,要求时间复杂度为O(log n),并且我难得细心了一次,看见了下面的提示

image.png 我寻思,O(log n)这不是二分嘛,但是这不是一个无序的数组嘛,怎么用二分???

一脸懵逼的我又傻眼了,这道题还能用二分?生无可恋的我发出了叹息


💡不对!题目告诉了对于所有有效的nums[i] !=nums[i+1],这告诉了我们什么信息呢?

联想到高中数学,我在纸上将其画了出来,再利用题目中给出的条件,可以将两个边界值看作-∞

于是我得出了这样的结论 “上坡必有下坡,下坡必有上坡”;

image.png

这一次,我秉持着认真的态度一步一步写下了我对这道题目的理解

✅ 最终正确代码如下:

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 = middleleft = middle
  • nums[middle] 一定不是目标 → 跳过它(left = middle + 1right = middle - 1

📌 小技巧:可以用“反证法”验证收缩逻辑是否合理。


四、拓展视野:二分不只是“找数字”

别再以为二分只能用于排序数组查值了!只要满足以下特性,就可以考虑二分:

问题具有“二段性”或“单调性”
——即可以根据某种判断条件将搜索空间划分为两部分,一部分满足性质 A,另一部分不满足。

你会发现,很多“看起来不像二分”的题,本质都是在一个有序空间里做决策划分。


五、写在最后:从“背模板”到“懂原理”

回顾整个学习过程,我最大的收获不是写出正确的代码,而是意识到:

算法的本质不是记忆,而是建模。

以前我总想着“背下几种二分模板”,结果换道题就不会了。而现在,我学会了从问题本身出发,通过三个问题自问自答:

  1. 我的搜索区间是什么?
  2. 什么时候该停下来?
  3. 当前中间点还能不能是答案?

只要这三个问题想清楚了,代码自然水到渠成。


💡 给读者的小建议

如果你也在准备面试或者刷题:

  • 不要急于追求“一次 AC”,更要关注“为什么会错”;
  • 多动手画图模拟过程,尤其是边界移动;
  • 遇到变体题时,回归本质,不要强行套模板;
  • 把每一道错题变成一次思维训练的机会。

🎯 结语

二分查找就像编程世界里的“九九乘法表”——简单却深刻,基础却致命。它教会我们的不只是如何高效查找,更是如何严谨地思考每一个细节。

愿你在每一次“死循环”之后,都能迎来一次认知的突破。


📌 欢迎点赞 + 收藏 + 关注,我会持续分享前端视角下的算法实战心得。