二分:分成两份,简单来说真的就是分成两份,分成两个区间,然后结果在那个区间就保留那个区间继续二分。
二分前提:存在单调性,注意,是单调性。只要具备单调性就一定可以进行二分搜索了。
注意:写二分搜索,本质上是不断的缩小搜索范围,并且必须确定答案处在搜索范围之中。排除不可能是答案的区间。
为何二分需要严格的单调性。帮助排查,只有答案具备一定的单调性。我们才能按照单调性进行二分形式的搜索。
先来一份二分搜索的代码,这是最常见的二分搜索写法.
int binarySearch(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l <= r) {
mid = l + (r - l)/2;
if (nums[mid] == target) return mid;//找到ans
if (nums[mid] < target) l = mid + 1;//去右边找
else r = mid - 1;//去左边找
}
return -1;//找不到.
}
二分搜索除了可以搜索具体的值,还可以搜索左右边界。或者说满足条件的第一个或者最后一个值。比如说 >= target的第一个值,或者 <= target的最后一个值
搜索满足条件的第一个1: 比如说第一个>= target的值
先写出这个的代码:原理,因为是第一个>= 的值. 所以如果是处在0的范围,则是处在右区间,并且肯定不可能是结果,所以我们跳过它。l = mid + 1。跳过左区间,搜索右区间。 如果是处在1的范围,则可能是最后答案,但是这个值已经是1了。我们需要的是右边的第一个1, 所以它右边的1肯定不可能是我们要的。所以此时 r = mid; 保留左区间。除去不可能存在结果的右区间。
由于,我们采取的是l < r的循环退出条件,所以当循环退出的时候,如果存在这个>=的第一个1则刚刚好就是此时的nums[l],也即为nums[r]. 若不满足条件,说明没找到.
注意:真的好好品一品,二分的精华就是保留下来的区间中一定包含正确结果,我们除去,排除掉的区间中一定不包含正确结果。
//寻找左边界第一个1, 第一个 >= target
int binarySearch01(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = l + (r - l)/2;
if (nums[mid] < target) l = mid + 1;
else r = mid;
}
return nums[l] >= target ? l : -1;
}
搜索最后一个满足条件的1:比如说最后一个<= target的值
上面的东西完全搞懂了,代码吃透了。此时你肯定是跃跃欲试,我晓得。这个我会写了。这不逻辑跟上边完全一样嘛,只是的代码对称过来写就OK了塞。这个我会。
于是下面这份代码被你写出来了。
int binarySearch10(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = l + (r - l)/2;
if (nums[mid] > target) r = mid - 1;
else l = mid;
}
return nums[l] <= target ? l : -1;
}
可惜了可惜了。这份代码逻辑没毛病,可惜会陷入死循环进而永远l 跟 r 无法重合得到最终答案。
why???? 出现问题不要方。我们自己思考。总是有原因的对吧。总不能无缘无故的就死循环,而且为毛上边不死循环,偏偏下面就会? 其实核心在查找的答案所在的区间不一样。我们这里找的是最后一个1. 这个1处在右区间。当l真正走到这个结果的时候。l便不能再动了,此时只能依靠r走到l 位置跟l 重合。但是但是但是。 若 r = l + 1的时候。(l+r)/2 = l 始终等于l。此时此时。我们渴望的是r向左再走一步呀。答案就在l处了呀。 但是但是。r 做不来喔。因为根本mid就始终等于l ,r说轮不到我判断呀。我爱莫能助呀。所以。就陷入了死循环,一直让l 走,可惜 l 说我走不得喔。我下面可是正确答案。
给大家留点小小思考吧? 为什么? 上面的第一份代码,寻找右边第一个1就不会陷入死循环?如果上面的解释看懂了。其实不难(我们就假设,l和r已经相邻了。马上就要重合得到最终结果答案了。那究竟能否走出这最后一部呢?)其实好多时候二分的死循环就在于此。
下面这份代码,可以破除上面的最后一部不能走的困境,找到正确结果。
int binarySearch10(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = l + (r - l)/2;
if (nums[mid] > target) r = mid - 1;
else {
if (mid + 1 == nums.size() || nums[mid + 1] > target) return mid;//ans
else l = mid + 1;
//不可能是答案, 为了不陷入死循环(此乃,破除死循环关键)
//有没有感觉这好像有点像写朴素的二分. 此处完全可以是 l <= r
}
}
return nums[l] <= target ? l : -1;
}
到了此处,让我们彻底解决二分边界问题吧。可恨的二分边界。经过我们上面的分析,出现二分边界问题陷入死循环的原因,无非就是写 (l < r)这种版本二分的时候最后一部 l 和 r相邻,但是谁都无法踏出这最后一部重合退出循环返回正确答案嘛。
那简单嘛。我可以不可以不走了。诶。对我就不走这最后几步了。要晓得,二分搜索的优势只有在大范围查找才能体现出来,要是只剩下几个元素,其实二分搜索和线性查找效率差不多。那OK, 我可以二分搜索结合线性查找呀。就像STL库中的排序那样。
上述代码改进代码如下:末尾有_的就是优化的可以避免死循环的二分骚操作。
//二分查找. 最简单版本的二分查找
/*
mid = l + (r - l) / 2;
mid = (l + r) >> 1;//maybe 越界.
mid = l + ((r - l)>>1);//少不了括号, 位移运算符优先级低于算数运算符
*/
int binarySearch(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l <= r) {
mid = l + (r - l)/2;
if (nums[mid] == target) return mid;
if (nums[mid] < target) l = mid + 1;//去右边找
else r = mid - 1;//去左边找
}
return -1;//找不到.
}
//避免死循环的 bs
int binarySearch_(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (r - l > 3) {
mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) l = mid + 1;//去右边找
else r = mid - 1;//去左边找
}
for (; l <= r; l ++) {
if (nums[l] == target) return l;
}
return -1;//找不到.
}
//寻找左边界第一个1, 第一个 >= target
int binarySearch01(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = l + (r - l)/2;
if (nums[mid] < target) l = mid + 1;
else r = mid;
}
return nums[l] >= target ? l : -1;
}
int binarySearch01_(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (r - l > 3) {
mid = l + (r - l)/2;
if (nums[mid] < target) l = mid + 1;
else r = mid;
}
for (; l <= r; l ++ ) {
if (nums[l] >= target) return l;
}
return -1;
}
//寻找右边界最后一个1 最后一个 <= target, 右边界问题.
//这份代码可以吗? 如果按照上述思路, 写下来的绝对是下面这份代码
//But 不对
int binarySearch10(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = l + (r - l)/2;
if (nums[mid] > target) r = mid - 1;
else {
if (mid + 1 == nums.size() || nums[mid + 1] > target) return mid;//ans
else l = mid + 1;//不可能是答案, 为了不陷入死循环
}
}
return nums[l] <= target ? l : -1;
}
int binarySearch10_(std::vector& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (r - l > 3) {
mid = l + (r - l)/2;
if (nums[mid] > target) r = mid - 1;
else l = mid;
}
for (; l <= r; r-- ) {
if (nums[r] <= target) return r;
}
return -1;
}
本质上还是二分。也是利用的答案的单调性变化。此处,我们就可以提到数组和函数的关联了。有没有感觉不论是数组还是函数。如果函数的规则是具有单调性的。函数跟有序数组的很像很像呀。其实函数和有序数组本质都是一样的。都是对自变量的一种映射规则。函数相当于是对于传入参数作为自变量映射出来新的数。数组相当于把下标作为自变量映射新的数字。
如果这个映射规则映射出来的值具备单调性。那么我们就完全可以进行二分搜索这个函数的解。
只能留着明天写下回了。这个就当作上吧。学校搞实训有点忙。