1.算法思路
假设目标值在闭区间 [l, r] 中, 每次将区间长度缩小一半,当 l = r 时,我们就找到了目标值。
2.模板
二分模板一共有两个,分别适用于不同情况。
1.
当我们将区间 [l, r] 划分成 [l, mid] 和 [mid + 1, r] 时,其更新操作是 r = mid 或者 l = mid + 1;,计算 mid 时不需要加 1 。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;//除2
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
2.
当我们将区间 [l, r] 划分成 [l, mid - 1] 和 [mid, r] 时,其更新操作是 r = mid - 1 或者 l = mid;,此时为了防止死循环,计算 mid 时需要加 1。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
3.样例
1.不修改数组找出重复的数字
给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
请找出数组中任意一个重复的数,但不能修改输入的数组。
样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回 2 或 3。
思考题:如果只能使用 O(1) 的额外空间,该怎么做呢?
题解
因为一共有 n+1 个数,每个数的取值范围是1到n,所以至少会有一个数出现两次,将每个数的取值的区间[1, n]划分成[1, n/2]和[n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int l = 1,r = nums.size()-1;
while(r > l)
{
int s = 0;
int mid = l+r >> 1;// 划分的区间:[l, mid], [mid + 1, r]
for(int i = 0;i < nums.size();i ++)
if(nums[i] >= l&&nums[i] <= mid)s++;//左边数的个数
if(s > mid-l + 1)r = mid;//缩小空间
else l = mid + 1;
}
return r;
}
};
2.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个升序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
数组可能包含重复项。
注意:数组内所含元素非负,若数组大小为0,请返回-1。
样例
输入:nums=[2,2,2,0,1]
输出:0
题解
为了便于分析,我们先将数组中的数画在二维坐标系中,横坐标表示数组下标,纵坐标表示数值,如下所示:
我们发现除了最后水平的一段(黑色水平那段)之外,其余部分满足二分性质:竖直虚线左边的数满足 nums[i]≥nums[0]nums[i]≥nums[0];而竖直虚线右边的数不满足这个条件。
分界点就是整个数组的最小值。
所以我们先将最后水平的一段删除即可。
另外,不要忘记处理数组完全单调的特殊情况:
当我们删除最后水平的一段之后,如果剩下的最后一个数大于等于第一个数,则说明数组完全单调。
class Solution {
public:
int findMin(vector<int>& nums) {
int n = nums.size()-1;
if(n < 0)return -1;//无元素返回-1
if(n > 0&&nums[0] == nums[n])n --;//删除最后重复部分
if(nums[0] < nums[n])return nums[0];//一直递增的情况
int l = 0,r = n;//二分
while(l < r)
{
int mid = l + r >> 1;
if(nums[0] > nums[mid])r = mid;
else l = mid + 1;
}
return nums[r];
}
};