二分法的注意事项

二分法有多少种写法都不重要,

重要的是要会写一种对的。

 

首先有几个数字要注意

中位数有两个,

  1. 下位中位数:lowerMedian = (length - 2) / 2
  2. 上位中位数:upperMedian = length / 2

常用的是下位中位数,通用的写法如下,语言int经常自动向下取整,

median = (length - 1) / 2

指针的区间当然可以开区间,也可以闭区间,也可以半开半闭。但老老实实两头取闭区间总是不会错。上面的中位数,转换成两头闭区间 [low,high] 就变成下面这样:

median = low + (high - low) / 2

这里还有个常见的坑,不要图快用加法,会溢出,

median = ( low + high ) / 2 // OVERFLOW

 

另外一个关键点是“终结条件”

不要以 low == high 做终结条件。会被跳过的,

if (low == high) { 
    return (nums[low] >= target)? low : ++low;
}

不相信在 [1, 5] 里找 0 试试?

 

正确的终结条件是:

low > high

也就是搜索空间为空。

 

满足终结条件以后,返回值完全不需要纠结,直接返回低位 low

因为回过头去放慢镜头,二分查找的过程就是一个 维护 low 的过程

low从0起始。只在中位数遇到确定小于目标数时才前进,并且永不后退。low一直在朝着第一个目标数的位置在逼近。知道最终到达。

至于高位 high,就放心大胆地缩小目标数组的空间吧。

 

所以最后的代码非常简单,

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

 

递归版也一样简单,

public int binarySearchRecur(int[] nums, int target, int low, int high) {
    if (low > high) { return low; } //base case
    int mid = low + (high - low) / 2;
    if (nums[mid] > target) {
        return binarySearchRecur(nums,target,low,mid-1);
    }  else if (nums[mid] < target) {
        return binarySearchRecur(nums,target,mid+1,high);
    } else {
        return mid;
    }
}

但上面的代码能正常工作,有一个前提条件:

元素空间没有重复值

推广到有重复元素的空间,二分查找问题就变成:

寻找元素第一次出现的位置。

也可以变相理解成另一个问题,对应C++的 lower_bound() 函数

寻找第一个大于等于目标值的元素位置。

 

但只要掌握了上面说的二分查找的心法,代码反而更简单,

public int firstOccurrence(int[] nums, int target) {
    int low = 0, high = nums.length-1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] < target) { low = mid + 1; }
        if (nums[mid] >= target) { high = mid - 1; }
    }
    return low;
}

 

翻译成递归版也是一样,

public int firstOccurrenceRecur(int[] nums, int target, int low, int high) {
    if (low > high) { return low; }
    int mid = low + (high - low) / 2;
    if (nums[mid] < target) {
        return firstOccurrenceRecur(nums,target,mid + 1,high);
    } else {
        return firstOccurrenceRecur(nums,target,low,mid-1);
    }
}

以上代码均通过leetcode测试。标准银弹。每天早起写一遍,锻炼肌肉。

 

最后想说,不要怕二分查找难写,边界情况复杂。实际情况是,你觉得烦躁,大牛也曾经因为这些烦躁过。一些臭名昭著的问题下面,经常是各种大牛的评论(恶心,变态,F***,等等)。而且这并不考验什么逻辑能力,只是仔细的推演罢了。拿个笔出来写一写,算一算不丢人。很多问题彻底搞清楚以后,经常就是豁然开朗,然后以后妥妥举一反三。以上。

原文地址:https://www.cnblogs.com/headchen/p/8962188.html