【二分法】算法总结/力扣题目整理


力扣官方总结

二分原理

场景分析: 当涉及需要在一个有限区间中筛选出满足指定条件的数据时,最直接的方法是遍历区间逐个排查,此时时间复杂度为O(n)。当 区间数据满足条件: \textcolor{red}{区间数据满足条件:} 区间数据满足条件:每次从区间中 任一位置 \textcolor{red}{任一位置} 任一位置 x i x_i xi处进行条件判断,即可知目标值是在左区间[0, x i x_i xi]还是右区间[ x i + 1 x_{i+1} xi+1, x n x_n xn],利用此条件可每次 省去一侧区间 \textcolor{red}{省去一侧区间} 省去一侧区间的查找次数,将时间复杂度降低到O(log n)。

理论分析: 这种规律排布的区间满足一个性质:在区间[x0,x1,…xn-1]中任意一个位置xi,通过比较xi与target的关系即可判断目标值所在的区间为左区间[x0,…xi]还是在右区间[xi,…xn],于是便省去了另一区间的遍历时间。划分目标所在的子区间后,子问题仍可使用这种方法不断迭代省去在另一个区间的遍历。如果每次从区间中点开始判断,则每个子过程都省略了一半区间长度的遍历,这就是二分查找,查找需要的次数n满足 2 n = N 2^n = N 2n=N(N为数组长度)时间复杂度为O(log n)

区间条件: 有序或无序都可利用二分法优化查找,关键在于 在任意元素处能否划分区间,从而收缩范围减少遍历次数 \textcolor{purple}{在任意元素处能否划分区间,从而收缩范围减少遍历次数} 在任意元素处能否划分区间,从而收缩范围减少遍历次数
1.区间有序时:
①无重复元素且按照升序/降序排列

②有重复元素但非递增/非递减
2.区间无序时:
适用于以下情况:每次从中值进行条件判断后即可判断目标值所在区间。例如判断区间峰值(见下文力扣题目)。

以下图为例说明区间有序时,可收缩区间查找目标值:
在区间任一点 x i x_i xi(i = 5),数组1和数组2的值都小于target,由此可判断出数组1中目标值所在范围为[6,12],但不能判断数组2中目标值是在左区间还是右区间,因此不能缩减范围
在这里插入图片描述

三种形式

二分查找有三种书写规范,循环条件、边界情况、应用场景各不相同,初见难以厘清,但只要牢牢抓住二分法的核心思想,也就是收缩区间、判断中值,也就容易根据情况选择了。
核心思想:
规定好边界收缩何时终止:根据每次循环区间至少包含几个元素,终止条件分为以下几种方式:
1.至少一个元素:
终止条件: w h i l e ( l e f t < = r i g h t ) while(left <= right) while(left<=right)
每次循环中区间至少包含一个元素,在每个循环中只能获取中点的信息。
边界如何收缩: 左右边界均收缩到中点之外
因为最后一次循环时只有一个元素,此时 n u m s [ l e f t ] = n u m s [ m i d ] = n u m s [ r i g h t ] nums[left] = nums[mid] = nums[right] nums[left]=nums[mid]=nums[right],无论是从右向左收缩还是从左向右收缩,都必须使得 l e f t > r i g h t left > right left>right才能终止循环。
中点小于目标值,目标值在中点右侧,收缩左边界: l e f t = m i d + 1 ; left = mid + 1; left=mid+1;
中点大于目标值,目标值在中点左侧,收缩右边界: r i g h t = m i d − 1 ; right = mid - 1; right=mid1;

2.至少两个元素:
终止条件: w h i l e ( l e f t < r i g h t ) while(left < right) while(left<right)
每次循环中区间至少包含两个元素,因此在每个循环中可以获取中点及相邻一侧元素的信息。
边界如何收缩: 一侧边界收缩到中点,另一侧边界收缩到中点之外
每一次循环,区间都存在中点和中点一侧元素,因为至少存在两个元素才能进入循环查找,因此边界收缩时需要至少多留一个空位,否则在特殊情况下会忽略掉mid相邻的元素,如下图,要找到target = 0的位置,至少需要缩减到[0,1]这个区间才能判断,当边界与mid相邻时右边界必须缩减到mid才能保证存在两个元素,否则会漏掉mid左侧元素的判断。
如果中点坐标的计算方式为 l e f t + r i g h t 2 \frac{left+right}{2} 2left+right,left最先与mid相邻,于是必须是右边界收缩增加余量。
因此边界收缩逻辑:
中点小于目标值,目标值在中点右侧,收缩左边界: l e f t = m i d + 1 ; left = mid + 1; left=mid+1;
中点大于目标值,目标值在中点左侧,收缩右边界: r i g h t = m i d ; right = mid ; right=mid;
在这里插入图片描述
边界收缩有两种方式,根据中点的定义不同,写法有所区别
在这里插入图片描述

