基础算法-二分查找

基础算法-二分查找

二分查找算法是在实践中用的最多的算法之一。因为它简单易懂,效率很高,成为很多程序员的首选。之前我们也看到过很多关于二分查找的文章,例如你真的会二分查找吗?这个看似简单的算法,却有很多需要我们注意的地方,这里我们要思考:

  1. 什么时候使用二分 ?
  2. 怎么使用二分 ?
  3. 二分为什么效率高?

二分查找

二分查找又名折办查找,是一种简单而且较有效的查找方法。要满足两点:循序结构存储关键字有序排序。二分查找有很多版本,我一般使用两端闭区间的代码格式,(注意这很重要,建议使用自己喜欢的唯一一种方法,防止出错)。
给出我自己喜欢的格式:

// A 关键字有序,而且顺序存储
function binary_search(A, n, target, cmp):
    left = 0
    right = n - 1
    while left <= right:
        mid = (left + right) / 2
        if cmp(A[mid], target):
            left = mid + 1;
        else if not cmp(A[mid], target):
            right = mid - 1;
        else:
            return answer = A[mid]
    return unsucessful

什么时候使用

1. 是否是查找问题
2. 是否能够使用顺序查找
3. (结果区间)是否有序
4. 是否是顺序存储(即通过下标可以直接定位到对应的值)     
5. 怎么进行二分,这个看似简单,其实二分的关键

我们直接来看几个问题,分析是否满足这几个条件,当然有些简单的问题我们可以直接看出来是二分查找,还是希望从头分析,一起往能满足我们给出的四个规则。

  1. 给定一个有序的整数数组A和一个值target,判断target是否在A中,如果存在返回对应的任意一个下标,否则返回-1.

有序,数组,判断是否存在(可以使用顺序查找),结果区间是整数数组(有序),这里我们只需要根据下标二分就行了。这就是我们最常用的二分查找的原型。

timg

  1. 有两个有序数组A(长度n)和B(长度m),求这两个数组合并之后的中位数,中位数我们认为是有序数组的中间的数,median = S[(n+m) / 2], S是排序后的数组。
  1. 可以直接看出来是查找问题
  2. 能使用顺序查找,顺序扫描两个数组,直到k = (n+m)/2,就得到我们要的中位数
  3. 结果区间是否有序,有序,我们可以假设有一个大数组是A+B排序后的结果
  4. A和B都是顺序存储
  5. 怎么进行二分,这里我们两个数组A和B同时二分,每次也是缩小一般查询区间。
A:[0, n-1], B: [0, m-1] 
mid_a = (left_a + right_a) / 2
mid_b = (left_b + right_b) / 2
if judge(A[mid_a], B[mid_b]): // A[mid_a] < B[mid_b]
    A: [mid_a+1, right_a]
    B: [left_b, mid_b-1]
else:
    A: [left_a, mid_a-1]
    B: [mid_b+1, right_b]
util left_a == right_a 
    return max(A[left_a], B[left_b])
  1. 实现 int sqrt(int x) 函数,计算并返回 x 的平方根。
  1. 查找问题,对应x = a * a, a属于[0, x]之间,即从[0, x]中查找a,是的a*a = x
  2. 能否使用顺序查找,可以遍历for a in [0, x]: 直到满足条件
  3. 结果区间是[0, x]有序
  4. 结果区间有序存储,关键字就是结果
  5. 根据结果区间[0, x]进行二分即可。
int sqrt(int x) {
    long long a=0,b=x;
    while(a < b){
        long long m = (a+b)/2;
        if(m*m == x) break;
        else if(m*m > x) b = m-1; //第一个judge(x)
        else a = m+1;
    }
    if(b*b > x) b-=1;
    return b;
}
  1. 原木切割

题目:有一些原木,现在想把这些木头切割成一些长度相同的小段木头,需要得到的小段的数目至少为 k。当然,我们希望得到的小段越长越好,你需要计算能够得到的小段木头的最大长度。
从这个问题中,我们很难想到二分查找问题,那就抽象这个问题,一些小木棒vector<int>, 小段木头的长度length, 一定满足0 < length <= max(vector<int>)

  1. 是查找问题,从0 < length <= max(vector<int>)找到最合适的长度满足条件,
  2. 能够使用顺序查找,遍历所有的长度,判断是否满足条件
  3. 结果区间[0, max(vector< int>)] 有序,而且顺序
  4. 之后就可以堆结果区间进行二分查找即可,

下面的代码是典型的转换问题之后的二分查找的问题。

int cmp(vector<int> L, int k, int length){
    if(length == 0) return -1;
    int cnt = 0;
    for(int i = 0; i < L.size(); i ++){
        cnt += L[i] / length;
    }
    if(cnt < k) return 0;
    if(cnt >= k) return 1; //长度短
}

int woodCut(vector<int> L, int k) {
    long long maxlen = 0, minlen = 0, midlen = 0;
    for(int i = 0; i < L.size(); i ++){
        if(maxlen < L[i]) maxlen = L[i];
    }

    while(minlen <= maxlen){
        midlen = (maxlen + minlen) / 2;
        if(cmp(L, k, midlen) == 1){
            minlen = midlen + 1;
        }
        else if(cmp(L, k, midlen) == 0){
            maxlen = midlen - 1;
        }
        else{
            break;
        }
    }
    return maxlen;
}

二分查找很简单,对于一些显而易见的问题,我们都能想到是二分查找的问题.但是有一些问题是需要我们进行抽象之后的才能看处理其本质。我们需要指出这里的有序和顺序是指结果区间有序,关键字一般都是我们需要问题的结果,不是去看这个题目给出的数组,例如题目4中我们没有关注给定的一些小木段,而是关注的结果区间的长度。

为什么有效

二分查找时间复杂度是O(logn),几乎每一个人都知道,这里我们就来公式证明一下这个显而易见的结果。
根据二分的性质,

T(n) = T(n/2) + O(1)  
T(n/2) = T(n/4) + O(1)    
    .
    .
    .   
T(2) = T(1) + O(1)     
T(1) = O(1)

这里个高度是logn(底数是2),因此 T(n) = O(1) * logn = O(logn)

Screenshot-from-2018-11-29-15-14-15

总结:

浅谈二分到这里就结束了,正所谓纸上得来终觉浅,绝知此事要躬行。多想多练必不可少。

生活如此,问题不大。喵~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值