算法-二分查找

算法简介

我们之前可能默认为只有数组是有序就可以用二分查找,但其实不是有序数组只要是数组有规律、具有二段性就可以用二分查找,并且这个算法是有对应三个模板的,下面会对他们进行介绍,强调一点:要先明白算法原理(即为什么可以这样做),模板才是有用的。

一、朴素的二分模板

二分查找
1、分析:这道例题很简单,分析题目给出的示例1,用暴力算法肯定是遍历所有元素一个个做比较;那假设先随便选取一个数:3,跟target对比之后发现,3以及左边的数据一定比target小,此时搜索区间就变成了[3,5],会发现即使一开始没有找中间点我们也可以排除一些数据,将数组分成有效区间和无效区间,这正是二段性的体现。我们选取中间点其实是因为这样的数学期望高,你选取1/3点和1/4点作为分界也是没有问题的,但是还是推荐1/2处点。

2、代码实现:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right= nums.size()-1;
        while(left <= right)
        {
            int mid = left + (right - left) /2;//防止大数相加溢出
            if(nums[mid] < target)
                left = mid +1;
            else if(nums[mid] > target)
                right = mid -1;
            else return mid;
        }
        return -1;
    }
};

模板实现:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right= nums.size()-1;
        while(left <= right)
        {
            int mid = left + (right - left) /2;//防止大数相加溢出
            if(........)
                left = mid +1;
            else if(.........)
                right = mid -1;
            else return ..........;
        }
        return -1;
    }
};


3、注意事项:(1)边界条件:当left和right相等时,区间[left,right]的值仍然未知,我们还是要继续找mid跟target作比较。
(2)如果left和right太大,大数相加会导致大整数溢出,所以我们用left+(right - left) / 2来替代。
(3)left+(right - left) / 2和left+(right - left+1) / 2在这个模板并没有区别,数组元素个数为奇数结果一样,为偶数后者大前者一,但是并不影响最终结果。

二、查找区间左右端点模板

例题一

在排序数组中查找元素的第一个和最后一个位置

1、分析:数组是非递减排列,也就是会出现多个相等的字符。这道题用暴力算法时间复杂度是O(N),如果用前面提到的朴素的二分查找算法,看似好像可以,找到目标值后向左向右寻找端点,但是如果数组元素都一样,最后算法时间复杂度会回退成O(N)。我们可以尝试对朴素的二分查找算法改造分别找出目标区间的左端点和右端点。
(1)左端点查找:target / t= 3,x = nums[mid]


以上图为例:我们把数组根据有序二段性分成<t 和 >=t 两部分。
核心思路定义left = 0 , right = nums.size() - 1。当 x < t 时,代表x以及x左边的值都不满足条件,都淘汰,应该更新left = mid +1;当x >= t时,代表 x 右边的值不满足条件,但是x可能满足条件(x恰好是所求区间的最左端点),所以right = mid,不能是 right = mid - 1。
注意细节
a. 这里的循环条件不是像朴素二分查找一样为(left <= right),应该是(left < right)

  • 假设最后有最左端点,当left==right,left就是最左端点
  • 若数组元素全大于t,[4 4 4 4 4 4 4 4],right一直左移,最后也会left==right
  • 数组元素全小于t,[1 1 1 1 1 1 1],right一直左移,最后也会left==right

以上情况只要多一步判断,nums[left/right]是否等于target即可,所以循环不需要加入==,==就
直接退出循环。如果循环条件是left<=right,当left==right,进入循环。若x>=t,right不动,依旧是left==right,此时陷入死循环。
b. 求中点的方式:前面的朴素二分查找是mid = left + (right - left) / 2 或者 mid = left + (right - left + 1) / 2都可以。但是在求最左端点应该选择mid = left + (right - left) / 2,在left + 1 == right (两个元素的偶数组)这种情况下,选择mid = left + (right - left + 1) / 2的话,进入循环,mid = right,当x >= t 时,right不动,会死循环。