循环结束后处理:
这种方式存在一个漏洞:边界从左向右收缩结束到 l e f t = r i g h t left = right left=right时,不能进入循环中进行条件判断,因此需要在循环之外单独在进行一次条件判断。

3.至少三个元素:
终止条件: w h i l e ( l e f t + 1 < r i g h t ) while(left + 1< right) while(left+1<right)
每次循环中区间至少包含三个元素,可以在最终循环中获取到中点及左右两侧元素的信息。
边界如何收缩: 左右边界均收缩到中点
中点小于目标值,目标值在中点右侧,收缩左边界: l e f t = m i d ; left = mid; left=mid;
中点大于目标值,目标值在中点左侧,收缩右边界: r i g h t = m i d ; right = mid; right=mid;

应用场景

一、无重复元素有序数组

1.查找数组中元素位置

★ 有序数组查找元素

力扣链接
题目描述:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

int binarySearch(vector<int>& nums, int target)
{
   int n = nums.size();
   if(n == 0)return -1;
   int left = 0, right = n-1;
   while(left <= right)//最后一次循环只剩下一个元素
   {
   	//防止越界处理
   	int mid = left + (right - left) / 2;
   	if(nums[mid] == target)return mid;
   	//根据当前值与target大小关系更新左右边界
   	else if(nums[mid] > target)right = mid - 1;
   	else if(nums[mid] < target){left = mid + 1;}
   }
   return -1;
}

2.查找元素插入位置

★ 搜索插入位置

力扣链接
题目描述:
给定一个排序数组(无重复)和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
思路分析:
这个应用场景是第一种的进阶版,即找到第一个大于等于目标值的位置,首先考虑边界收缩何时终止:while(left <= right)最后一次收缩时只剩下一个元素,该元素要么是target目标值,要么是目标值的左右元素之一。当目标值不存在于数组中时,对于无重复数组,右元素比目标值大,右元素位置即为按顺序插入的位置,左元素比目标值小,下个位置即为按顺序插入的位置。
代码参考:

int searchInsert(vector<int>& nums, int target)
{
	int n = nums.size();
	int l = 0, r = n-1;
	while(l <= r)
	{
		int mid = l + (r - l) / 2;
		if(nums[mid] < target){l = mid + 1;}
		else if(nums[mid] >= target){r = mid - 1;}
	}
	return l;
   //这里return l  和 return r+1 都可以的
}

3.求解问题

★ x的平方根

力扣链接
题目描述:
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
题目分析:
1.遍历的思想:遍历从0到x范围的整数,比较其平方与x的关系,找到第一个=x的i,返回i,或者第一个>x的i,返回i-1;使用二分法进行优化每次从x/2处进行判断,找到第一个平方值>=x的位置。
2.数学公式变换: x = e 1 2 ln ⁡ ( x ) \sqrt{x} = e^{\frac{1}{2}\ln(x)} x =e21ln(x)(原题要求不能使用sqrt函数)
3.数学求解法:牛顿迭代法求 f ( x ) = x 2 − c f(x) = x^2-c f(x)=x2c的零点,迭代公式: x i + 1 = 1 2 ( x i + C x i ) x_{i+1} = \frac{1}{2}(x_i + \frac{C}{x_i}) xi+1=21(xi+xiC),取初值为c,当 x i + 1 − x i x_{i+1}-x_i xi+1xi足够小时为近似解。
在这里插入图片描述
二分法代码:

class Solution {
public:
    int mySqrt(int x) {
       long long left = 0, right = x;
       while(left <= right) 
       {
            long long mid = left + (right - left)/2;
            if(mid * mid < x)left = mid + 1;
            else if(mid * mid == x)return mid;
            else right = mid - 1;
       }
       return left * left > x ? left - 1 : left;
    }
};
//优化版
class Solution {
public:
    int mySqrt(int x) {
       int left = 0, right = x, ans = -1;
       while(left <= right) 
       {
            int mid = left + (right - left)/2;
            if((long long)mid * mid <= x)
            {
                left = mid + 1;
                ans = mid;
            }
            else right = mid - 1;
       }
       return ans;
    }
};

公式转换法代码:

class Solution {
public:
    int mySqrt(int x) {
        int ans = exp(0.5*log(x));
        return (long long)(ans+1) * (ans+1) <= x ? ans+1 : ans;
    }
};

牛顿迭代法代码:

class Solution {
public:
    int mySqrt(int x) {
        if(x == 0)return 0;
        double x0 = x,x1 = 0,c = x;
        while(true)
        {
            x1 = 0.5*(x0+c/x0);
            if(fabs(x0-x1) < 1e-7)break;
            x0 = x1;
        }
        return (int)x1;
    }
};

4.查找区间

★ 找到K个最接近的元素

力扣链接
题目描述:
给定一个 排序好 的数组 arr ,两个整数 k 和 x ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。

整数 a 比整数 b 更接近 x 需要满足:

|a - x| < |b - x| 或者
|a - x| == |b - x| 且 a < b
题目分析:
因为是升序无重复元素的数组,最终结果一定是一个连续的区间,可以先找到第一个最接近x的数,然后从这个位置向左右扩散寻找。
法一:开辟一个数组用来保存结果
法二:使用双指针保存区间左右边界,返回临时对象
二分法一:

class Solution {
public:
    vector<int> findClosestElements(vector<int>& arr, int k, int x) {
        vector<int> res;
        int l = 0, r = arr.size()-1;
        while(l <= r){
            int m = l + (r - l) / 2;
            if(arr[m] < x){
                l = m + 1;
            }
            else if(arr[m] >= x){
                r = m - 1;
            }
        }
        int i = -1,j = 0;
        while(res.size() < k){
            if(l+i < 0){
                res.push_back(arr[l+(j++)]);
            }
            else if(l+j >= arr.size()){
                res.push_back(arr[l+(i--)]);
            }
            else if(l + i >= 0 && l + j < arr.size()){
                res.push_back(abs(arr[l+i]-x)>abs(arr[l+j]-x)?arr[l+(j++)]:arr[l+(i--)]);
            }
        }
        sort(res.begin(),res.end());
        return res;
    }
};

二分法+双指针:

class Solution {
public:
    vector<int> findClosestElements(vector<int>& arr, int k, int x) {
        int l = 0, r = arr.size()-1;
        while(l <= r){
            int m = l + (r - l) / 2;
            if(arr[m] < x){
                l = m + 1;
            }
            else if(arr[m] >= x){
                r = m - 1;
            }
        }
        //上面求得的l为第一个大于等于x的下标
        //将其作为双指针的起始右边界
        //左边界为其-1
        r = l;
        l = r - 1;
        //注意l是左边界-1的位置,r也是右边界+1的位置
        //所以返回的数组左右边界为begin + l + 1和end + r
        while(k--){
            if(l < 0){
                r++;
            }
            else if(r >= arr.size()){
                l--;
            }
            else if(x - arr[l] <= arr[r] - x){
                l--;
            }
            else {
                r++;
            }
        }
        return vector<int>(arr.begin()+l+1,arr.begin()+r);
    }
};

二、有重复元素有序数组

此类场景数组非递减/非递增,允许存在重复元素,也可看作有序.
元素的边界: 对于数组[0,1,1,4,6,8,8,8,9],[1,3,5,7,7,7,9,9]定义8的边界位置0,1,1,4,6, 8 \color{red}{8} 8,8,8, 9 \color{red}{9} 9、[1,3,5,7,7,7, 9 \color{red}{9} 9,9](左右边界位置都为红色9位置)。
对于此种存在重复元素的数组,边界收缩时需要考虑方向性,才能确定收缩至重复元素区间的左边界还是右边界,以下是此类问题的几种情况:

1.查找元素的左、右边界位置

★ lower_bound()

