二分查找:程序员的“左手右手慢动作”——从 LeetCode 704 到 35 的全方位剖析

121 阅读4分钟

“你以为你很快,其实二分查找比你还快。”
—— 一位被 O(n) 暴力搜索支配过的程序员

前言

在算法的江湖里,二分查找绝对是“快刀斩乱麻”的代表。它不仅速度快,还能让你在面试官面前装得一手好逼。今天,我们就用 LeetCode 704 和 35 两道经典题,带你从入门到精通,顺便聊聊二分查找背后的那些“骚操作”。


一、二分查找的本质:你以为的“中庸之道”

二分查找(Binary Search)是一种在有序数组中查找目标值的算法。它的核心思想是:每次都把查找区间砍成两半,目标值要么在左边,要么在右边,反正不在中间就继续砍。
用一句话总结:“不是左边就是右边,绝不拖泥带水。”

1.1 复杂度分析

  • 时间复杂度:O(log n),每次查找都能把问题规模减半,效率杠杠的。
  • 空间复杂度:O(1),只用几个指针,内存消耗感人。

二、LeetCode 704:标准二分查找,教科书级别的范例

2.1 题目简介

给定一个升序数组 nums 和一个目标值 target,如果目标值存在返回下标,否则返回 -1。

2.2 代码实现

让我们来看看 704.js 文件里的实现:

var search = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let mid = left + ((right - left) >> 1);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
};

2.3 技术点剖析

2.3.1 mid = left + ((right - left) >> 1) 的骚操作

你可能见过 mid = (left + right) / 2,但这里用了 left + ((right - left) >> 1)。为啥?
因为如果 leftright 都很大,直接相加可能会溢出!虽然 JavaScript 的 Number 比较大,但养成好习惯总没错。
>> 1 是右移一位,相当于除以2并向下取整,效率高还显得你很懂位运算。

2.3.2 左闭右闭区间

left = 0, right = nums.length - 1,循环条件是 left <= right
这叫左闭右闭区间,即 [left, right]
优点是边界处理简单,缺点是新手容易写错循环条件。

2.3.3 返回值

找到了就返回 mid,找不到就返回 -1,一目了然。


三、LeetCode 35:搜索插入位置,二分查找的“变种兄弟”

3.1 题目简介

给定一个升序数组和一个目标值,返回目标值在数组中的索引。如果不存在,返回它应该被插入的位置。

3.2 代码实现

35.js 文件的实现如下:

var searchInsert = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let middle = left + ((right -left) >> 1);
        if (nums[middle] < target) {
            left = middle + 1;
        } else if (nums[middle] > target) {
            right = middle - 1;
        } else {
            return middle;
        }
    }
    return left;
};

3.3 技术点剖析

3.3.1 为什么最后返回 left

当目标值不存在时,left 最终会停在第一个大于目标值的位置,也就是目标值应该插入的位置。
举个例子:

  • nums = [1,3,5,6], target = 2
  • 最后 left = 1,正好是 2 应该插入的位置。

3.3.2 依然是左闭右闭区间

和 704 一样,区间是 [left, right],循环条件是 left <= right,边界处理依然优雅。

3.3.3 代码复用性强

你会发现,35 题和 704 题的代码几乎一模一样,只是最后的返回值不同。
这说明二分查找的模板化程度很高,学会一套,走遍天下都不怕。


四、二分查找的“坑”与“骚操作”

4.1 溢出问题

虽然 JavaScript 不容易溢出,但在 C++/Java 里,mid = (left + right) / 2 可能会炸。
所以推荐用 mid = left + ((right - left) >> 1),这也是大厂面试官喜欢问的点。

4.2 区间选择

  • 左闭右闭 [left, right]:循环条件 left <= rightright = nums.length - 1
  • 左闭右开 [left, right):循环条件 left < rightright = nums.length

不同区间写法不同,细节决定成败。

4.3 无限循环

如果 leftright 更新不对,可能会死循环。比如忘了 +1-1,就会原地打转。


五、二分查找的应用场景

  • 查找有序数组中的元素:如 LeetCode 704
  • 查找插入位置:如 LeetCode 35
  • 求平方根、开方、分数逼近:只要是“有序+查找”,都能用二分查找
  • 面试必考:大厂面试官最爱问的基础算法之一

六、幽默总结

二分查找就像是程序员的“左手右手慢动作”,每次都优雅地把问题一分为二,直到找到目标或者插入点。
学会了二分查找,你就能在 O(n) 的人群中脱颖而出,成为 O(log n) 的男人/女人/程序猿!


七、附录:代码演示

7.1 704 题代码

var search = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let mid = left + ((right - left) >> 1);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
};

7.2 35 题代码

var searchInsert = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let middle = left + ((right -left) >> 1);
        if (nums[middle] < target) {
            left = middle + 1;
        } else if (nums[middle] > target) {
            right = middle - 1;
        } else {
            return middle;
        }
    }
    return left;
};