「数组」二分查找模版|二段性分析|恢复二段性 / LeetCode 35|33|81(C++)

目录

概述

思路

二分查找模版

核心概念:二段性

二段性分析

恢复二段性

复杂度

总结


概述

在一个有序数组中找到第一个大于k的数,这该怎么做?

我们当然可以进行暴力枚举,但是这显然不是最优的做法。

既然这是一个有序数组,我们能不能跳过一些无意义的垃圾区域呢?

今天我们来谈:二分查找。


思路

二分查找的第一个问题就是红蓝染色问题。

在一个有序递增数组中找到第一个大于等于target的数:

将小于target的数都标记为蓝色,大于等于target的标记为红色。那么我们要寻找的就是红蓝边界处的右侧首元素。

LeetCode 35:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

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

先来看这段代码: 

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l=-1,r=nums.size();
        while(l+1!=r){
            int m=(l+r)/2;
            if(nums[m]<target)l=m;
            else r=m;
        }
        return r;
    }
};

我们定义了左右指针l和r,他们起初各自指向数组的-1位置和n位置,也就是前后的两个无效位置。

随后每次都取中间值m:

如果中间值小于target,就转入m右侧区域[m,r]寻找。 

如果中间值大于等于target,就转入m左侧区域[l,m]寻找。 

这一切的根源是因为数组是有序的。如果l到m都是蓝色的,那[l,m]内一定没有我们需要的红蓝边界。

这也叫折半查找:每次都将数组对半分开,在分开处判断其颜色,然后转入我们需要的区域继续寻找。 

这样做的本质其实就是选择性的跳过大量的垃圾区域,你会注意到其时间复杂度是logn级别的。


二分查找模版

红蓝染色模版就是最基本的二分查找模版。

你会见到各种不同的模版,但是我想提供最清晰明了的一种:

int binary_serach(vector<int>& nums, int target) {
    const int n=nums.size();
    int l=-1,r=n;
    while(l+1!=r){
        int m=(l+r)/2;
        if(is_blue(m))l=m;
        else r=m;
    }
    return r;//或return l,这取决于你的目的
}

我们来看几个细节:

①int l=-1,r=n;

这样使得我们的m有机会取到数组中的每一个位置,包括0和n-1。

②while(l+1!=r)

有些模版将终止条件设为l>r,但是明显不够直观:我们应该严格限制l始终在r的左侧,终止时l与r恰好挨在一起。

③if(is_blue(m))l=m;else r=m;

注意到:我们总将蓝色m赋值给l,红色m赋值给r。结合上一条你会发现:当循环结束时,l与r挨在一起,且各分属红蓝两区,即l与r之间即为红蓝边界,我们只需要返回我们需要的位置即可。

此外,你会注意到:当数组全为红时,l总是-1,;当数组全为蓝时,r总是n;


核心概念:二段性

二段性分析

在基本的红蓝染色法之中我们发现:只需要is_blue(m)总是在数组的左侧区域返回true值就将数组这部分判断为蓝色。

[0,blue]是一段蓝色区域,[red,n-1]是一段红色区域,且blue+1==red,至于红和蓝的判定规则,可以不仅仅是对target比大小这么简单。

LeetCode 33:

整数数组 nums 按升序排列,数组中的值 互不相同,独一无二 。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:

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

示例 2:

输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

Code 

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l=-1,r=nums.size();
        auto to_left=[&](int m)->bool{
            int x=nums[m],end=nums.back();
            if(x>end)return target<=x&&target>end;
            return target>end||target<=x;
        };
        while(l+1!=r){
            int m=(l+r)/2;
            if(to_left(m))r=m;
            else l=m;
        }
        return nums[r]==target?r:-1;
    }
};

to_left内部的规则是:{

如果:中间位置的值大于数组末尾(即第二段有序区的末尾),那么可以断定此处为第一段有序区,此时如果target也在第一段有序区(target也大于数组末尾)且target<=nums[m]

或:中间位置的值小于等于数组末尾,那么可以断定此处为第二段有序区,如果target在第一段有序区或target<=nums[m]

这两种情况都应该转入左侧寻找。

}

非有序数组的二分查找看起来很诡异,但这透露出二分查找的本质:二段性。

在本题中,蓝色区域总满足大于末尾元素且小于target,红色区域总满足小于等于末尾元素或大于等于target。

在上文的二分查找基本模版中,二段性分析是简单的if(nums[m]<target),而基于这道题,数组有两段有序区,但归根结底还是可以基于二段性分为一段blue区和一段red区。

我们鲜明的认识到这样一个二段性结论:

所谓二段性,就是数组一定有一段左侧区域严格满足某一性质(即is_blue返回true),右侧严格满足另一性质,这两种性质必须是严格互斥的,这两段中间即为二段性边界。

恢复二段性

请牢记上述的二段性结论,我们来看这道题:

LeetCode 81:

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 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

此题与上一题唯一的不同就是数字是重复出现的了,这导致了什么呢?

其实,它几乎与上题没什么区别,除了有些数据破坏了二段性而已。 

如你所见,[2,5,6,0,0,1,2]就破坏了二段性:

[2,5,6],满足的是大于等于2,[0,0,1,2]满足的是小于等于2,而:二段性就是数组一定有一段左侧区域严格满足某一性质(即is_blue返回true),右侧严格满足另一性质,这两种性质必须是严格互斥的。

大于等于2和小于等于2并不是严格互斥的,所以本题的一些数据会卡掉上一题的二分查找代码,那怎么解决呢?恢复二段性不就好了。

从数组首尾同时删数,一直到首尾元素不同,我们就实现了两段数字的性质严格互斥。

Code

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        while(!nums.empty()&&nums.front()==nums.back()){
            if(target==nums.back())return true;
            else nums.pop_back();
        }
        if(nums.empty())return false;
        auto is_blue=[&](int m)->bool{
            if(nums[m]>nums.back())return target>nums.back()&&target<=nums[m];
            else return target<=nums[m]||target>nums.back();
        };
        int l=-1,r=nums.size();
        while(l+1!=r){
            int m=(l+r)/2;
            if(is_blue(m))r=m;
            else l=m;
        }
        return nums[r]==target;
    }
};

复杂度

时间复杂度:O(logn)

空间复杂度:O(1)

总结

二分查找的本质就是利用数组的二段性进行快速的分析,希望你理解了我们的二段性分析和恢复二段性的思想。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值