【二分查找延伸--实际算法应用】数组类题目

声明:博主是基于labuladong微信公众号文章模板驱动刷题,进行的自我刷题感悟和记录在此。
模板详情见labuladong微信公众号文章文末;原创于自己在此基础上的笔记、感悟、整合其它文献和自己的code。

二分查找到底能运用在哪里?

【基础】使用

基本/必备应用

一、求开方——二分查找求解二元一次方程问题

题目描述: x的平方根——使用二分查找算法

  • 实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4 输出: 2 示例 2:

输入: 8 输出: 2 说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。

分析:我们可以把这道题想象成,给定一个非负整数 a,求 f(x) = x2− a = 0 的解。因为我们只考
虑 x ≥ 0,所以 f(x) 在定义域上是单调递增的。考虑到 f(0) = −a ≤ 0,f(a) = a2− a ≥ 0,我们
可以对 [0,a] 区间使用二分法找到 f(x) = 0 的解。

注意,在以下的代码里,为了防止除以 0,我们把 a = 0 的情况单独考虑,然后对区间 [1,a]
进行二分查找。我们使用了左闭右闭的写法。

#include <cmath>
#include <iostream>
#include <map>
#include <unordered_map>
#include <vector>
#include <windows.h>

using namespace std;

class Solution
{
public:
    // 最简单可以完成
    int mySqrt1(int x)
    {
        return sqrt(x);
        //二分查找 中间值
    }

    // 通过二分查找,找到方大于x 直至刚好小于x的下确界
    int mySqrt2(int x)
    {
        // if (x == 0)
        //     return x;
        // int l = 1, r = x, mid, sqrt;
        // while (l <= r)
        // {
        //     mid = l + (r - l) / 2;
        //     sqrt = x / mid;
        //     if (sqrt == mid)
        //     {
        //         return mid;
        //     }
        //     else if (mid > sqrt)
        //     {
        //         r = mid - 1;
        //     }
        //     else
        //     {
        //         l = mid + 1;
        //     }
        // }
        // return r;
        if (x == 0) return 0;
        int left = 1;   //gai
        int right = x; // 左闭右闭
        // 查找所以等于
        while (left <= right)
        {
            // int mid = (left + right) / 2; // 会溢出!所以最好用下面一行的!
            int mid = left + (right - left) / 2;
            int sqr2 = x/mid;  // 重点
            if ( sqr2 == mid)
            {
                return mid;
            }
            else if (sqr2 < mid)  //【重点】 类似于nums[mid] > nums[target]
            {
                right = mid - 1;
            }
            else if (sqr2 > mid)
            {
                left = mid + 1;
            }
        }
        return right; // 返回一个 刚好的下确界  【重点!】
    }
};

int main()
{
    string s = "abcabcbb";
    // string s = "bbbb";
    // // string s = "pwwkew";
    // // string s = "";
    // // string s = " ";
    // Solution1 so;
    // int num = so.lengthOfLongestSubstring(s);

    Solution so;
    int x = 2147483647;

    cout << "x" << x << "的向下根号为:" << so.mySqrt2(x) << endl;
    system("pause");
    return 0;
}

注意: 上述过程 即为求解方程问题,且 注意最后mySqrt2()返回使用 right这也是由停止条件决定,此时right处于下确界!

另外,这道题还有一种更快的算法——牛顿迭代法,其公式为 xn+1= xn− f(xn)/f′(xn)。给
定 f(x) = x2− a = 0,这里的迭代公式为 xn+1= (xn+ a/xn)/2,其代码如下。

 // 牛顿迭代法求解方程.
    int mySqrt3(int a)
    {
        long x = a;
        while (x * x > a)
        {
            x = (x + a / x) / 2;
        }
        return x;
    }

注意 这里为了防止 int 超上界,我们使用 long 来存储乘法结果。

二、查找区间

Find First and Last Position of Element in Sorted Array (Medium)
在排序数组中查找元素的第一个和最后一个位置

  • 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4] 示例 2:

输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1] 示例 3:

输入:nums = [], target = 0 输出:[-1,-1]

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109 nums 是一个非递减数组
-109 <= target <= 109

这个就是直接左右边界的二分搜索。 没啥好说的,前文看懂了就行。

三、搜索排序好的旋转数组

搜索旋转排序数组II * 假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。

编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。

示例 1:

输入: nums = [2,5,6,0,0,1,2], target = 0 输出: true

示例 2:

输入: nums = [2,5,6,0,0,1,2], target = 3 输出: false 进阶:

这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
雾曹!最后这句话!!!—— 绝对增加复杂度了!见下方解题思路!!!

解题思路
1、即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点,
如果它指向的值小于等于右端,那么说明右区间是排好序的;反之(直接else),那么说明左区间是排好序的。

2、如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找(修改搜寻l&r);反之(else),我们对于另一半区间继续二分查找。

3、【注意】,因为数组存在重复数字,如果中点和右端/(或左端)的数字相同,我们并不能确定是左区间全部
相同,还是右区间完全相同。在这种情况下,我们可以简单地将右端左移一位/(或左端点右移一位),然后继续进行二分查找。

4、其次:①code过程中,出现的边界问题提取出来的判断;②以及等于号的细节运用 & else的算法边界限制【要好好的思考或 用一些特殊的实例进行排除!】
完整代码

// 思路:本题虽然有循环,但是内部也还是有递增的序列; 
// 仍然可以找到一边的 递增序列;进行log(n)的二分查找。
class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int n = nums.size();
        if(n==0) return false;
        // int split;
        int left=0, right = n;
        //外部寻找 有序数组
        while(left < right){
            int mid = left + (right-left)/2;
            if(nums[mid] == target) return true;  // 错了拿出来
            
             // 少边界!!! 相等时候,
            if( nums[mid] == nums[right-1] ) right -= 1;
            // 内部在有序内 真正的二分查找
            else if( nums[mid] < nums[right-1] ){
                // 说明右边是排好序的
                // if(target < nums[mid]){
                //     right = mid;
                // }else{
                //     right-=1;
                // }
                if(target >= nums[mid] && target <= nums[right-1]){
                    // 做靠谱的事情, 剩下未知的交给 else
                    // right = mid;
                    left = mid + 1;

                }else{// 不在右边
                    // right-=1;
                    right = mid;
                }

                //--- 下面的太慢了
                // // auto a = this->binarySearch(nums.begin(), target);
                // int low = mid+1, high = right;
                // while( low < high){
                //     int midRight = low+ (high-low)/2;
                //     if(nums[midRight] == target){
                //         return true;
                //     }else if(nums[midRight] < target){
                //         low = midRight+1;
                //     }else high = midRight;
                // }
                // // 跳出来就是未找到.
                // right = mid;
            }else{ // (nums[mid] > nums[left]) 错误!
                // 左边排好序  左边二分查找
                if(target>=nums[left] && target <= nums[mid-1]){  // 错了  ——  23 4|512   找1就不行!
                    // left = mid + 1;
                    right = mid;
                }else{
                    left= mid + 1;
                }


                //-----下面的太慢了!
                // int low = left, high = mid;
                // while( low < high){
                //     int midLeft = low+ (high-low)/2;
                //     if(nums[midLeft] == target){
                //         return true;
                //     }else if(nums[midLeft] < target){
                //         low = midLeft+1;
                //     }else high = midLeft;
                // }
                // // 说明左边未找到
                // left = mid + 1;
            }
           
        }
        // 循环结束,二分查找 还是没能找到,返回false
        return false;
    }

    // int binarySearch(vector<int> &nums, int target){
    //     int n = nums.size();
    //     if (n == 0) return -1;
    //     int left = 0;
    //     int right = n ;
    //     // 查找所以等于
    //     while (left < right)
    //     {
    //         int mid = left + (right - left)/2;
    //         if (nums[mid] == target)
    //         {
    //             return mid;
    //         }
    //         else if (nums[mid] > target)
    //         {
    //             right = mid;
    //         }
    //         else if (nums[mid] < target)
    //         {
    //             left = mid + 1;
    //         }
    //     }
    //     return -1; // 表示未查找到。
    // }
};