查找元素的左边界:
可转化为寻找第一个大于等于目标值的位置,对应c++标准库中的lower_bound()函数,其底层使用二分查找,输入开始位置和结束位置的迭代器类、目标值、和一个可选的仿函数对象(默认使用less仿函数,如果传入自定义lambda或者传入greater仿函数,效果就变成了寻找第一个小于等于目标值的位置)

//模板泛型编程,定义模板迭代器类型_FwdIt、数值类型_Ty、仿函数对象类型_Pr
_EXPORT_STD template <class _FwdIt, class _Ty, class _Pr>
_NODISCARD _CONSTEXPR20 _FwdIt lower_bound(_FwdIt _First, const _FwdIt _Last, const _Ty& _Val, _Pr _Pred) {
   // find first element not before _Val
   _Adl_verify_range(_First, _Last);
   //通过迭代器取出原始类数据
   auto _UFirst                = _Get_unwrapped(_First);
   //计算传入数据个数_Count 
   _Iter_diff_t<_FwdIt> _Count = _STD distance(_UFirst, _Get_unwrapped(_Last));

   while (0 < _Count) { // divide and conquer, find half that contains answer
	   //计算中点相对左边界偏移量_Count2
       const _Iter_diff_t<_FwdIt> _Count2 = _Count / 2;
       //取出该位置数据_UMid         
       const auto _UMid                   = _STD next(_UFirst, _Count2);
       //中点数据小于目标值,收缩左边界
       if (_Pred(*_UMid, _Val)) { // try top half
       	//相当于left = mid + 1
           _UFirst = _Next_iter(_UMid);
           //更新新的区间的元素个数
           _Count -= _Count2 + 1;
        //中点数据大于等于目标值,收缩右边界
       } else {
           _Count = _Count2;
       }
   }

   _Seek_wrapped(_First, _UFirst);
   return _First;
   _EXPORT_STD template <class _FwdIt, class _Ty>
_NODISCARD _CONSTEXPR20 _FwdIt lower_bound(_FwdIt _First, _FwdIt _Last, const _Ty& _Val) {
   // find first element not before _Val
   return _STD lower_bound(_First, _Last, _Val, less<>{});
}

简化后的lower_bound()代码:
和查找无重复数组中元素位置的代码几乎一致,只是多了一个操作:收缩时遇到重复元素,从右向左收缩
对应到代码中就是中点数据>=目标值的时候都收缩右边界

int lower_bound(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;
		}		
	}
	return left;
	//或者 return right + 1;
}
★ upper_bound()

查找元素的右边界位置:
可转化为查找第一个大于目标值的位置,对应upper_bound()函数,与lower_bound()不同点就在于当中点数据小于等于目标值的时候,需要从左到右收缩左边界,这样才能把所有重复元素过滤掉,
其底层实现:

_EXPORT_STD template <class _FwdIt, class _Ty, class _Pr>
_NODISCARD _CONSTEXPR20 _FwdIt upper_bound(_FwdIt _First, _FwdIt _Last, const _Ty& _Val, _Pr _Pred) {
    // find first element that _Val is before
    _Adl_verify_range(_First, _Last);
    auto _UFirst                = _Get_unwrapped(_First);
    _Iter_diff_t<_FwdIt> _Count = _STD distance(_UFirst, _Get_unwrapped(_Last));

    while (0 < _Count) { // divide and conquer, find half that contains answer
        _Iter_diff_t<_FwdIt> _Count2 = _Count / 2;
        const auto _UMid             = _STD next(_UFirst, _Count2);
        //这里_Val和 *_UMid的位置调换了一下,判断的是中点数据大于目标值(默认less的情况下)
        //当传入仿函数为less_greater的时候,upper_bound 就和lower_bound一样了
        if (_Pred(_Val, *_UMid)) {
            _Count = _Count2;
        } else { // try top half
            _UFirst = _Next_iter(_UMid);
            _Count -= _Count2 + 1;
        }
    }

    _Seek_wrapped(_First, _UFirst);
    return _First;
}

_EXPORT_STD template <class _FwdIt, class _Ty>
_NODISCARD _CONSTEXPR20 _FwdIt upper_bound(_FwdIt _First, _FwdIt _Last, const _Ty& _Val) {
    // find first element that _Val is before
    return _STD upper_bound(_First, _Last, _Val, less<>{});
}
	//这两个函数的仿函数并没什么大用,使用默认的就可以
	//下面的情况两个函数的效果是一样的,都是寻找第一个大于等于目标值的位置
	vector<int> v = { 5,6,7,7,9,9 };
	sort(v.begin(), v.end(),less<int>());

	iter1 = lower_bound(v.begin(), v.end(), 6, [](int a, int b) {return a < b; });//
	iter2 = upper_bound(v.begin(), v.end(), 6, less_equal<int>());//

