Binary Search 模板总结(连续,离散,递归,循环等版本)

Binary Search 根据我的总结,可以分成离散和连续两类。离散就是一个排好序的数组里面找数,连续就是在[0,x]这个连续区间内找一个符合条件的解。因为0–x是自然连续增加的,所以当然可以用Binary Search。

离散Binary Search 通用模板:
离散Binary Search用前闭后开区间[a,b)和前闭后闭区间[a,b]都可以。前闭后开是一种STL风格的写法。这种写法的好处是:

  1. 给遍历元素时,循环的结束时机提供一个简单的判断依据。只要尚未到达end(),循环就可以继续下去。
  2. 不必对空区间采取特殊处理手段。空区间的begin()就等于end()。
    我们可以跟后面的前闭后闭一比较就可以看出来。前开后闭的写法要简单很多。但是注意它不能找重复元素出现的第一次或最后一次位置!!!因为会导致死循环。

前闭后开[start, end) 模板:

int binarySearch(vector<int> &a, int start, int end, int target) {
   int mid;
   while(start<end) {   //note it is start<end, not <=
        mid=start+(end-start)/2;
        if (a[mid]>target) 
             end=mid;
        else if (a[mid]<target)
             start=mid+1;
        else
             return mid;
  } 
  return -1;
}

注意:
1) 如果有重复元素的话,该模板返回该重复元素的任意位置(不一定是第一个,也不一定是最后一个)。
2) 需要特别注意的是,这种前开后闭区间的写法只能用于一旦找到就马上return pos的情况,而不能用于寻找重复元素出现的第一次或最后一次出现位置的情况,否则会导致死循环!这种情况下应该用下面的前闭后闭区间模板。

前闭后闭[start, end] 模板
前闭后闭递归版:

int findPos(vector<int> &a, int target) {
    return binarySearch(a, 0, a.size() - 1, target);
}
int binarySearch(vector<int> &a, int target) {
    int start = 0, end = a.size() - 1;
    if (start > end) return -1; 
    int mid = start + (end - start) / 2;
    if (a[mid] == target) return mid;   
    if (a[mid] < target) return binarySearch(a, mid + 1, end, target);
    return binarySearch(a, start, mid - 1, target);
}

注意我们不需要写if (start == end)的情况,因为如果start == end, 后面的mid = start, 再call
binarySearch(a, mid + 1, end, target)

binarySearch(a, start, mid - 1, target)
都会出现(start > end) 的情况。

前闭后闭循环版 (九章的版本):

int binarySearch(vector<int> nums, int target){
    if (nums.size() == 0 || start > end)
       return -1;
    int start = 0, end = nums.size() - 1;        

    while (start + 1 < end) { //注意这里是start+1,是<不是<=
         int mid = start + (end - start) / 2;
            // 注意: =, <, > 三种情况,mid 不+1也不-1
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        
        // 注意,循环结束后,单独处理start和end
        if (nums[start] == target) {
            return start;
        }
        if (nums[end] == target) {
            return end;
        }
        return -1;
    }
}

为什么while()后面还要加2个if判断呢?我的理解是为了处理一些特殊情况,比如说,start==end(即只有1个元素)时;或者当有重复元素时,后面2个if判断可以保证
找到第一个或者最后一个该元素。

2个主要问题及解答(from 九章):
Q: 为什么要用 start + 1 < end?而不是 start < end 或者 start <= end?
这是为了避免死循环。二分法的模板中分为两个部分:

  1. 通过 while 循环,将区间范围从 n 缩小到 2 (只有 start 和 end 两个点)。2)在 start 和 end 中判断是否有解。
    如果我们用start < end 或者 start <= end, 在寻找目标最后一次出现的位置的时候,会出现死循环。