(2)右端点查找和左端点类似,target / t= 3,x = nums[mid]
以上图为例:我们把数组根据有序二段性分成 >t 和 <=t 两部分。
核心思路:定义left = 0 , right = nums.size() - 1。当 x > t 时,代表x以及x右边的值都不满足条件,都淘汰,应该更新 right = mid -1;当x <= t时,代表 x 左边的值不满足条件,但是 x 可能满足条件(x恰好是所求区间的最右端点),所以left = mid,不能是 left = mid + 1。
注意细节
a. 跟左端点一样注意循环条件。
b. 求中点的方式:在求最右端点应该选择mid = left + (right - left +1) / 2,在left + 1 == right (两个元素的偶数组)这种情况下,选择mid = left + (right - left ) / 2的话,进入循环,mid = left,当x <= t 时,left 不动,会死循环。

2、代码实现:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.empty())    return {-1,-1};
        vector<int> ret;
        int left = 0,right =nums.size()-1;
        int mid = 0;
        while(left < right)
        {
            mid = left + (right - left) /2;
            if(nums[mid] < target) left= mid +1;
            else right = mid;
        }
        if(nums[left] == target)    ret.push_back(left);
        else ret.push_back(-1);

        left = 0, right = nums.size()-1, mid = 0;
        while(left < right)
        {
            mid = left + (right - left + 1) /2;
            if(nums[mid] > target) right= mid -1;
            else left = mid;
        }
        if(nums[left] == target)    ret.push_back(left);
        else ret.push_back(-1);

        return ret;
    }
};

模板实现

//查找区间左端点模板
while(left < right)
{
    int mid = left + (right - left) / 2;
    if(......)  left = mid + 1;
    else right = mid;
}

//查找区间右端点模板
while(left < right)
{
    int mid = left + (right - left + 1) / 2;
    if(......)  left = mid ;
    else right = mid - 1;
}

这个模板只要记住mid的求法,对于核心思路 if...else 这里要自己分析,不要死记硬背,当你分析出来right = mid - 1,那么mid那里就要 + 1,就这么简单,前提是要根据二段性得出核心思路。下面再举一些例题更好地理解这个模板。

例题二

山脉数组的峰顶索引

这个例题我们可以抽象出来如下图:
1、分析:由图不难看出,当找到第一个数的值比后面的值大时,这个数就是我们要找的。如果用遍历的话,当下坡只有一个数时,就相当于从头遍历到尾,此时算法复杂度是O(N)。但是如果我们把数组分成两部分:根据arr[i] > arr[i-1]和arr[i] < arr[i-1]分成两个区间,不就变成找最右端点的模板吗?

2、核心思路:当arr[i] > arr[i-1]时(这里的i就是mid),需要更新left = mid(不能是mid +1,如果mid刚好是峰顶的话);arr[i] < arr[i-1]时,要更新right = mid - 1;那mid的求法是不是要加上1啊。

3、代码实现:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0,right = arr.size()-1;
        while(left < right)
        {
            int mid = left + (right - left +1)/2;
            if(arr[mid] > arr[mid-1])   left = mid;
            else right = mid-1;
        }
        return left;
    }
};

4、另一种方法:这里同样也可以把数组分成下面这种形式:

注意划分条件以及核心思路,用的是寻找最左端点模板。下面给出代码:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0,right = arr.size()-1;
        while(left < right)
        {
            int mid = left + (right - left )/2;
            if(arr[mid] > arr[mid+1])  right = mid; 
            else left = mid +1;
        }
        return left;
    }
};

例题三

寻找峰值

1、分析:这道题需要我们找到数组中第一个这样的数:它的值比下一个数大,仔细看题目,nums[-1] = nums[n] = -∞由这个可以看出,数组的峰值会存在三种情况:第一个元素就比第二个元素大,由于nums[-1] = -∞,所以第一个元素是峰值;指针走到中间才找到有一个值比下一个数大;
走到最后一个数,都是递增的,这个也算峰值,因为nums[n] = -∞,这个时候就相当于是遍历整个数组了,时间复杂度已经达到了O(N)。由于题目1 <= nums.length <= 1000,所以用遍历也未尝不可,这里就不演示了。
接着分析是否能用二段性解决,虽然数组可能包含多个峰值,不像上道题可以分成一组严格的上升子数组和一组严格下降的子数组,但是我们也可以通过判断 nums[mid] 的值是否大于 nums[mid+1] 的值来舍弃一些不确定地区间,如下图分析:mid就是i

