• 二分查找深度分析


    总结一句话就是:思路很简单,细节是魔鬼,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次比较
    如有时间,下次阐述原理。

  • 相关阅读:
    MongoDB学习笔记(一) MongoDB介绍及安装
    MVC DefaultControllerFactory代码分析
    WCF中的变更处理
    分布式文档存储数据库 MongoDB
    wcf学习资料
    vs2010打包安装
    Android语音识别RecognizerIntent
    Eclipse快捷键
    甲骨文公司老板埃里森在耶如大学的…
    Android&nbsp;TTS语音识别
  • 原文地址:https://www.cnblogs.com/baizihua/p/12288204.html
Copyright © 2020-2023  润新知