浅谈二分查找算法

691 阅读2分钟

我最近在 Leetcode 刷了一些二分查找的题目,被虐得体无完肤。原以为二分查找很简单,只是套个模板就好。但是做了题目之后,就悟到了网上流传的一句话 "思路很简单,细节是魔鬼"。于是我费尽苦心,在知乎上找到一篇我觉得对二分查找分析得很透彻的文章,在这里分享一下。

本文章摘录于知乎 labuladong 的回答。

一、寻找一个数(基本的二分搜索)

int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) { //注意1
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1; //注意2
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    return -1;
}

1、循环条件

right 的 值为 nums.length - 1,即最后一个元素的索引。此时「搜索区间」为左闭右闭 [left, right]

while(left <= right) 的终止条件是 left == right + 1,写成区间的形式是 [right + 1, right] 。可见这时候区间为空,所以这时候 while 循环终止是正确的。

while(left < right) 的终止条件是 left == right,写成区间的形式是 [left, left],这时候区间非空,还有一个数 left。但此时 while 循环终止了,也就是说此时漏掉了一个索引 left。

2、区间收缩

因为 mid 已经搜索过,所以应该从「搜索区间」中去除,即 left = mid + 1right = mid - 1

二、寻找左侧边界的二分搜索

int binarySearch(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length; //注意1
    while (left < right) { //注意2
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1; 
        } else if (nums[mid] > target) {
            right = mid; //注意3
        }
    }
    return left;
}

1、循环条件

right 的值为 nums.length,因此每次循环的「搜索区间」是左闭右开 [left, right)。

while (left < right) 的终止条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止循环。

2、区间收缩

因为「搜索区间」是左闭右开 [left, right),所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid,分割成两个区间,即 [left, mid) 和 [mid + 1, right)。

为什么该算法能够搜索左侧边界?

答:关键在于对于 nums[mid] == target 这种情况的处理:

if (nums[mid] == target) {
    right = mid;
}

可见,拿到 target 时不要立即返回,而是缩小搜索区间的上界 right,在区间 [left, mid) 中继续搜索,即不断向左搜索,达到锁定左侧边界的目的。

为什么返回 left 而不是 right?

答:都是一样的,因为 while 终止的条件是 left == right。

三、寻找右侧边界的二分搜索

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

1、循环条件

此处与寻找左侧边界相同。

2、区间收缩

因为「搜索区间」是左闭右开 [left, right),所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid,分割成两个区间,即 [left, mid) 和 [mid + 1, right)。

为什么该算法能够搜索右侧边界

答:类似地,关键点还是这里:

if (nums[mid] == target) {
    left = mid + 1;
}

当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。

为什么最后返回 left - 1?

if (nums[mid] == target) {
    left = mid + 1;
    // 这样想:mid = left - 1
}

也就是说,while 循环结束时, nums[left] 一定不等于 target 了,而 nums[left - 1] 有可能是 target。

注意事项

  • 分析二分查找代码时,不要出现 else,全部展开 else if 方便理解。
  • 注意「搜索区间」和 while 的终止条件。如果存在漏掉的元素,记得在最后检查。
  • 如需要搜索左右边界,只要在 nums[mid] == target 时做修改即可。搜索右侧是需要减 1。