当arr[i] > arr[i+1]时,虚线左边一定有峰值(nums[-1] = -∞),右边不确定有没有峰值,那就舍弃右边的所有元素;
当arr[i]< arr[i+1]时,虚线右边一定有峰值(nums[n] = -∞),左边不确定有没有峰值,那就舍弃左边的所有元素。


2、核心思路:arr[i] > arr[i+1],更新right = mid;arr[i]< arr[i+1],更新left = mid +1,先写核心思路后,发现没有减1的情况,那么mid = left + (right - left) / 2。

3、代码实现

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right - left)/2;
            if(nums[mid] > nums[mid+1]) right = mid;
            else left = mid+1;
        }
        return left;
    }
};

例题四

寻找旋转排序数组中的最小值

1、分析:这道题目的意思是输入的数组原来是一个升序数组旋转了n次后的数组。看例子和题目应该可以理解,很明显我们需要找到一个数比前一个数小,其实就是最小值,并返回它。那这样的极端情况就是这个最小值在最后,时间复杂度为O(N)。题目要求时间复杂度为O(logN),提醒我们用二分查找。
单看例子可能看不出什么二段性,把数组抽象出来:

不难发现,虚线左边的值是不是都比最后一个元素大,右边的值都比最后一个元素要小于等于(右半部分可能只有一个数)。那是不是就可以根据这个性质来解这道题了,我们就需要来找右半部分的最左端点。

2、核心思路:定义x 为数组最后一个元素nums[size-1],当nums[mid] > x,mid落在左半部分,更新left = mid + 1;当nums[mid] <= x,mid落在右半部分,更新right = mid 。不知道有没有人疑惑,要是数组都是递增,那这个思路还能得出结果吗?数组都是递增的情况,那么nums[mid] <= x,right相当于一直往左移动了,因为此时mid = left + (right - left)/2,最后right会等于left,退出循环,返回的nums[left]不就是我们要的第一个元素吗,所以这种情况也是可以满足的。

3、代码实现

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        int x = nums[right];
        while(left < right)
        {
           int mid = left + (right-left)/2;
           if(nums[mid] > x) left = mid+1;
           else right = mid; 
        }
        return nums[left];
    }
};

4、我们刚刚使用数组最后一个元素nums[size-1]作为二段性划分,那能不能用第一个元素呢?
假设定义第一个元素为y,虚线左边的值是不是要大于等于它,右边的值一定小于它。当nums[mid] >= y,mid落在左半部分,更新left = mid + 1;当nums[mid] < x,mid落在右半部分,更新right = mid 。还是一样,要是数组都是递增,那这个思路还能得出结果吗?随便输入一个递增测试用例[11,13,15,17]都是不通过的,因为mid一直落在左半部分,一直更新left = mid + 1,最后left怎么可能变成0,return nums[left]怎么可能正确呢?
那这样子就代表这个办法不可行吗?可能有同学会想到,这种情况right一直不动,我只要在循环退出时判断,right是否还是nums.size()-1不就可以了吗?是就返回第一个元素,不是就正常返回return nums[left]。但是有没有可能 [4,5,6,7,0] 这个用例,right最后还是nums.size()-1。别灰心,再仔细观察一下就可以得出处理办法了[11,13,15,17] 和 [4,5,6,7,0]虽然right最后都是nums.size()-1,但是nums[right] 和 nums[0]的大小情况是不是不一样,只要多一步判断就可以解决咯:if(right == nums.size()-1 && nums[right]>nums[0]) return nums[0];

代码实现

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        int y = nums[0];
        while(left < right)
        {
           int mid = left + (right-left)/2;
           if(nums[mid] >= y) left = mid+1;
           else right = mid; 
        }
        //[4,5,6,7,0]和[11,13,15,17]:right不动,但是nums[right]>nums[0]?
        if(right == nums.size()-1 && nums[right]>nums[0]) return nums[0];
        return nums[left];
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值