文章目录
力扣官方总结
二分原理
场景分析: 当涉及需要在一个有限区间中筛选出满足指定条件的数据时,最直接的方法是遍历区间逐个排查,此时时间复杂度为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=mid−1;
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)=x2−c的零点,迭代公式:
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+1−xi足够小时为近似解。
二分法代码:
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=mid−1?
找最小值时,最后一次循环的两个数,最小值在左边,需要从右向左逼近,来结束循环,这样直接返回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 函数(看不懂来捶我!!!)