Q: 为什么明明可以 start = mid + 1 偏偏要写成 start = mid?
A: 大部分时候,mid 是可以 +1 和 -1 的。在一些特殊情况下,比如寻找目标的最后一次出现的位置时,当 target 与 nums[mid] 相等的时候,是不能够使用 mid + 1 或者 mid - 1 的。因为会导致漏掉解。统一写成 start = mid / end = mid 并不会造成任何解的丢失,并且也不会损失效率——log(n) 和 log(n+1) 没有区别。

注意:
1) 如果有重复元素的话,该模板返回该重复元素的某个位置(不一定是第一个,也不一定是最后一个)。

2)当需要寻找重复元素出现的最后一个位置时,把return mid改为start = mid。最后两行顺序为

        if (nums[end] == target) return end;        
        if (nums[start] == target) return start;

3)当需要寻找重复元素出现的第一个位置时,把return mid改为end = mid。最后两行顺序为

        if (nums[start] == target) return start;
        if (nums[end] == target) return end;      
  1. 当需要寻找刚好小于或等于target的第一个元素时,我的理解是while()后的情况可能是start<=target<=end,也有可能是target<start<end,或start<end<target。所以
        if (nums[start] == target) return start; //nums[start] == target
        if (nums[end] == target) return end; //nums[end] = target
        if (nums[end] < target) return end;  //nums[start]<nums[end]<target      
        if (nums[start] < target) return start; //nums[start]<target<nums[end]

注意,
if (nums[end] < target) return end;
必须在
if (nums[start] < target) return start;
之前,否则nums[start]<nums[end]<target的情况就会return start了。

  1. 当需要寻找刚好大于等于target的第一个元素时,与上面类似。
        if (nums[start] == target) return start;
        if (nums[end] == target) return end;
        if (nums[start] > target) return start; //target < nums[start] < nums[end]
        if (nums[end] > target) return end;   //nums[start] < target < nums[end] 

递归版:

int binarySearch(int[] nums, int start, int end, int target) {
    if (start > end) return -1;
    
    int mid = start + (end - start) / 2;
    if (nums[mid] == target) return mid;
    if (nums[mid] < target) return binarySearch(nums, mid + 1, end, target);
    return binarySearch(nums, start, mid - 1, target); 
}

连续Binary Search版本。跟离散版本差不多,而且还简单些。但是要注意找解的时候不能用==,而必须用一个很小的threshold来比较。小于这个threshold就可以认为相等. 下面是我写的一个double版的x/y的function(不用到/)。这里我认为double的被除数不会为0。欢迎指正。
代码如下:

#include <iostream>
#include <climits>
#include <cmath>
using namespace std;

double divideV(double x, double y) {
    //if (abs(y)<1e-6) return y>0 ? INT_MAX : INT_MIN;
    bool neg = false;

    if (x < 0 && y < 0) {x = -x; y = -y;}
    else if (x < 0) {neg = true; x = -x;}
    else if (y < 0)  {neg = true; y = -y;}

    double start = 0.0, end = x;

    while(start < end) {
        double mid = start + (end - start) / 2;
        if (abs(mid * y - x)<1e-6)
            return neg? -mid : mid;
        else if (mid*y - x < 0)
            start = mid;
        else
            end = mid;
    }
}

int main()
{
    cout << divideV(81.0, 7.0) << endl;
    cout << divideV(-81.0, 7.0) << endl;
    cout << divideV(81.0, -7.0) << endl;
    cout << divideV(-81.0, -7.0) << endl;
    return 0;
}

一些关于Binary Search的要点:

  1. Binary Search和Hash Table的比较:
    我们都知道Binary Search的复杂度是O(logn), 而Hash Table的复杂度是O(1)。但Hash Table有个问题是它的空间复杂度较大,而且Hash Table必须常驻内存,不能放在磁盘里面。
    而**Binary Search的数据不需要常驻内存,可以放在磁盘里面再读到内存里面。**所以Binary Search不受内存限制。文件系统通常都用Binary Search。
  2. 二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。对于动态变化的集合,应该采用类似红黑树之类的结构。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值