草庐IT

发现了二分查找的秘密

博学谷狂野架构师 2023-04-19 原文

二分查找(Binary Search)算法,也叫折半查找算法。

1.1、原理分析

二分查找是一种非常简单易懂的快速查找算法,其思想在生活中随处可见,比如朋友聚会的时候爱玩的一个猜数游戏,我随机写一个0-100之间的数字,然后大家依次来猜,猜的过程中大家每猜一次我都会告诉大家猜大了还是猜小了,直到有人猜中为止,猜中的人会有一些惩罚措施。这个过程其实就是二分查找思想的一种体现。

回到实际的开发场景中,假设有10个订单,其金额分别是:6,12,15,19,24,26,29,35,46,67。请从中找出订单金额为15的订单,利用二分查找的思想我们每次都与区间中间的数据进行大小的比较以缩小查找的范围,下面这幅图代表了查找的过程,其中 left,right代表了待查找区间的下标,mid表示待查找区间中间元素的下标(如果范围区间是偶数个导致中间数有两个就选择较小的那个)

通过这个查找过程我们可以对二分查找的思想做一个汇总:二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0

1.2、复杂度分析

理解了二分查找的思想后我们来分析二分查找的时间复杂度,首先我们要明确二分查找是一种非常高效的查找算法,通过分析其时间复杂度我们就可以发现,我们假设数据大小为n,每次查找完后数据的大小缩减为原来的一半,直到最后数据大小被缩减为1此时停止,如果我们用数据来描述其变化的规律那就是:

n,n/2,n/4,n/8,n/16,n/32,.......................,1;

可以看出来,这是一个等比数列,当数据大小变为1时:

其中k 的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,通过计算k的值我们可以得出二分查找的时间复杂度就是 O(logn)

这是一种非常高效的时间复杂度,有时候甚至比O(1)复杂度更高效,为什么这么说呢?因为对于log n来说即使n非常的大对应的log n的值也会很小,之前在学习O(1)复杂度时我们讲过O(1)代表的是一种常量级复杂度并不是说代码只需要执行一次,有时候可能要执行100次,1000次这种常数级次数的复杂度都可以用O(1)表示,所以,常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。

1.3、代码实现

二分查找的实现方式有基于循环的实现方式,也有基于递归的方式,现给出这两种方式编写的代码模板

1、基于循环的二分查找代码模板