简化后的代码:

int upper_bound(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;
		}		
	}
	return left;
	//或者 return right + 1;
}
★ 在排序数组查找元素的第一个和最后一个位置

力扣链接
题目描述:
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
题目分析: 这道题利用lower_bound()和upper_bound()函数非常简单,也可以在一次二分查找中确定左右边界:先找到左边界再向右逐个遍历
方法一:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int>::iterator first = lower_bound(nums.begin(),nums.end(),target);
        vector<int>::iterator last = upper_bound(nums.begin(),nums.end(),target);
        if(first == nums.end() || *first != target)return {-1,-1};
        return {static_cast<int>(first-nums.begin()), static_cast<int>(last == first ? first - nums.begin() : last - nums.begin() - 1)};
    }
};

方法二:

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

2.查找满足特定条件的元素

一般的数组查找问题是通过比较大小确定元素,而有些情境下查找的依据不是数值大小,而是特定条件。

★ 第一个错误的版本

力扣链接
题目描述:
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
题目分析:
这个数据区间满足一个特点:前k个数据都不满足isBadVersion,后面的数据都满足isBadVersion,需要找到满足isBadVersion的区间的第一个元素,这和寻找第一个大于等于特定值的元素是类似的,当中点为target或者target右邻元素时从元素右侧向左收缩,中点为target左侧元素时从左向右收缩。
参考代码:

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int left = 0, right = n;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(isBadVersion(mid))right = mid - 1;
            else left = mid + 1;
        }
        return left;
    }
};

三、无序数组

1.查找指定位置

★★ 搜索旋转排序数组

力扣链接
题目描述:
整数数组 nums 按升序排列,数组中的值互不相同 。
在传递给函数之前,nums 在预先 未知的某个下标 k \textcolor{red}{未知的某个下标 k} 未知的某个下标k(0 <= k < nums.length)上进行了 旋转 \textcolor{red}{旋转} 旋转,使数组变为 [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 。
题目分析:
容易发现旋转后的数组被分割成了两个有序区间:左区间任何一个元素都比右区间的大,可以通过边界值判断目标值所在半区,然后逐步将区间向此半区收缩,在单独的半区使用二分查找很容易。
在这里插入图片描述
问题在于每次迭代中点所在半区位置都是不确定的,由此可以划分出四种情况:
在这里插入图片描述

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0,right = nums.size()-1;
        if(right == -1)return -1;
        while(left <= right)
        {
        	//此处bound按照left定义,意味着我们把一个升序序列定义为左半区
        	//如果bound按照right定义,意味着将一个升序序列定义为右半区
        	//这个定义方式需要根据实际情况,当面对一个升序序列,我们是希望他按照左半区还是右半区定义
        	//左半区和右半区的区别就是向中点收缩的方向不同,如果要找最值,需要考虑如何处理一个升序序列
            int bound = nums[left];
            int mid = left + (right - left)/2;
            //target和中点都在左半区
            if(nums[mid] >= bound && target >= bound)
            {
                if(nums[mid] < target)left = mid + 1;
                else if(nums[mid] == target)return mid;
                else if(nums[mid] > target)right = mid - 1;
            }
            //中点在左半区,target在右半区
            else if(nums[mid] >= bound && target < bound)
            {
                left = mid + 1;
            }
            //中点和target都在右半区
            else if(nums[mid] < bound && target < bound)
            {
                if(nums[mid] < target)left = mid + 1;
                else if(nums[mid] == target)return mid;
                else if(nums[mid] > target)right = mid - 1;
            }
            //中点在右半区,target在左半区
            else if(nums[mid] < bound && target >= bound)
            {
                right = mid - 1;
            }
        }
        return -1;
    }
};
★ 搜索旋转排序数组 II