int main(){
    // vector<int> arr = {2,5,6,0,0,1,2};
    // vector<int> arr = {1,1,1,3,1};
    // vector<int> arr = {1};
    // vector<int> arr = {1,1,3};
    // vector<int> arr = {3,5,1};
    vector<int> arr = {2,2,2,0,1};

    int target = 0;
    // int target = 1;
    Solution so;
    bool answer = so.search(arr,target);
    cout<<"answer is : "<< answer << endl;

    system("pause");
    return 0;
}

【提升】实际应用

最常见的就是教科书上的例子,在有序数组中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。

PS:以上提到的三种二分查找算法形式在前文 二分查找算法详解 有代码详解,如果没看过强烈建议看看。
以及一些基础必备的应用问题求解见前文。

抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。

说起来玄乎得很,本文用「Koko 吃香蕉」和「货物运输」的问题来举个例子。

一、Koko 吃香蕉

在这里插入图片描述

也就是说,Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的最小速度(根/小时)。
如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。

那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢?

首先,算法要求的是「H小时内吃完香蕉的最小速度」,我们不妨称为speed,请问speed最大可能为多少,最少可能为多少呢?

显然最少为 1,最大为max(piles),因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到max(piles),一旦发现发现某个值可以在H小时内吃完所有香蕉,这个值就是最小速度:

int minEatingSpeed(int[] piles, int H) {
    // piles 数组的最大值
    int max = getMax(piles);
    for (int speed = 1; speed < max; speed++) {
        // 以 speed 是否能在 H 小时内吃完香蕉
        if (canFinish(piles, speed, H))
            return speed;
    }
    return max;
}

注意这个 for 循环,就是在连续的空间线性搜索,这就是二分查找可以发挥作用的标志。

由于我们要求的是最小速度,所以可以用一个搜索左侧边界的二分查找来代替线性搜索,提升效率:

int minEatingSpeed(int[] piles, int H) {
    // 套用搜索左侧边界的算法框架
    int left = 1, right = getMax(piles) + 1;
    while (left < right) {
        // 防止溢出
        int mid = left + (right - left) / 2;
        if (canFinish(piles, mid, H)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

PS:如果对于这个二分查找算法的细节问题有疑问,建议看下前文 二分查找算法详解 搜索左侧边界的算法模板,这里不展开了。

剩下的辅助函数也很简单,可以一步步拆解实现:

// 时间复杂度 O(N)
boolean canFinish(int[] piles, int speed, int H) {
    int time = 0;
    for (int n : piles) {
        time += timeOf(n, speed);
    }
    return time <= H;
}

int timeOf(int n, int speed) {
    return (n / speed) + ((n % speed > 0) ? 1 : 0);
}

int getMax(int[] piles) {
    int max = 0;
    for (int n : piles)
        max = Math.max(n, max);
    return max;
}

至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。

我的做法类似:分析:1、首先暴力搜索方法思考 2、其次,用二分搜索,替代暴力搜索 3、直接二分搜索可以的堆树的 左侧界,吃的天数和H相比的左侧界
完整代码:


#include <iostream>
#include <windows.h>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>

using namespace std;


class Solution {
public:
    int minEatingSpeed(vector<int>& piles, int H) {
        int n = findMaxNum(piles);
        // cout << "n:" << n << endl;
        if(n==0) return 1;
        int left=1, right = n;  //至多至少 吃的根数
        while(left <= right){   //采用左闭右闭
            int mid = left + (right - left) /2;
            int days = canEatInV(piles, mid);
            // cout << "mid v:" << mid << " ;eat days:" << days << endl;
            if(days <= H){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        if( left >= n+1 || canEatInV(piles, left) > H) return -1;
        return left;
    }
    int canEatInV(vector<int> piles, int v){
        int days=0;

        for(auto p:piles){
            days+=ceil((float)p/v);
            // cout << "caneat:" << days ;
        }
        // cout << "Caneat End" << endl;
        return days;
    }
    int findMaxNum(vector<int> piles){
        // vector<int>::iterator p = piles.begin();
        int max = 0;
        for(auto p : piles){
            if(p>max){
                max = p;
            }
        }
        return max;
    }
};

int main(){

    // vector<int> arr = {3,6,7,11};
    vector<int> arr = {30,11,23,4,20};
    // int H = 8;
    int H = 5;
    // int H = 6;
    Solution so;
    int ans = so.minEatingSpeed(arr, H);
    cout << "koko最少的吃香蕉速度为:" << ans << endl;

    system("pause");
    return 0;
}

【重点:】学会使用:days+=ceil((float)p/v);

二、包裹运输问题

类似的,再看一道运输问题:

在这里插入图片描述

要在D天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为cap)?
其实本质上和 Koko 吃香蕉的问题一样的,首先确定cap的最小值和最大值分别为max(weights)和sum(weights)。

类似刚才的问题,我们要求最小载重,可以用 for 循环从小到大遍历,那么就可以用搜索左侧边界的二分查找算法优化线性搜索:

// 寻找左侧边界的二分查找
int shipWithinDays(int[] weights, int D) {
    // 载重可能的最小值
    int left = getMax(weights);
    // 载重可能的最大值 + 1
    int right = getSum(weights) + 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canFinish(weights, D, mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

// 如果载重为 cap,是否能在 D 天内运完货物?
boolean canFinish(int[] w, int D, int cap) {
    int i = 0;
    for (int day = 0; day < D; day++) {
        int maxCap = cap;
        while ((maxCap -= w[i]) >= 0) {
            i++;
            if (i == w.length)
                return true;
        }
    }
    return false;
}

通过这两个例子,你是否明白了二分查找在实际问题中的应用呢?

首先思考使用 for 循环暴力解决问题,观察代码是否如下形式:

for (int i = 0; i < n; i++)
    if (isOK(i))
        return answer;

如果是,那么就可以使用二分搜索优化搜索空间:如果要求最小值就是搜索左侧边界的二分,如果要求最大值就用搜索右侧边界的二分。

和上个一样,知道了也没啥了。重点学到了 返回值有多个时的处理方法。

我的完整代码:

#include <iostream>
#include <windows.h>
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>

using namespace std;

class Solution {
public:
    /**
     * @description: 1、首先寻找所有货物重量 2、查找当前mid载重的天数 3、二分查找,找最低载重——即左侧界
     * @param {*}
     * @return {*}
     * @notes: 注意:最低包裹限制要  大于等于最小的包裹。
     */
    int shipWithinDays(vector<int>& weights, int D) {
        auto pair = findMaxMinBoatWeight(weights);
        int n = pair.first, max = pair.second;
        if(n==0) return 1;
        int left = max,right = n;
        // cout << "max:" << max << ",n:" << n << endl;
        while(left<=right){
            int mid = left + (right - left)/2;
            int days = canDaysInboat(weights, mid);
            if(days <= D){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        if(left >= n+1 || canDaysInboat(weights,left)>D) return -1;
        return left;
    }
    int canDaysInboat(vector<int> weights, int boat){
        int sum = 0, days=1;
        for(auto w : weights){
            sum += w;
            if(sum>boat){
                days+=1;
                sum = w;
            }
        }
        return days;

    }
    pair<int,int> findMaxMinBoatWeight(vector<int> weights){
        int sum = 0,max = 0;
        for(auto w : weights){
            sum+=w;
            max = w > max ? w : max ;
        }
        pair<int,int> res = {sum, max};
        return res;
    }
};


int main(){

    vector<int> arr = {1,2,3,4,5,6,7,8,9,10};
    // vector<int> arr = {3,2,2,4,1,4};
    // vector<int> arr = {1,2,3,1,1};
    int D = 5;
    // int D = 3; 
    Solution so;
    int ans = so.shipWithinDays(arr, D);
    cout << "船只的最低载重量为:" << ans << endl;

    system("pause");
    return 0;
}

参考

  1. labuladong的算法小抄.
  2. 具体篇章——二分搜索.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值