二分

二分

​ 二分查找,是用来在一个有序数组中查找某一元素的算法。算法的巧妙之处在于它的时间复杂度十分优秀。在问题的答案具有单调性的情况下,我们也可以通过二分求解转换为二分判定。

​ 在写二分算法的时候,也有非常多的实现方式,所以细节方面要仔细思考。对于整数的二分,主要要注意一下左右边界的问题,还有最终答案的取值。对于实数的二分,就只需要注意精度的问题。

原理

​ 以在一个升序数组中查找一个数为例。

​ 它每次取当前部分的中间位置,把当前部分一分为二,分别为左区间和右区间。查找的数在左区间,就在左区间里面接着查找。如果查找的数在右区间,那么就在右区间里面接着查找,直到找到目标元素。所以每次查询的过程都会舍弃一半的元素。时间复杂度变成了优秀的 O ( l o g 2 n ) O(log_2n) O(log2n).

整数域上的二分

​ 二分有很多种的写法,这里我提供我认为最好的一种,方便大家理解。二分的写法保证最终答案处于闭区间 [l, r] 以内。

​ 在单调递增的序列 a 中查找 >= x 的数中最小的一个位置。

// 数组 a 元素位置为 [1, n]
int l = 1, r = n, ans = -1;  // l 为左端点, r 为右端点, ans 为最终答案的位置。
while(l <= r){
    int mid = (l + r) >> 1; // mid 为区间 [l, r] 的中间位置,加起来除以 2, 这种写法是加起来右移一位,同样的效果,但是速度更快。
    if (a[mid] >= x){ // 代表答案有可能在mid,有可能在左区间,所以要记录一下答案,然后去左区间找。
        r = mid - 1;   // 更新右端点的位置
        ans = mid;     // 记录答案的位置
    } else l = mid + 1; // 代表答案一定在右区间,去右区间找。更新左端点的位置。
}

​ 在判断的时候,如果改了判断的条件,代码需要跟着判断一起改动。

​ 如果改成 a[mid] < x, 代码就变成了下面这样。

int l = 1, r = n, ans = -1;  
while(l <= r){
    int mid = (l + r) >> 1;
    if (a[mid] < x){ 
        l = mid + 1;   
    } else {
        r = mid - 1; 
        ans = mid;
    }
}

所以二分的代码并不是一成不变的,上面只是一个模板,具体要根据自己的判断条件来灵活更改左右边界。

如果看代码不太好理解,建议大家找一个例子,然后用手画一下代码的运行过程。

有一类题目,题目中会说明要求的答案是最大值最小,或者最小值最大。这类题目就是暗示了要使用二分答案,比如例题2.

实数域上的二分

​ 二分有两种写法,一种是知道精度的。

while((r - l) < 0.00001){
    double mid = (l + r) / 2;
    if (check(mid)) r = mid; else l = mid;
}

​ 如果精度不太能确定,就可以使用下面的方法。

for (int i = 0; i < 70; ++i){
    double mid = (l + r) / 2;
    if (check(mid)) r = mid; else l = mid;
}

最终的答案就是 l 或者 r, 两者都行。

例题选讲

题意:

0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8

思路:

因为只会缺少一个数字,我们可以把题目转换一下,找到数组中第一个值和下标不一样的位置。

如果数组中所有的数和下标都一样,那就说明缺少的是最后一个数,

那就可以初始化把答案设置成最后一个数,在有序的情况下,二分是不会更改答案的值的。

这样就可以二分了。

public int missingNumber(int[] nums) {
    int n = nums.length;  // 得到数组的长度
    int ans = n, l = 0, r = n - 1;  // 初始化答案为n,初始化左右边界。
    while (l <= r){
        int mid = (l + r) >> 1;
        if (nums[mid] != mid){ // 值和下标不一样,说明 mid位置可能是答案,也有可能答案在左区间。
            r = mid - 1;  // 更新右区间
            ans = mid;   // 更新答案
        } else l = mid + 1;   // 说明答案在右区间,更新左区间的位置。
    }
    return ans;  // 返回答案。
}

题意:

1011. 在 D 天内送达包裹的能力

传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。

示例:

输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 52 天:6, 73 天:84 天:95 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 

思路:

再理解一下题目意思,就是一个最大值最小问题。

二分一个答案,这个答案是每天运输的最大值,即每天运输的量不超过这个答案。最后要求这个答案最小。

我们二分答案的时候,要算出来在这个答案下要多少天能完成运输,

  • 如果运输的天数大于 D, 说明二分的答案小了,要变大。
  • 如果运输的天数小于等于 D, 说明二分的答案有可能是答案,也有可能大了,所以要记录一下,然后把答案变小。
public int check(int x, int[] weights){  // x 就是二分的答案。每天的运输量不能超过 x 
    int cnt = 0, weight = 0; // cnt 是天数, weight 是每天的运输量。
    for (int i = 0; i < weights.length; ++i){
        weight += weights[i];  
        if (weight > x) {  // 如果运输量大于x,把最后一个物品放到第二天来运输。
            cnt++;    // 天数加一。
            weight = weights[i];  // 初始化第二天的运输量。
        }
    }
    return cnt+1; // weight 不是 0, 所以要加一天来运输最后的货物。
}
public int shipWithinDays(int[] weights, int D) {
    int l = 0, r = 0, ans = 0;  // l,r 分别是左右端点。
    for (int i = 0; i < weights.length; ++i) {
        r += weights[i];    // 右端点就是所有货物加起来
        l = Math.max(l, weights[i]);   // 左端点是 物品重量最大的一个,要保证船可以单独运输每一个物品。
    }
    while(l <= r){   // 二分答案
        int mid = (l + r) >> 1;
        if (check(mid, weights) <= D) {   //说明二分的答案有可能正好,有可能大了, 要变小。
            r = mid - 1;
            ans = mid;
        } else l = mid + 1;   // 说明二分的答案一定小了, 要变大。
    }
    return ans;
}

练习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值