文章目录
二分搜索小记
二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,博主这里对二分搜索进行了归类:
第一类: 需查找和目标值完全相等的数
这是最简单的一类,也是我们最开始学二分查找法需要解决的问题,比如我们有数组[2, 4, 5, 6, 9],target = 6,那么我们可以写出二分查找法的代码如下:
int find(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;
else if (nums[mid] < target) left = mid + 1;
else right = mid;
}
return -1;
}
会返回3,也就是target的在数组中的位置。注意二分查找法的写法并不唯一,主要可以变动地方有四处:
第一处是right的初始化,可以写成 nums.size() 或者 nums.size() - 1
第二处是left和right的关系,可以写成 left < right 或者 left <= right
第三处是更新right的赋值,可以写成 right = mid 或者 right = mid - 1
第四处是最后返回值,可以返回left,right,或right - 1
但是这些不同的写法并不能随机的组合,像博主的那种写法,若right初始化为了nums.size(),那么就必须用left < right,而最后的right的赋值必须用 right = mid。但是如果我们right初始化为 nums.size() - 1,那么就必须用 left <= right,并且right的赋值要写成 right = mid - 1,不然就会出错。所以博主的建议是选择一套自己喜欢的写法,并且记住,实在不行就带简单的例子来一步一步执行,确定正确的写法也行。
第二类: 查找第一个不小于目标值的数,可变形为查找最后一个小于目标值的数
这是比较常见的一类,因为我们要查找的目标值不一定会在数组中出现,也有可能是跟目标值相等的数在数组中并不唯一,而是有多个,那么这种情况下nums[mid] == target这条判断语句就没有必要存在。比如在数组[2, 4, 5, 6, 9]中查找数字3,就会返回数字4的位置;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回第一个数字1的位置。我们可以使用如下代码:
int find(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
return right;
}
最后我们需要返回的位置就是right指针指向的地方。在C++的STL中有专门的查找第一个不小于目标值的数的函数lower_bound,在博主的解法中也会时不时的用到这个函数。但是如果面试的时候人家不让使用内置函数,那么我们只能老老实实写上面这段二分查找的函数。
这一类可以轻松的变形为查找最后一个小于目标值的数,怎么变呢。我们已经找到了第一个不小于目标值的数,那么再往前退一位,返回right - 1,就是最后一个小于目标值的数。
值得一提的是,在数组中没有符合要求的项时,该代码返回num.size()。
第三类: 查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数
这一类也比较常见,尤其是查找第一个大于目标值的数,在C++的STL也有专门的函数upper_bound,这里跟上面的那种情况的写法上很相似,只需要添加一个等号,将之前的 nums[mid] < target 变成 nums[mid] <= target,就这一个小小的变化,其实直接就改变了搜索的方向,使得在数组中有很多跟目标值相同的数字存在的情况下,返回最后一个相同的数字的下一个位置。比如在数组[2, 4, 5, 6, 9]中查找数字3,还是返回数字4的位置,这跟上面那查找方式返回的结果相同,因为数字4在此数组中既是第一个不小于目标值3的数,也是第一个大于目标值3的数,所以make sense;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回坐标5,**通过对比返回的坐标和数组的长度,我们就知道是否存在这样一个大于目标值的数。**参见下面的代码:
int find(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) left = mid + 1;
else right = mid;
}
return right;
}
这一类可以轻松的变形为查找最后一个不大于目标值的数,怎么变呢。我们已经找到了第一个大于目标值的数,那么再往前退一位,返回right - 1,就是最后一个不大于目标值的数。比如在数组[0, 1, 1, 1, 1]中查找数字1,就会返回最后一个数字1的位置4,这在有些情况下是需要这么做的。
题目练习
求开方
该题目可以转变为求数组的中最后一个小于等于(不大于)目标值的数,即第三类的变种。
class Solution {
public:
int mySqrt(int x) {
if (x <= 1) return x;
int left = 0, right = x;
while (left < right) {
int mid = left + (right - left) / 2;
if (x / mid >= mid) left = mid + 1;
else right = mid;
}
return right - 1;
}
};
大于给定元素的最小元素
744. Find Smallest Letter Greater Than Target (Easy)
Input:
letters = ["c", "f", "j"]
target = "d"
Output: "f"
Input:
letters = ["c", "f", "j"]
target = "k"
Output: "c"
题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。
第三类问题。
class Solution {
public:
char nextGreatestLetter(vector<char>& letters, char target) {
if(target>=letters.back())
return letters.front();
int left=0,right=letters.size();
while(left<right)
{
int mid=left+(right-left)/2;
if(letters[mid]<=target)
left=mid+1;
else
right=mid;
}
return letters[right];
}
};
有序数组的 Single Element
540. Single Element in a Sorted Array (Medium)
Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
Output: 2
题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。
观察题目可知偶数情况下:在index>=target时,index和index+1的数不相等。在index<target时,index和index+1的数相等。
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int left=0,right=nums.size()-1;
int n=nums.size();
while(left<right) //left<right确保了mid+1不会越界,因为mid一定小于right
{
int mid=left+(right-left)/2;
if(mid%2) --mid; //++mid的话下一行的mid+1可能出界。
if(nums[mid]==nums[mid+1])
left=mid+2;
else
right=mid;
}
return nums[right];
}
};
第一个错误的版本
题目描述:给定一个元素 n 代表有 [1, 2, …, n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
// Forward declaration of isBadVersion API.
bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
int left=1,right=n;
while(left<right)
{
int mid=left+(right-left)/2;
if(isBadVersion(mid))
right=mid;
else
left=mid+1;
}
return right;
}
};
旋转数组的最小数字
153. Find Minimum in Rotated Sorted Array (Medium)
Input: [3,4,5,1,2],
Output: 1
class Solution {
public:
int findMin(vector<int>& nums) {
int left=0,right=nums.size()-1;
if(nums[left]<=nums[right])
return nums[left];
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<nums[right])
right=mid;
else
left=mid+1;
}
return nums[right];
}
};
旋转数组的最小数字II
154. Find Minimum in Rotated Sorted Array II
和前一题相比,数字可重复。在碰到mid和要比较的数字相等时,去掉一个重复数字,对结果无影响,最坏情况时间复杂度为O(n),比如数组所有元素相同。
class Solution {
public:
int findMin(vector<int>& nums) {
int left=0,right=nums.size()-1;
if(nums[0]<nums.back())
return nums[0];
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<nums[right])
right=mid;
else if(nums[mid]>nums[right])
left=mid+1;
else
--right;
}
return nums[right];
}
};
查找区间
34. Search for a Range (Medium)
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]
先用二分查找找第一个数,再找最后一个数。第一个二分查找找数组中第一个大于等于目标的数,第二个二分查找找数组中最后一个不大于目标的数(也即第一个大于目标的数减一)。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> res(2, -1);
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
if (right==nums.size()||nums[right] != target) return res;//条件一判断是否这个数组里所有的数字都比目标大,第二个条件判断是否数组中第一个大于等于目标的数不等于目标。
res[0] = right;
left=0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) left = mid + 1;
else right= mid;
}//由于上面的if已经保证了一定有目标值在给定数组中,这里不用判断了,值得一提的事。
res[1] = right - 1;
return res;
}
};