力扣链接
题目描述:
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= 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 。
题目分析:
在I的基础上条件改为有重复元素,这样边界处左右区间的值有可能相等,无法通过直接比较中点和边界的大小判断中点位置,在 a [ l ] = a [ m i d ] = a [ r ] a[l]=a[mid]=a[r] a[l]=a[mid]=a[r]情况下,只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。
代码参考:

class Solution {
public:
 int search(vector<int>& nums, int target) {
     int left = 0,right = nums.size()-1;
     if(right == -1)return -1;
     while(left <= right)
     {
         int mid = left + (right - left)/2;
         if(nums[mid] == target)return true;
         //首先排除num[mid] = nums[left] = nums[right]
         if(nums[mid] == nums[left] && nums[mid] == nums[right])
         {
             left++;
             right--;
             continue;
         }
         //target和中点都在左半区
         if(nums[mid] >= nums[left] && target >= nums[left])
         {
             if(nums[mid] < target)left = mid + 1;
             else if(nums[mid] == target)return true;
             else if(nums[mid] > target)right = mid - 1;
         }
         //中点在左半区,target在右半区
         else if(nums[mid] >= nums[left] && target < nums[left])
         {
             left = mid + 1;
         }
         //中点和target都在右半区
         else if(nums[mid] < nums[left] && target < nums[left])
         {
             if(nums[mid] < target)left = mid + 1;
             else if(nums[mid] == target)return true;
             else if(nums[mid] > target)right = mid - 1;
         }
         //中点在右半区,target在左半区
         else if(nums[mid] < nums[left] && target >= nums[left])
         {
             right = mid - 1;
         }
     }
     return false;
 }
};
★★ 寻找旋转排序数组的最小值

力扣链接
题目描述:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素
在这里插入图片描述
题目分析:
旋转排序数组如上图所示,分为两种情况.要想找最值,就要根据是找最大还是最小值规定区间如何收缩,显然找最小值时,需要从右边界收缩至最值点,那么我们只需判断当前点属于哪个区间,如果在左区间,则收缩左边界,如果是右区间,则收缩右边界,这样就能保证区间向最值位置收缩。

最小值时:如何收缩所在区间?
对于排序数组1这种形式:
mid点大于边界点,则mid位于区间,找最小值需要收缩边界
mid点小于等于边界点,则mid位于区间,找最小值需要收缩边界
对于排序数组2这种形式:
mid点永远小于等于边界点,找最小值需要收缩边界

最大值时:如何收缩所在区间?
对于排序数组1这种形式:
mid点大于等于边界点,则mid位于区间,找最小值需要收缩边界
mid点小于边界点,则mid位于区间,找最小值需要收缩边界
对于排序数组2这种形式:
mid点永远大于等于边界点,找最大值需要收缩边界

简单总结如何挑选左边界还是右边界作为区间判定的依据:如果想要将数组2看成是数组1中的左区间,则以左边界进行区间判断,如果想要将数组2看成是数组1中的右区间,则以右边界进行区间判断,而找最小值时,需要将数组2看成是数组1中的右区间,这样写出的代码如下:
代码参考:

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

找最大值时:

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

注意找最大和最小值二分法的写法有细微差别,为什么一个 l = m i d + 1 , r = m i d l = mid + 1,r = mid l=mid+1,r=mid,一个却是 l = m i d , r = m i d − 1 l = mid, r = mid - 1 l=mid,r=mid1
找最小值时,最后一次循环的两个数,最小值在左边,需要从右向左逼近,来结束循环,这样直接返回nums[left]就是最终结果
找最大值时,最后一次循环的两个数,最大值在右边,需要从左向右逼近最大值
这只针对升序序列。
对于降序序列,就反过来了。
在这里插入图片描述

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

力扣链接
题目描述:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

题目分析:
在I的基础上数组变为有重复元素,也就是左右边界有可能相等,不能再直接划分区间了。那最简单的策略就是让一侧缩进一个元素。
代码参考:

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

2.查找突变位置

★ 寻找峰值

