先说点题外话:
不知不过在公司干了半年,转正之后连拿了两个S评级,这半年工作熟悉业务,顺便还因为机缘巧合从三月开始基本掌握了React+Flask+数据库的开发。所以很久没更新了,现在工作基本稳定下来,准备继续把408复习复习,重新夯实基础。
进入正题,咱们跟着LEETCODE重新从算法基础看起。
首先是算法入门—二分查找
二分查找的原理比较简单,就是每次折半,直到定位到我们要的答案。因为它如此简单,所以时间复杂度是O(logn)。
但就像《一拳超人》,埼玉老师强大的代价是秃头,二分查找强大的代价就是让它在实际工作中几乎用不上的限制条件:目标必须是有序的。
因此二分查找的规律也是非常的明显
先不放问题,来看看我这几题的解答(C++直球式写法):
//第一题
class Solution {
public:
int search(vector<int>& nums, int target) {
int start = 0, end = nums.size() - 1;
int mid = end / 2;
if(nums[mid] == target)
return mid;
while(start != end)
{
if(target > nums[mid])
start = mid + 1;
else if(target < nums[mid])
end = mid;
mid = (end - start) / 2 + start;
if(nums[mid] == target)
return mid;
}
return -1;
}
};
//第二题
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
if(n == 1)
return 1;
int start = 0, end = n-1;
int mid = end / 2;
while(start != end)
{
if(isBadVersion(mid + 1) == true)
end = mid;
else if(isBadVersion(mid + 1) == false)
start = mid + 1;
mid = (end - start)/2 + start;
}
return start + 1;
}
};
//第三题
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int start = 0, end = nums.size() - 1;
int mid = end / 2;
while(start != end)
{
if(target > nums[mid])
start = mid + 1;
else if(target < nums[mid])
end = mid;
mid = (end - start) /2 + start;
if(nums[mid] == target)
return mid;
}
if(target > nums[start])
return start + 1;
else
return start;
}
};
可以注意到,二分查找问题的思路有一个固定的代码模板:
//此处为伪代码,假设这是在找一个有序数组array中的数target(即数组中的数必定包含我们要的数)
int start = 0, end = n-1; //n是要读取的有序数组的长度
int mid = end - start / 2; //注意c++ integer向下取整
while(start != end)
{
if(array[mid] > target)
end = mid;//如果数组中值大于target,则说明目标在mid前面,折半操作通过end移动到mid
else if (array[mid] < target)
start = mid + 1;//如果数组中值小于target,则说明目标在mid后面,折半操作通过start移动到mid+1
mid = (end - start) / 2 + start;//折半后不要忘记更新中点坐标
}
return start; //当起点终点在循环中重合,说明最后剩下的数字就是我们的答案。
一切的二分法都可以从上面的基础代码修改完成,首先说一下大家很容易迷糊(包括我)的index,即数组下标。
首先这个代码比较迷糊的地方是:为什么end = mid而start = mid + 1。
首先我们要明白,把算法转换为实际是需要一个起点的,而这里的起点就是
start = 0, end = n - 1, mid = (end - start) / 2 + start
在这个基础上,我们只需要考虑一种折半的情况是否能走通while(start != end),而不会导致无限循环即可:
当array = [0, 1]的时候,因为理想中所有二分法的最后一步都是这个(不考虑中途找到结果)
以这个数组为例,array = [0, 1]; start = 0; end = 1; mid = 0
因此array[mid] = array[0]。
如果target < array[mid]。
根据上面的基础代码,我需要把end往前折半,即end = mid = 0 = start,这个时候while(start != end)就能走出循环,那么我们上面的基础代码
if(array[mid] > target) end = mid;
这个判断在这个是没有问题的
如果target > array[mid]。
根据上面的基础代码,我需要把start往后折半,即start = mid + 1 = 1 = end, 这个时候这个时候while(start != end)就能走出
循环,那么我们上面的基础代码
else if (array[mid] < target) start = mid + 1;
这个判断在这个是没有问题的
因此为什么循环中end = mid而start = mid + 1,这是为了锚定唯一值,脱出二分循环。
接下来我们回顾一下LEETCODE的问题
LEETCODE中的算法题算是基础算法的思路扩张:
1.如果数组不一定包含我们要的值怎么办:
//第一题
class Solution {
public:
int search(vector<int>& nums, int target) {
int start = 0, end = nums.size() - 1;
int mid = end / 2;
if(nums[mid] == target)
return mid;//以防数组长度为1但是唯一的值又刚好等于结果走下面的循环直接跳出输出-1
while(start != end)
{
if(target > nums[mid])
start = mid + 1;
else if(target < nums[mid])
end = mid;
mid = (end - start) / 2 + start;
if(nums[mid] == target)
return mid;//这一步包含了最后的start=end=mid时候的判断,如果定位到数组只剩一个值,但是nums[mid]依旧和target不相等,那就说明数组没有target。
}
return -1;
}
};
2.我们如何去抽象概括出数组来使用二分法
//第二题 抽象有难度,但是几乎是直接套用
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
if(n == 1)
return 1;
int start = 0, end = n-1;//将n看作0~n-1的数组套用公式即可
int mid = end / 2;
while(start != end)
{
if(isBadVersion(mid + 1) == true)
end = mid;
else if(isBadVersion(mid + 1) == false)
start = mid + 1;
mid = (end - start)/2 + start;
}
return start + 1;
}
};
3.当返回值变化时的二分查找
//第三题
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int start = 0, end = nums.size() - 1;
int mid = end / 2;
while(start != end)
{
if(target > nums[mid])
start = mid + 1;
else if(target < nums[mid])
end = mid;
mid = (end - start) /2 + start;
if(nums[mid] == target)
return mid;
}
if(target > nums[start])//有两种输出怎么办?写个if----else即可啦
return start + 1;
else
return start;
}
};
可以看到,所有题目基本都是套用公式,以后看到二分查找不用怕,记住基础公式即可啦~:
//此处为伪代码,假设这是在找一个有序数组array中的数target(即数组中的数必定包含我们要的数)
int start = 0, end = n-1; //n是要读取的有序数组的长度
int mid = end - start / 2; //注意c++ integer向下取整
while(start != end)
{
if(array[mid] > target)
end = mid;//如果数组中值大于target,则说明目标在mid前面,折半操作通过end移动到mid
else if (array[mid] < target)
start = mid + 1;//如果数组中值小于target,则说明目标在mid后面,折半操作通过start移动到mid+1
mid = (end - start) / 2 + start;//折半后不要忘记更新中点坐标
}
return start; //当起点终点在循环中重合,说明最后剩下的数字就是我们的答案。