// 返回的是元素在数组中的下标
public int binarySearch(int[] array, int target) {
        int left = 0, right = array.length - 1, mid;
        while (left <= right) {
            // mid = (left + right)>> 1
            mid  = left + ((right - left) >>1) ;

            if (array[mid] == target) {
                return mid;
            } else if (array[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return -1;
    }

用mid不断去逼近我们的目标值,相对好的情况直接在某段中间找到了目标值,最坏的情况是不断去逼近,最后left==right找到目标值,当然如果真的找不到目标值也就是left>right的时候。

2、基于递归的二分查找代码模板

public int recurBinarySearch(int[] array, int target,int left,int right) {
        //terminal
        if (left > right) {
            return -1;
        }
        //process current   计算中间元素的下标
        int mid = left + ((right - left)>>1);
        if (array[mid] == target) {
            return mid;
        }else if (array[mid] > target) {
            //drill down
            return recurBinarySearch(array,target,left,mid-1);
        }else {
            return recurBinarySearch(array,target,mid+1,right);
        }
    }

进阶:二分查找的实现我们可以分为两大类情况

1,有序数列中不存在重复元素的简单实现;

2:有序数列中存在重复元素的变形实现,

针对第一种,上面已经给出了代码模板,针对第二种,在实际的应用场景中可能会出现如下几种情况:

2.1、从数据序列中查找第一个值等于给定值的元素,比如在{6,12,15,19,24,26,29,29,29,67}中找第一个等于29的元素

2.2、从数据序列中查找最后一个值等于给定值的元素。还是刚刚的元素序列,找最后一个等于29的元素

2.3、从数据序列中查找第一个大于等于给定值的元素。

2.4、从数据序列中查找出最后一个值小于等于给定值的元素。

课后思考:针对这四种情况,代码应该如何实现呢?

1.4、应用场景说明

二分查找的时间复杂度是O(log n),其效率非常高,那是不是说所有情况下都可以使用二分查找呢?下面我们讨论一下二分查找的应用前提

1、待查找的数据序列必须有序

二分查找对这一要求比较苛刻,待查找的数据序列必须是有序的(单调递增或者单调递减),假如数据无序,那我们要先排序,然后二分查找,如果我们针对的是一组固定的静态数据,也就说该数据序列不会进行插入和删除操作,那我们完全可以先排序然后二分查找,这样子一次排序多次查找;但是如果数据序列本身不是固定的静态的,可能涉及数据序列的插入和删除操作,那我们每次查找前都需要进行排序然后才能查找,这样子成本非常的高。

2、数据的存储依赖数组

待查找的数据序列需要使用数组进存储,也就是说依赖顺序存储结构。那难道不能用其他的结构来存储待查找的数据序列吗?比如使用链表来存储,答案是不可以的,通过我们前面实现的二分查找的过程可知,二分查找,算法需要根据下标,left,right,mid来访问数据序列中的元素,数组按照下标访问元素的复杂度是O(1),而链表访问元素的时间复杂度是O(n),因此如果使用链表来存储数据二分查找的时间复杂度就会变得很高。

3、数据序列存在上下边界

数据序列有上下边界,才能找到中间点,这样才能二分下去。

4、数据量太小或太大都不适合用二分查找

数据量很小的情况下,没有必要使用二分查找,使用循环遍历就够了,因为只有在数据量比较大的情况下二分查找才能体现出优势,不过在某些情况下即使数据量很小也建议大家使用二分查找,比如数据序列中的数据都是一些长度非常长的字符串,这些长度非常长的字符串比较起来也会非常的耗时,所以我们要尽可能的减少比较的次数,这样反倒有助于提高性能。

那为什么数据量太大的情况下也不建议使用二分查找呢?因为我们前面刚讲到二分查找底层需要依赖数组存储待查找的数据序列,而数组的特点是需要连续的内存空间,比如现在有1G的订单数据,如果用数组来存储就需要1G的连续内存,即便有2G的剩余内存空间,如果这2G的内存空间不连续那也无法申请到1G大小的数组空间,所以我们说数据量太大也不适合用二分查找。

2、面试实战

69. x 的平方根

字节跳动,美团点评最近面试题,69. x 的平方根

class Solution {
     // 求sqrt(x)即求:x=n^2 (n>0),就是我们所熟知的抛物线(y=x^2)右侧,单调递增,且有上下界,[1,x/2]
    public int mySqrt(int x) {
        //特殊判断
        if (x <=1) {
            return x;
        }
        //找一个数k,k^2<=x,找一个最大的k就是我们想要的
        long left=1,right = x>>1,mid = 0;
        while (left <=right ) {
            mid = (left+right) >>1;
            if (mid * mid  < x) {
                left = mid + 1;
            }else if (mid * mid > x) {
                right = mid - 1;
            }else {
                return (int)mid;
            }
        }
        return (int)left-1;
    }
}

代码解释:

1:如果正好找到一个mid^2 = x则在while loop中就可以直接返回了,

2:如果while loop中还没找到,就类似x=8,我们在[ 1,2,3,4 ]中去寻找,while loop中最后一次循环left == right == 3,我们只需找k^2 <=x的最大k值即可。

进阶:牛顿迭代法解决该问题!参考精选题解:https://leetcode-cn.com/problems/sqrtx/solution/er-fen-cha-zhao-niu-dun-fa-python-dai-ma-by-liweiw/方法二

同类题目367. 有效的完全平方数

class Solution {
    public boolean isPerfectSquare(int x) {
        //特殊判断
        if (x <=1) {
            return true;
        }
        long left=1,right = x>>1,mid = 0;
        while (left <=right ) {
            mid = (left+right) >>1;
            if (mid * mid  < x) {
                left = mid + 1;
            }else if (mid * mid > x) {
                right = mid - 1;
            }else {
                return true;
            }
        }
        return false;
    }
}

33. 搜索旋转排序数组

字节,快手,百度最近面试题,33. 搜索旋转排序数组

二分查找的变形题目

思考要点:

1:二分的条件:满足有上下边界,数组存储可利用下标获取,单调性这块原始数组是单调递增的,旋转之后虽然整体不是单调性的,但是其中有一半一定是单调递增的。

2:要达到log n的复杂度肯定是要二分,但是并不能简单套用二分的模板,我们需要先找到哪半部分是具备单调性的,并判断target是否在该范围内,如果在则在这部分查找target,如果不在则在另外半部分查找。

class Solution {
    public int search(int[] nums, int target) {
        //此处left,right代表的是下标
        int left=0,right = nums.length-1,mid=0;
        while (left <= right) {
            mid = (left+right) >>1;

            if (nums[mid] == target) {
                return mid;
            }

            //前半部分有序
            if (nums[left] <= nums[mid]) {
                //target如果在前半部分则在前半部分找,否则在后半部分找
                if (target < nums[mid] && target >=nums[left] ) {
                    right = mid -1;
                }else {
                    left = mid + 1;
                }
            }else {
                //后半部分有序
                //target如果在后半部分则在后半部分找,否则在前半部分找
                if (target > nums[mid] && target <= nums[right]) {
                    left = mid +1;
                }else {
                    right = mid -1;
                }
            }

        }
        return -1;

    }
}

153. 寻找旋转排序数组中的最小值

网易,拼多多,今日头条面试题,153. 寻找旋转排序数组中的最小值

class Solution {
    //二分,但不能套用简单的二分模板,修改二分的判断条件
    public int findMin(int[] nums) {
        int left=0,right=nums.length-1,mid = 0;

        /*
            在这里如果left==right则可以直接返回了,最小元素一定是它
        */
        while (left < right ) {

            mid = (left + right) >>1;
            
            if (nums[mid] < nums[right]) {//右区间连续,最小值一定在左半区间
                //mid可能是最小值也可能不是,简单二分的模板代码写right=mid-1;
                right = mid;
            }else if (nums[mid] > nums[right]) { //右边区间不连续,最小值一定在该区间内
                left = mid +1;
            }
        }
        return nums[left];

    }
}

进阶思考题:

使用二分查找,寻找一个半有序数组 [4, 5, 6, 7, 0, 1, 2] 中间无序的地方

74. 搜索二维矩阵

新浪,爱奇艺,三星面试题,74. 搜索二维矩阵

解题思路:标准的二分m x n的矩阵可以看成 m x n 的有序数组

参考官方题解:https://leetcode-cn.com/problems/search-a-2d-matrix/solution/sou-suo-er-wei-ju-zhen-by-leetcode/

class Solution {
    //标准的二分m x n的矩阵可以看成 m x n 的有序数组
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        if (m ==0) {
            return false;
        }
        int n = matrix[0].length;
        int left = 0;
        int right = m * n  -1;

        int mid=0,row=0,col=0;

        while (left<=right) {
            mid = (left+right)>>1;
            //最重要的就是将mid转换陈二维数组中的下标。
            row = mid / n;
            col = mid % n;

            if (matrix[row][col] == target) {
                return true;
            }else if (matrix[row][col] < target) {
                left = mid +1;
            }else {
                right = mid-1;
            }
        }
        return false;
    }
}

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

有关发现了二分查找的秘密的更多相关文章

  1. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  2. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  3. ruby-on-rails - 在 Rails 中更高效地查找或创建多条记录 - 2

    我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr

  4. ruby - 查找重叠的正则表达式匹配项 - 2

    我想找到给定字符串中的所有匹配项,包括重叠匹配项。我怎样才能实现它?#Example"a-b-c-d".???(/\w-\w/)#=>["a-b","b-c","c-d"]expected#Solutionwithoutoverlappedresults"a-b-c-d".scan(/\w-\w/)#=>["a-b","c-d"],but"b-c"ismissing 最佳答案 在积极的前瞻中使用捕获:"a-b-c-d".scan(/(?=(\w-\w))/).flatten#=>["a-b","b-c","c-d"]参见Rubyde

  5. ruby - 在 Ruby 中查找多个正则表达式匹配的模式和位置 - 2

    这应该是一个简单的问题,但我找不到任何相关信息。给定一个Ruby中的正则表达式,对于每个匹配项,我需要检索匹配的模式$1、$2,但我还需要匹配位置。我知道=~运算符为我提供了第一个匹配项的位置,而string.scan(/regex/)为我提供了所有匹配模式。如果可能,我需要在同一步骤中获得两个结果。 最佳答案 MatchDatastring.scan(regex)do$1#Patternatfirstposition$2#Patternatsecondposition$~.offset(1)#Startingandendingpo

  6. arrays - Ruby 可枚举 - 查找最多 n 次匹配元素 - 2

    我有以下数组:arr=[1,3,2,5,2,4,2,2,4,4,2,2,4,2,1,5]我想要一个包含前三个奇数元素的数组。我知道我可以做到:arr.select(&:odd?).take(3)但我想避免遍历整个数组,而是在找到第三个匹配项后返回。我想出了以下解决方案,我相信它可以满足我的要求:my_arr.each_with_object([])do|el,memo|memo但是有没有更简单/惯用的方法来做到这一点? 最佳答案 使用lazyenumerator与Enumerable#lazy:arr.lazy.select(&:o

  7. ruby - 使用指向 ruby​​ 可执行文件的符号链接(symbolic link)时查找相关库 - 2

    假设您有一个可执行文件foo.rb,其库bar.rb的布局如下:/bin/foo.rb/lib/bar.rb在foo.rb的header中放置以下要求以在bar.rb中引入功能:requireFile.dirname(__FILE__)+"../lib/bar.rb"只要对foo.rb的所有调用都是直接的,这就可以正常工作。如果你把$HOME/project和符号链接(symboliclink)foo.rb放入$HOME/usr/bin,然后__FILE__解析为$HOME/usr/bin/foo.rb,因此无法找到bar.rb关于foo.rb的目录名.我意识到像ruby​​gems这

  8. 对象的 Ruby 方法查找路径 - 2

    是否有内置的Ruby方法或众所周知的库可以返回对象的整个方法查找链?Ruby查看一系列令人困惑的类(如thisquestion中所讨论)以查找与消息对应的实例方法,如果没有类响应消息,则调用接收方的method_missing。我将以下代码放在一起,但我确信它遗漏了某些情况或者它是否100%正确。请指出任何缺陷并指导我找到一些更好的代码(如果存在)。defmethod_lookup_chain(obj,result=[obj.singleton_class])ifobj.instance_of?Classreturnadd_modules(result)ifresult.last==B

  9. arrays - 在两个数组中查找不相交元素的有效方法是什么? - 2

    我有以下数组:A=[1,2,3,4,5]B=[2,6,7,1]我想找到不相交的元素,如下:output=[3,4,5,6,7]我是这样实现的,output=A+B-(A&B)但它效率低下,因为我添加了两个数组,然后删除了公共(public)元素。它类似于查找不相交的元素。我能做得比这更好吗?如果是,怎么办? 最佳答案 如何只选择A中的元素而不是B中的元素以及B中的元素而不是A中的元素。(A-B)+(B-A) 关于arrays-在两个数组中查找不相交元素的有效方法是什么?,我们在Stack

  10. ruby - 使用 Ruby 在数组中查找大小为 N 的所有子集 - 2

    给定一个数组['a','b','c','d','e','f'],我如何获得包含两个的所有子集的列表、三、四元素?我是Ruby的新手(从C#迁移过来),不确定“Ruby之道”是什么。 最佳答案 查看Array#combination然后是这样的:2.upto(4){|n|array.combination(n)} 关于ruby-使用Ruby在数组中查找大小为N的所有子集,我们在StackOverflow上找到一个类似的问题: https://stackoverf

随机推荐