二分查找深度分析

Posted baizihua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找深度分析相关的知识,希望对你有一定的参考价值。

总结一句话就是:思路很简单,细节是魔鬼,hhhh。

本博客探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。

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

public int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; 

    while(left <= right) {  // 注意点
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; 
        else if (nums[mid] > target)
            right = mid - 1; 
        }
    return -1;
}
  • 为什么 while 循环的条件中是 <=,而不是 < ?

答:
举个例子推一下即可得知。因为当left=4,right=6时,此时mid=5;如果进入(nums[5] < target)分支时,left=6,如果是<,下次循环left=right=6不满足left<right,此时会跳出循环返回-1,然而索引是6的元素还没有判断是否等于target,故while 循环的条件中是 <=。

left <= right相当于两端都闭区间 [left, right]中搜索,left < right相当于左闭右开区间 [left, right)中搜索。

  • 此算法有什么缺陷?

答:
比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
我们后续的算法就来讨论这两种二分查找的算法。

寻找左侧边界的二分搜索

public int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意点1
    
    while (left < right) {    //注意点2
        int mid = (left + right) / 2;
        if (nums[mid] >= target) {  // 注意点3
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } 
    }
    // target 比所有数都大
    if (left == nums.length) return -1;
    // 类似之前算法的处理方式
    return nums[left] == target ? left : -1;
}
  • 为什么该算法能够搜索左侧边界?(注意点3)

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

if (nums[mid] >= target) {  // 注意点
    right = mid;
}

即找到 target 时不是立即返回,而是缩小搜索区间的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
为什么不是right = mid -1?因为这有可能会错过想要找的目标,比如:数组中存在唯一的值等于target,当mid刚好等于该值索引,进入(nums[mid] >= target),则right = mid -1;结果是错过唯一的目标,找不到target。

  • 为什么这里的while循环条件是left < right而不是<=(注意点2)
    举个例子:
int[] nums = {1,2,4,6,6,6,6,6,6,6,6,9};
target = 6;

如果是left <= right,则会出现这种情况:
left=3,right=3,此时mid=3,当target在mid处命中即target=nums[mid],此时进入(nums[mid] >= target)分支,right = 3;所以这种情况它跳不出循环。

  • 为什么int right = nums.length?数组不会越界?(注意点1)
    答:
    left=9,right=10,(9+10)/2=9;
    left=10,right=11,(10+11)/2=10;
    所以mid永远不会超过nums.length-1

  • 那为什么和第一种情况中right = nums.length-1不一样呢?
    因为left <= right代表[left,right],而left < right代表[left,right)

  • 为什么最终返回left而不是right?
    答:
    都是一样的,因为 while 终止的条件是 left == right。

寻找右侧边界的二分查找

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

与寻找左侧边界的二分查找原理类似,故不在赘述。

进阶:基本的二分搜索的左右分支转向代价不平衡的问题:
如[1,2,3],left=0,right=2,mid=1,进入左分支需要比较2次,进入右分支需要比较3次。
解决办法:
1,斐波那契数(Fibonacci)
2,变为2次比较
如有时间,下次阐述原理。

以上是关于二分查找深度分析的主要内容,如果未能解决你的问题,请参考以下文章

深入分析二分查找及其变体

详解二分查找

聊聊算法——二分查找算法深度分析

Java源码分析:二分查找 + 循环递归实现

「算法笔记」一文摸秃二分查找

[算法分析]二分查找细节分析与技术要点