力扣链接
题目描述:
峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 n u m s [ − 1 ] = n u m s [ n ] = − ∞ nums[-1] = nums[n] = -∞ nums[1]=nums[n]=
题目分析:
因为数组两侧边界值认定为-∞,所以只要数组存在一个元素,该元素就是峰值。对于其他峰值,只要从左到右查找到第一个大小关系跳变的位置即可。因此对于相邻的两个元素 x 1 , x 2 x_1,x_2 x1,x2,当 x 1 < = x 2 x_1<=x2 x1<=x2,一定存在峰值在 x 2 x_2 x2右方区间 [ x 2 , x n ] [x_2,x_n] [x2,xn],当 x 1 > x 2 x_1>x2 x1>x2,一定存在峰值在 x 1 x_1 x1左方区间 [ x 0 , x 1 ] [x_0,x_1] [x0,x1]。这样每次判断后即可将区间收缩至一半,可以使用二分法。
因此在每次循环中需要判断两个元素的关系,这就要用到二分法第二种范式。
参考代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
    int l = 0, r = nums.size() - 1, m;
        while (l < r) {
            m = l + (r - l) / 2;
            if (nums[m] <= nums[m + 1]) {
                l = m + 1;
            } else if(nums[m) > nums[m + 1]{
                r = m;
            }
        }
        return l;
    }
};
★★ 寻找峰值 II

力扣链接
题目描述:
一个2D 网格中的 峰值 是指那些 严格大于相邻格子(上、下、左、右)的元素。

给你一个 从 0 开始编号 的 m x n 矩阵 mat ,其中任意两个相邻格子的值都 不相同 。找出 任意一个 峰值 mat[i][j] 并 返回其位置 [i,j] 。

你可以假设整个矩阵周边环绕着一圈值为 -1 的格子。
在这里插入图片描述
题目分析:
首先暴力搜索法肯定是可行的,因为题目要求只要找到任一个峰值位置就可,峰值的条件是大于上下左右元素,而每一行的最大值就满足了大于左右元素的条件,只需要判断该元素是否大于上下元素就能判断出是否为峰值。
对该行最大值(下面称其为中间元素)分情况讨论:
①中间元素大于上下两行紧邻元素:为峰值
②中间元素小于上方紧邻元素,则上一行最大值元素一定大于中间元素所在行,也就是上一行的最大值元素已经满足了大于左右下的条件,基于最外层元素认定为-1的条件,我们可以知道一定存在一个峰值元素,其位置在上半区
③中间元素小于下方紧邻元素,与上种情况类似,我们知道一定存在一个峰值元素,其位置在下半区
这样对于任意一行,我们找到其最大值,经过判断就能知道峰值是在上半区还是下半区,或者就是该最大值,这样就可以使用二分法优化查找时间。
在这里插入图片描述

方法一:暴力遍历搜索

class Solution {
public:
    vector<int> findPeakGrid(vector<vector<int>>& mat) {
        int h = mat.size();
        if(h == 0)return {};
        int w = mat[0].size();
        for(int i = 0; i < h; ++i){
            for(int j = 0; j < w; ++j)
            {
                bool find = true;
                vector<pair<int,int>> bounds = {
                    {i-1,j},
                    {i+1,j},
                    {i,j-1},
                    {i,j+1}
                };
                for(auto bound : bounds){
                    if(bound.first < 0 
                    || bound.second < 0 
                    || bound.first == h 
                    || bound.second == w)
                    continue;
                    find &= (mat[i][j] > mat[bound.first][bound.second]);
                }
                if(find)return {i,j};
            }
        }
        return {};
    }
};

方法二:二分法

class Solution {
public:
    vector<int> findPeakGrid(vector<vector<int>>& mat) {
        int up = 0, down = mat.size()-1;
        while(up <= down){
            int mid = up + (down - up) / 2;
            int max_index = max_element(mat[mid].begin(),mat[mid].end())-mat[mid].begin();
            //如果比上一行小,去上半区找
            if(mid > 0 && mat[mid][max_index] < mat[mid-1][max_index]){
                down = mid - 1;
                continue;
            }
            //如果比上一行大,比下一行小,去下半区找
            if(mid < mat.size()-1 && mat[mid][max_index] < mat[mid+1][max_index]){
                up = mid + 1;
                continue;
            }
            //比上下两行都大,即为峰值
            return {mid,max_index};
        }
        return{};
    }
};

深入浅出 C++ Lambda表达式:语法、特点和应用
C++ vector 自定义排序规则(vector<vector<int>>、vector<pair<int,int>>)
关于lower_bound( )和upper_bound( )的常见用法
【C++】 详解 lower_bound 和 upper_bound 函数(看不懂来捶我!!!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值