二分查找
大体写完了,懒得校对,因为最近都不想看到二分法这玩意了
二分法是什么
在数学中,其实我们是接触过二分法的概念的,
记得当时是在函数的单调性中,用来判断零点的近似值,
截图自百度百科
此时是在计算机中,处理的是一组组数据,本质上和函数是无差的,
比如,我们同样是要根据一段数据的中点进行判断,
而且,同样是要是一段”单调区间“,也就是要在有序数组中,
还有,数组中不能有重复元素,这一点对应“函数的一一对应”
以下一段话是维基百科的解释:
在计算机科学中,二分查找算法也称折半搜索算法,对数搜索算法,是一种在有序数组中查找某一特定元素的搜索算法。
搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;
如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
为什么要使用二分法
如果是暴力求解,时间自由度就是O(n)
,用了二分法之后,时间自由度为O(logN)
,更快了
关于复杂度的简单认识可参考CSDN文章:ntq73
关于对数型的时间自由度的解释,这个博主的解释很有意思,可以看看:Ycccc
二分法的两种写法
极力推荐:
- <【手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找】 https://www.bilibili.com/video/BV1fA4y1o715?share_source=copy_web&vd_source=6809974e05da2ac6884b9251fab43087>
- 0704.二分查找.html#_704-二分查找
- qjNff
本部分内容基本根据卡哥的视频和他的文章得来。
全闭:[left, right]
程序里要坚守统一的区间格式
先要明确一点,
我们既然确定了使用某一种区间,就要一条道走到黑,
也就是说在这个程序中都要使用它,
这个容易理解,避免出错嘛,假如有人很NB能确保自己怎样都不会把自己绕晕,也就随便了
while循环中left<=right
随便举一个例子:
对于图中那个20的计算方法,通常直接(10+30)/ 2即可,
但是这里为了避免越界,还是用的最规范的方法
所以,按照这个统一的全闭写法,这里的while括号里的条件应该是left <= right
,
即当左边界的值大于右边界的时候,while循环就破开了。
关于这里的等号,他也是有实际意义的,
可以看到,我上面数轴上,10和30都是实心点,
他们在数轴上构成区间的意义是,从10到30,包括10和30的所有点的集合
随着二分的继续,或者就直接说取一个极端情况:这个包括端点的实心点的集合,里面此时只有一个点!
假设这个点为1, 如下图:
可以看到,在数轴上,这个1似乎就只是一个点,
但是,从我们数组或者刚刚“点的集合”来看,1其实也可以写成[1, 1]的
if判断中的right = middle - 1
然后把视角看向我刚刚分出来的,两个还没填写完成的全闭区间,
之所以没有填写完整,是因为这里有坑:
我们知道,我们二分法其实是分了三种情况的,以数组中middle代表元素的值与target进行判断,这三种情况分别用if-else if-else if来写
如果直接middle就是查找的那个,恭喜直接通关,
若是在左边的区间(假设数组元素递增),那么已经满足nums[middle] > target
这里的“右区间‘ 只是为了方便理解,不要当真
还有,既然分了是三种情况,就必须要明确那个中点是单独判断的,就不要把中点的实点带到左右区间了
所以说,此时的左区间,也是可以得出来的,
但是需要注意要满足坚守区间格式:
此时,右边区间已经没用了,直接将那个right拿过来表示右边界,
于是将 right 赋值为 middle - 1
左闭右开:[letf, right)
两个区间的区别
听卡哥说不只有这两种,但另外一种咱左开右闭都不喜欢用而已,
但最好不要用,毕竟两种都已经有点绕了,,,
对于全闭和左闭右开,当他们表示同一个int数组区间的时候:
可以看到,若是表示同一个区间,
如果说全闭的右边界是right,那么左闭右开区间的边界就是right+1,
准确来说是给定数组的尾下标加一
此时其实还有一个不太确定的,就是middle和right的判定,
可以参考卡哥的例子,这个R=7是右边界,M =(1+7)/ 2 = 3
第二个二分里面,4是边界,M = (0+3)/ 2 = 1
无论哪一种区间,middle的求法都一样,不一样也行,但前提是你能保证你最后不会漏掉值
while循环中left < right
直接看那个极端情况吧:[1, 1),
左边的括号意思是可以等于1,但是右边括号说不等于1,两个怎么不打一架,
所以left不能等于right,
也就是说,while后面的条件是:left < right
if 判断中right = middle+1
题目_1
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
来源:力扣(LeetCode)
题目序号:704
链接:https://leetcode.cn/problems/binary-search
自己的代码
int search(int* nums, int numsSize, int target)
{
int Left = 0;
int Right = numsSize-1;
int Middle = Left + (Right-Left)/2;
while(Left <= Right)
{
Middle = Left + (Right-Left)/2; // 更新Middle
if(nums[Middle] > target) // 左
{
Right = Middle - 1;
}
else if(nums[Middle] == target) // 中
{
return Middle;
}
else if(nums[Middle] < target) // 右
{
Left = Middle+1;
}
}
return -1;
}
自己写的这个代码还算行,就不找其他代码了
题目_2
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1
来源:力扣(LeetCode)
题号:69
链接:https://leetcode.cn/problems/sqrtx
参考思路
本题在做的时候没啥思路,知道要用二分法但不知道咋用,
研究了下评论区一个大佬的代码,大抵知道思路了:
就是把给定的x当成由0递增1的有序数组的右极限(这里在意义上数组中不包括x这个点,有的只是开始时候被赋初值的max),
然后查找mid*mid
,
每一次判断mid*mid
与x的大小后,都通过判断结果来改变左右极限,
让mid*mid
在数组中不断逼近x,
直到最后这个数组中最后只剩下一个元素或者两个元素,
此时初始数组的右边的元素是min或min+1,
左边的元素一定是min,
x此时被min*min
和(min+1)*(min+1)
夹着,或直接等于这两中的一个
但无论是两种情况下的哪一种,此时都存在min==max
,
此时直接返回min即可
参考代码
基本上就是copy的上面那位大佬的,只是在细节上处理了一下
int mySqrt(int x)
{
int Max = x;
int Min = 0;
int Mid = Min + (Max - Min)/2;
if(1 == x) // 对下面提到的除数为0的情况单独讨论
{
return 1;
}
while(Max - Min > 1) // 此时Max与Min要么相等,要么相差1,已经是极限了
{
Mid = Min + (Max - Min)/2; // 更新Mid
if(x/Mid < Mid) // 左。这里写的更直观点应该是 Mid*Mid < x,但是还是要避免越界的。可写成除的形式还要注意除数不能为0
{
Max = Mid; // 中间值的平方大于x,更新右边界
}
else if(x/Mid == Mid) // 中
{
return Mid;
}
else if(x/Mid > Mid) // 右
{
Min = Mid;
}
}
return Min; // 写的时候发现力扣编译器的一个bug:这个位置不给return就报错。Max与Min要么相等,要么相差1,两种情况Min都是所求的那个值
}
题目_3
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
进阶:不要 使用任何内置的库函数,如 sqrt 。
示例 1:
输入:num = 16
输出:true
示例 2:
输入:num = 14
输出:false
提示:
1 <= num <= 2^31 - 1
来源:力扣(LeetCode)
题号:367
链接:https://leetcode.cn/problems/valid-perfect-square
参考代码
思路跟上一题一样的,不细说,注释都懒得写了
bool isPerfectSquare(int num)
{
int Max = num;
int Min = 0;
int Mid = Min + (Max - Min)/2;
if(1 == num)
{
return true;
}
while(Max - Min > 1)
{
Mid = Min + (Max - Min)/2;
if(num/Mid < Mid)
{
Max = Mid;
}
else if(num/Mid == Mid) // 由于用的是int,所以存在5/2 == 2的情况,需要用if来再次筛选
{
if(num%Mid == 0)
{
return Mid;
}
Min = Mid;
}
else if(num/Mid > Mid)
{
Min = Mid;
}
}
return 0;
}