使用二分的前提是:
1必须要保证每次判断之后,能确定答案在哪一侧
2必须可以直接通过下标知道对应是什么元素,即必须是顺序查找
二分查找,一直二分
直到左指针等于右指针时结束二分
返回值返回左指针或者右指针都可以,因为现在我推论下左指针最后都等于右指针(哪怕不等于也可以,但是应该是等于的)
返回的这个指针的含义要在你的check函数里找
怎么记忆呢,
两个指针
一个while,while里面三条:
确定mid,
然后一个if else的check,是或者不是导致区间往左还是右更新
最后return 左或者右指针
//以下两个为整数二分
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
//如果有多个结果符合条件,这种写法找到的是最左边的那个符合条件的下标
//如果没有符合条件的(不存在有三种情况,一种大小处于区间范围内,另一种属于突破区
间范围了,要么>R,要么<L),
最后的下标就是它该插入的那个位置
/*演示一下,最后一步L和R指针是挨在一起的,这个是肯定的,众所周知的
L和R挨着的,mid还是L(因为两者是挨着的下标取平均值忽略小数点
会偏左,所以mid变成L).观察mid值与target值。其实就是L值和target值
接下来分情况
如果target 在L和R之间但没有target ,那最后 L和R会移动到R这个位置,也正好是插入后的位置
后面这两种情况入狱特殊情况,主要是针对leetcode35,一半target永远都属于在之间的
target大于R,会移动到R,但是还需要+1是那个位置
target小于L,会移动到L,但不需要额外处理
总结就是 target如果大于最后的nums[L],需要+1,其余情况只需要直接返回。
那如果用下面那种写法,最后得到的是R值与target进行比较
类比总结就是
也是一样的
*/
----见leetcode35
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;
}
//如果有多个结果符合条件,这种写法找到的是最右边的那个符合条件的下标
//如果目标元素存在多个,那么,第一种写法最后坐标靠左,第二种靠右
//实数二分
//一般是浮点数
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
//这里似乎返回的不是下标,而是结果值了
/*这里使用 1e10-8,是因为题目要求保留小数点后6位,如果使用1e10-7, 那么这个因为四舍五入第八位到第七位,已经产生误差,输出六位时候误差会积累。如果使用-8,那么四舍五入第九位到第八位,取前六位时候,第七位会被truncate掉,但是第七位是准确值。一般都要多两位,就能保证准确了。这是个常识,记住就行了。*/
旋转数组的最小数字这道题用到的二分,但是他不是个单调数组
我理解为这里的二分用来左右横跳反复切割直到最后符合跟mid值进行check的那个点的时候l和r汇集在一起
即使刚好符合它的第一个点
使用二分的前提是:必须要保证每次跟mid的值进行判断之后,能确定答案在哪一侧。如果不把重复部分去掉,则当mid在后一段且nums[mid]和nums[0]相等时,则无法判断。
似乎check函数里面的大小关系必须是大于等于或者小于等于,这个等于意味着当区间内发现了符合条件的下标,即可将l和r汇聚在哪个下标那里,否则l和r怎么可能会聚在一块呢
关于旋转数组
旋转点左右判断的凭据nums[0]
由于这个nums[0]可以判断旋转点的方位,也就是最小值,所以他是可以直接套用二分模板的。任意值则需要更多一层的处理,不过也不复杂,记忆一下就好。
旋转数组查找的逻辑,先判断中点在旋转点的哪边,在哪边就会主动去确定target是不是也在旋转点这边,且target比中点值更远离旋转点,然后把区间缩过来,不行了就移过去就是了。
l + r >> 1,这个也很重要,如果l + r >> 1得出来的是实数,他会直接省略小数部分,只保留整数部分。
这个导致的找不到target的话,最终会停靠在左右中的右边那一个。
因为这个让mid省略小数,意味着最后mid永远在target的左边。所以最后指针会往右边移动区间,重合在一起。