当前位置: 首页 > 后端技术 > Java

发现了二分查找的秘密

时间:2023-04-02 00:01:28 Java

二分查找(BinarySearch)算法,又称二分查找算法。1.1.原理分析二分查找是一种非常简单易懂的快速查找算法。它的创意在生活中随处可见,比如我喜欢在朋友聚会时玩的猜谜游戏。我随机写一个0-100之间的数字,然后大家依次猜。在猜的过程中,每次猜的时候我都会告诉大家猜的是大还是小,直到有人猜对了,猜对的人会有一些惩罚措施。这个过程其实就是二分查找思想的体现。回到实际开发场景,假设有10个订单,金额分别为:6、12、15、19、24、26、29、35、46、67,请找出订单金额为15的订单。使用二分查找的思想,我们每次都和区间中间的数据比较大小,缩小查找范围。下图表示查找过程,其中left和right表示是要查找范围的下标,mid表示要查找范围中间元素的下标(如果范围range是偶数且有两个中间的数字,选择较小的一个)。通过这个查找过程,我们可以对二分查找思想进行总结:二分查找针对的是有序的数据集,查找思路有点类似于分治思想。每次通过与区间中间的元素比较,将要查找的区间缩小为前一个的一半,直到找到要查找的元素,或者区间缩小为01.2。复杂度分析理解了二分查找的思想之后,我们来分析一下二分查找查找的时间复杂度,首先我们要明确一点,二分查找是一种非常高效的查找算法。通过分析其时间复杂度可以发现,我们假设数据大小为n,每次查找后数据大小都缩减为原来的大小。到一半,直到最后数据量减少到1,此时停止,如果我们用数据来描述它变化的规律,就是:n,n/2,n/4,n/8,n/16,n/32,...............,1;可以看出这是一个几何序列,当数据大小变为1时:![上传文件...]()其中k的值为总收缩次数。而每次归约操作只涉及两个数据的大小比较,所以经过k个区间的归约操作后,通过计算k的值,我们可以得到二分查找的时间复杂度为O(logn),这是一个非常高效的时间复杂度有时甚至比O(1)复杂度更高效,为什么这么说呢?因为对于logn来说,即使n很大,logn的值也会很小。我们在学习O(1)复杂度时说O(1)表示一个恒定级别的复杂度。据说代码只需要执行一次,有时候可能会执行100次,1000次等常量级的复杂度可以用O(1)来表示。因此,时间复杂度为常量级的算法有时可能没有O(logn)算法执行效率高。1.3.代码实现二分查找有基于循环的实现方式和基于递归的实现方式。下面是这两种方法编写的代码模板1.基于循环的二分查找代码模板//返回数组中的元素SubscriptinpublicintbinarySearch(int[]array,inttarget){intleft=0,right=array.length-1,中间;while(left<=right){//mid=(left+right)>>1mid=left+((right-left)>>1);if(array[mid]==target){returnmid;}elseif(array[mid]>target){right=mid-1;}else{左=中+1;}}返回-1;}使用mid来不断接近我们的目标值。在比较好的情况下,我们直接在某个段的中间找到目标值。最坏的情况,我们不断逼近,最后left==right找到了目标值,当然如果真的找不到目标值,就是left>right的时候了。2.基于递归的二分查找代码模板publicintrecurBinarySearch(int[]array,inttarget,intleft,intright){//terminalif(left>right){return-1;}//处理当前计算中间元素下标intmid=left+((right-left)>>1);if(array[mid]==target){returnmid;}elseif(array[mid]>target){//向下钻取returnrecurBinarySearch(array,target,left,mid-1);}else{returnrecurBinarySearch(array,target,mid+1,right);}}进阶:二分查找的实现可以分为两类1,有无序号重复元素的简单实现;2:按顺序重复元素的变形实现。对于第一种,上面已经给出了代码模板。对于第二种,在实际应用场景中可能会出现以下几种情况:2.1.从数据序列中查找第一个值等于给定值的元素,例如查找{6,12,15,19,24,26,29,29,29,67}中的第一个元素等于292.2、从数据序列中找出最后一个值等于给定值的元素。还是只是元素序列,找到最后一个等于29的元素2.3,从数据序列中找到第一个大于等于给定值的元素。2.4.从数据序列中查找其值小于或等于给定值的最后一个元素。课后思考:针对这四种情况,代码应该如何实现?1.4.应用场景表明,二分查找的时间复杂度为O(logn),效率非常高。这是否意味着在所有情况下都可以使用二分查找?下面谈谈二分查找的应用前提1、要查找的数据序列必须是有序的。二分查找对于这个要求更加严格。查找的数据序列必须有序(单调递增或单调递减)。如果数据是乱序的,那么我们需要先排序,再进行二分查找,如果我们针对的是一组固定的静态数据,也就是说数据序列不会进行插入和删除操作,那么我们可以排序先进行二分查找,这样一次排序多次查找;但是如果数据序列本身不是固定静态的,并且可能涉及到数据序列的插入和删除操作,那么我们需要在每次查找之前进行排序,代价是非常大的。2.数据的存储依赖于数组。需要查找的数据序列存储在数组中,也就是说依赖于顺序存储结构。那是不是不能用其他结构来存储要查找的数据序列呢?比如你用链表来存储,答案是不可能的。根据我们前面实现的二分查找过程,二分查找算法需要根据下标、left、right、mid来访问数据序列中的元素,而数组是根据下标来访问的,元素的复杂度为O(1),而链表访问元素的时间复杂度是O(n),所以如果使用链表存储数据,二分查找的时间复杂度会变得很高。3.数据序列有上下边界。数据序列有上下边界,这样就可以找到中间点,这样就可以一分为二了。4.数据量太小或太大不能使用二分查找。当数据量较小时,没有必要使用二分查找。用循环遍历就够了,因为二分查找只有在数据量比较大的时候才能体现出来。优点,但在某些情况下,即使数据量很小,也推荐使用二分查找。比如数据序列中的数据是一些很长的字符串,对这些很长的字符串进行比较的开销会很大,所以我们希望尽可能减少比较的次数,这样有助于提高性能。那为什么数据量太大的时候不推荐使用二分查找呢?因为刚才我们提到二分查找底层需要依赖数组来存储要查找的数据序列,而数组的特点就是需要连续的内存空间。比如现在有1G的订单数据,如果用数组来存储,需要1G的连续内存。即使还有2G的剩余内存空间,如果这2G的内存空间不是连续的,申请1G大小的数组空间也是不可能的,所以说数据量太大,不适合二进制搜索。2.面试实战69.x的平方根跳动,美团点评最近的面试题,69.x的平方根classSolution{//求sqrt(x):x=n^2(n>0),也就是众所周知的抛物线右边(y=x^2),单调递增,有上下界,[1,x/2]publicintmySqrt(intx){//特殊判断如果(x<=1){返回x;}//找一个数k,k^2<=x,找到最大的k就是我们想要的longleft=1,right=x>>1,mid=0;while(left<=right){mid=(left+right)>>1;如果(中*中x){right=mid-1;}else{return(int)mid;}}返回(int)left-1;}}代码解释:1:如果找到一个mid^2=x,可以在while循环中直接返回,2:如果在while循环中没有找到,就像x=8,我们在[1,2,3,4],while循环中的最后一个循环left==right==3,我们只需要找到k^2<=x的最大k值即可。进阶:牛顿迭代法解决此题!参考选题:https://leetcode-cn.com/probl...方法二类似题目:367.有效完全平方数类解法{publicbooleanisPerfectSquare(intx){//特殊判断if(x<=1){返回真;}longleft=1,right=x>>1,mid=0;while(left<=right){mid=(left+right)>>1;如果(中间*中间x){right=mid-1;}else{返回真;}}返回假;}}33.搜索旋转排序数组字节,快手,百度近期面试题,33.搜索旋转排序数组二分搜索变形题考虑点:1:二分条件:满足上下边界,数组存储即可使用下标求得,单调性,原数组是单调递增的,虽然旋转后整体不单调,但有一半一定是单调递增的。2:为了达到logn的复杂度,必须二分,但是不能简单套用二分模板。我们需要找到哪一半部分是单调的,判断目标是否在这个范围内。如果是,那么就在这里Partially找到目标,如果不是,则在另一半中搜索。classSolution{publicintsearch(int[]nums,inttarget){//这里left和right代表下标intleft=0,right=nums.length-1,mid=0;while(left<=right){mid=(left+right)>>1;if(nums[mid]==target){returnmid;}//前半部分是有序的if(nums[left]<=nums[mid]){//target如果在前半部分就在前半部分找,否则在后半部分找if(target=nums[left]){right=mid-1;}else{左=中+1;}}else{//后半段有序//如果目标在后半段,则在后半段查找,否则在前半段查找if(target>nums[mid]&&target<=nums[right]){left=mid+1;}else{右=中-1;}}}返回-1;}}153.在旋转排序的数组中找到最小值序数数组中的最小值classSolution{//分了,但是不能应用简单的二分模板,修改二分的判断条件publicintfindMin(int[]nums){intleft=0,right=nums.长度-1,中=0;/*这里如果left==right,可以直接返回,最小的元素一定是它*/while(left>1;if(nums[mid]nums[right]){//右边的区间是不连续的,最小值必须在这个区间内left=mid+1;}}返回nums[左];}}进阶思维题:利用二分查找,找到一个中间没有顺序的半有序数组[4,5,6,7,0,1,2]74.搜索二维矩阵新浪,爱奇艺,三星面试题,74.搜索二维矩阵解题思路:标准的二元mxn矩阵可以看成是一个mxn有序数组。参考官方解法:https://leetcode-cn.com/probl...classSolution{//标准的二进制mxn矩阵可以看作是mxn有序数组publicbooleansearchMatrix(int[][]matrix,inttarget){intm=matrix.length;如果(m==0){返回假;}整数n=矩阵[0].长度;int左=0;int右=m*n-1;intmid=0,row=0,col=0;while(left<=right){mid=(left+right)>>1;//最重要的是将mid转化为二维数组中的下标row=mid/n;col=mid%n;如果(矩阵[行][列]==目标){返回真;}elseif(matrix[row][col]