二分查找思路简单,但细节很搞人。个人习惯用左闭右开的区间写法,以下是模板:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 我们定义target在左闭右开的区间里,[left, right)
while (left < right) {
// 因为left == right的时候,在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,因为是左闭右开的区间,nums[middle]一定不是我们的目标值,所以right = middle,在[left, middle)中继续寻找目标值
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else {
// nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
return right;// l r重叠,这里写left也可以
}
};
大意:
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
样例:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
思路:相当于求x方 - a = 0的解。所以就是在[0,a]之间二分法找一个数可以x * x = a;
class Solution {
public:
int mySqrt(int a) {
if(a == 0 || a == 1) return a;
int n = a - 1;
int left = 1, right = a,sqrt;
while (left < right) //左闭右开
{
int mid = left + (right - left) / 2;
sqrt = a / mid;
if (sqrt == mid)
return mid;
else if (sqrt < mid) //目标值在左区间中,此时mid一定不是目标值
right = mid; //右边是开区间
else
left = mid + 1; //左闭是闭区间,而Mid已经不是目标值了,所以要加一
}
return right-1; //退出时,左右重合
}
};
大意:
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
样例:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty())
return vector<int>{
-1, -1};
int lower = lower_bound(nums, target);
int high = high_bound(nums, target);
if (lower == nums.size() || nums[lower] != target) //没有赋值的情况
return vector<int>{
-1, -1};
return vector<int>{
lower, high-1};
}
int lower_bound(vector<int>& nums,int target)
{
int l = 0, r = nums.size();
while (l < r)
{
int mid = l + (r - l) / 2;
if (nums[mid] < target)
l = mid + 1;
else if (nums[mid] >= target) //等于也要继续往左边找
r = mid;
}
return r; //左闭右开写法,最后 跳出来l == r所以这里写l,r都可以
}
int high_bound(vector<int>& nums, int target) //寻找第一个大于target的数
{
int l = 0,r = nums.size();
while (l < r)
{
int mid = l + (r - l) / 2;
if (nums[mid] > target)
r = mid;
else
l = mid + 1; //如果相等也要往右边找,因为要找到第一个大于Target的数
}
return r;
}
};
后面的区间问题,常用找下界函数来做,以下是本题引申出的模板:
不知道要不要写等号的时候,想极端情况如{1,1,1,1,1},你找下界(第一个大于等于的数)肯定等于的情况往左找啊。
int low_bound(vector<int>& vec,int target) //注意这里是寻找第一个大于或等于target的数字,即上题中的Lowbound函数
{
int l = 0, r = vec.size()-1; //这里减一是有助于下面return那里的判断,免得越界。不写减一,下面判断也要改成r == vec.size()
while (l < r)
{
int mid = l + (r - l) / 2;
if (vec[mid] >= target)
r = mid; //仅剩一个元素的情况下,右指针往左移
else
l = mid + 1;
}
return (vec[r] >= target)? r:-1; //如果小于说明target太大,没找到
}
题意:给你一个整数数组 nums ,和一个整数 target 。
该整数数组原本是按升序排列,但输入时在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
和下面那题的差别就是这题不允许重复。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == target)
return mid;
if (nums[mid] <= nums[right-1]) //右区间有序 注意这里一定要有等于号
{
if (target > nums[mid] && target <= nums[right-1])
left = mid + 1;
else
right = mid;
}
else //左区间有序
{
if (target >= nums[left] && target < nums[mid])
right = mid;
else
left = mid + 1;
}
}
return -1;
}
};
大意:假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
即给的数组是由某个递增数组平移变形而来的。
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
思路:二分时,先找确定的区间。如确定右边是有序的,而且target又可以落入区间内,就先去右边找target。在这里要注意,target必须在有序的范围内 (这里上届下届都要写,因为如果不写,可能会漏查)。如果不在,就要到另一半区间去尝试二分寻找。
如给数组[3,1],和target = 3;
一开始判断1是有序的递增数列,如果不写上界,就会直接结束程序,然而3是在mid的左边。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == target)
return true;
if (nums[mid] == nums[left]) //没法知道有序无序
++left;
else if (nums[mid] > nums[left]) //中间大于最左边,说明左边有序
{
if (nums[mid] > target && target >= nums[left]) //target落入左边且大小合法
right = mid;
else
left = mid + 1;
}
else
{
int end = (right == nums.size() ? nums.size() - 1 : right);
if (nums[mid] < target && target <= nums[end])//target落入右边且合法
left = mid + 1; // 如输入[3, 1], target = 3
else
right = mid; //落入右边但大小不合法,说不定在左边可以找到,所以要去尝试一下左边
}
}
return false;
}
};
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size()-1;//任是左闭右开,这里只不过方便下面的比较
//这样可以让mid正好是最小值的时候,也被纳入寻找区间
while (l < r)
{
int mid = l + (r - l) / 2;
//最小值必不可能在有序部分
if (nums[mid] < nums[r]) //右边有序
{
r = mid;
}
else
{
l = mid+1;
}
}
return nums[l];
}
};
题目:
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
样例:
输入: [3,1,5]
输出: 1
输入: [2,2,2,2]
输出: 2
本题核心思想:最小值一定不在判断为有序的那一部分内。(因为数组是旋转平移得来的,所以最大和最小的交接是无序的,所以最小的那个数就在这个无序区间内)。
一开始没想明白,自己写的版本:
可以发现主要的坑点就是一直重复的那个数就是最小值,在没法判断有序区间的时候,不知道如何处理了。
class Solution {
public:
int findMin(vector<int>& nums) {
int n = nums.size();
int left = 0, right = n;
int MIN = 99999;
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == nums[left]) //没法判断哪边有序
{
if (left == right - 1 && nums[left] < MIN) //如果是最小区间了
MIN = nums[left]; //单独判断一下
++left;
}
else if (nums[mid] > nums[left]) //左边有序
{
if (nums[left] < MIN) //左边最小的数是Left
MIN = nums[left];
left = mid + 1;
}
else //右边有序
{
if (nums[mid] < MIN) //右边最小的数是mid
MIN = nums[mid];
right = mid;
}
}
return MIN;
}
};
理清思路后,发现了本题核心思想。这样在没法判断有序区间的时候把right减一,缩小这个区间。
这里出现了第二个坑点,最好是用right-1(左闭右开写法)指向的数来判断有序区间,因为如果用left来做,在无法判断的情况下要写left++,这样很容易数组越界。
为什么总喜欢和右边比?
因为(l+r)/2是向下取整,也就是说,假如 l=0, r=1, 那么mid = 0, 这样就不能使用mid-1进行比较。反正之后都和右边比就完事了。
(在下一题中十分明显,所以养成习惯都和右边比不容易出错,右边初始为 vec.size() -1)
此外左闭右开也可以写成 right = nums.size() -1的。这样可以方便当前区间最右边的那个数的判断。
class Solution {
public:
int findMin(vector<int>& nums) {
int n = nums.size();
int left = 0, right = n-1; //这里仍然是左闭右开,只不过为了判断区间方便
//决定是否是开闭是根据while里面是否有等于号,以及r,l的移动方式来的
/*最小值必定不在有序的那部分,所以每次都要去无序的部分寻找*/
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) //左边有序
left = mid + 1;
else if (nums[mid] < nums[right]) //右边有序
right = mid;
else //无法判断左还是右有序
right--;
}
return nums[left];
}
};
样例:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
本题核心思想:
如何判断要去左边还是右边呢?就是通过左边区间size的奇偶来判断。这样相当于在数组之中一次删除两个相同的元素,并且分区间判断。
同时,左闭右开只是left < right描述区间为1的时候是固定的,否则在讨论low和high的移动的时候,总是死卡着这点容易数组越界。
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int low = 0, high = nums.size() - 1;
while (low < high)
{
int mid = low + (high - low) / 2;
bool BehindMidEven = (high - mid) % 2 == 0; //mid后的部分是否是偶数
if (nums[mid + 1] == nums[mid]) //mid后面那个数和Mid这个相同
{
if (BehindMidEven) //后面那部分不相同的部分是奇数了
low = mid + 2;
else
high = mid - 1;
}
else if (nums[mid - 1] == nums[mid])//mid前面的数和Mid相同
{
if (BehindMidEven)
high = mid - 2;
else
low = mid + 1;
}
else
return nums[mid];
}
return nums[low];
}
};
样例:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
思路:
用当前Mid的元素和mid+1的元素相比,如果小于则mid当前属于一个递减区间。这样peak必然在mid的左边区间(这个新的查找区间包含了Mid本身)。去左边区间进行新一轮二分查找。
此外,因为(l+r)/2是向下取整,也就是说,假如 l=0, r=1, 那么mid = 0, 这样就不能使用mid-1进行比较。反正之后都和右边比就完事了。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r)
{
int mid = l + (r - l) / 2;
if (nums[mid] > nums[mid + 1]) //若成立说明Mid在一个递减区间内,peak在其左边
r = mid;
else
l = mid + 1;
}
return l;
}
};