草庐IT

【二分查找】一文带你掌握二分法 (附万能模板)

努力学习的小马 2023-05-03 原文

目录

一、简介

哪怕没有学过编程的同学,也许不知道二分法这个名字,但也一定接触过它的核心思想。不了解的同学也没关系,我用一句话就能概括出它的精髓:将一个区间一分为二,每次都舍弃其中的一部分。

二分法能够极大地降低我们在解决问题时的时间复杂度。假如你要在一个单调递增的数组a[n]中寻找一个数target,遍历的话时间复杂度是O(n)。但如果采用二分法,时间复杂度则是O(log n)。这种效率的提高无疑是巨大的!

二、易错点

1、while循环中是写 left < right 还是写 left <= right ?

2、如下图所示,if(nums[mid]>target),右边界更新区间时是写成 right = mid 还是 right = mid-1?

这两点是很容易弄混的。要弄清楚上面这两个问题的答案,首先要确定你想写的循环的区间到底是左闭右开还是左闭右闭?(题目没明确要求的话就看你个人选择,都是可以的)


左闭右闭的区间是这样写的:[left, right]

它包含了 left 和 right 这两个数


左闭右闭的区间是这样写的:[left, right)

它包含了 left,但不包含 right 。


假设我选择了左闭右闭,此时需要在数组nums中寻找一个数target,找到的话返回下标,没找到的话返回 -1,伪代码如下:

left=0;
right=nums.size()-1;
while(l <= r){ // 注意这里不是 l < r
	int mid = l + (r - l)/2;
	if(nums[mid] > target) right = mid-1;
	else if(nums[mid] < target) left = mid+1;
	else{
		return mid;
	}	
}

首先,为什么选择左闭右闭的区间,第3行就要写成<=呢?因为写入while循环的判断条件应该与你选择的区间相吻合,而左闭右闭的区间包含了left和right,也就是说可能会出现 left== right的情况。如果写成<,那么就漏掉了left==right 这种情况,所以应该写成<=。

其次,第5行右边界为什么更新为 mid-1 而不是更新为 mid?因为我们已经确定了nums[mid] > target,所以mid一定不是我们要找的值,因此在下一轮搜索中,不应再包含mid这个数。而我们选择的是左闭右闭区间,它包含了左右边界,每次搜索时都会包含左右边界。所以为了使已经被排除的mid不被再次搜索,右边界应该更新为mid-1。


假设我选择了左闭右开,此时需要在数组nums中寻找一个数target,找到的话返回下标,没找到的话返回 -1,伪代码如下:

left=0;
right=nums.size();
while(l < r){ // 注意这里不是 l <=r
	int mid = l + (r - l)/2;
	if(nums[mid] > target) right = mid;
	else if(nums[mid] < target) left = mid+1;
	else{
		return mid;
	}	
}
return -1;

左闭右开代表包含左边界,不包含右边界。右边界一定比左边界大,不存在 right == left 的情况。所以第3行只能写成<。

而第5行之所以写成right=mid,是因为下一次搜索中本就不会包含right,我们也确定了right不是要找的值,所以直接写成right=mid就可以了。

此时,注意第2行,因为左闭右开不包含右边界,所以右边界要设置为nums.size(),这样才能把nums.size()-1包含在搜索区间里。

三、例子

如果你已经理解了上面的内容,可以尝试做下面这道题:

左闭右闭:

class Solution {
public:
    int search(vector<int>& nums, int target) {
         int l = 0, r =  nums.size() - 1; 
        while(l <= r){ 
            int m = l + (r - l)/2;
            if(nums[m] > target)r = --m;
            else if(nums[m] < target) l = ++m;
            else{
                return m;
            }
        }
    return -1; 
    }
};

左闭右开:

class Solution {
public:
    int search(vector<int>& nums, int target) {
         int l = 0, r =  nums.size(); 
        while(l < r){ 
            int m = l + (r - l)/2;
            if(nums[m] > target)r = m;
            else if(nums[m] < target) l = ++m;
            else{
                return m;
            }
        }
    return -1; 
    }
};

四、万能模板

尽管理解了上面的内容,但很多时候解题还是会遇到困难。

比如说给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

你会发现上面的两个代码很难应用到这种题,所以我找到了一个万能模板,跟大家分享一下:

class Solution {
    /**
     * 如果返回值等于 nums.size(),代表 nums 中不存在 满足 nums[i] >= target 的 i,也就是所有元素都小于 target。
     * 否则返回满足 nums[i] >= target 的最小 i(最左 i)。也就是说,如果有多个连续的 target,会返回最靠左的那个的下标。
     * nums为单调不减数组
     * target为边界值  
     **/
    public: int binarySearch(vector<int>& nums, int target) {
        int l = 0, r = nums.size()- 1; // 二分查找的左右初始边界
        while(l <= r){ // 注意这里不是 l < r
            int m = l + (r - l)/2;
            if(nums[m] >= target)r = --m;
            else l = ++m;
        }
        return l; // 此时,l 代表arr[i] >= x 的最小 i。
    }
};

如何运用这个模板去解决不同的问题呢?请往下看

1、查找某个数 x 首次出现的位置,如果不存在,返回-1。

如果 binarySearch(arr, x) == nums.size(),代表所有元素都小于x,也就无法查找到 x 首次出现的位置;如果有多个元素等于 x,则 binarySearch(arr, x) 代表 x 首次出现的位置的下标;如果binarySearch(arr, x) != x,则不存在某个数等于 x,binarySearch(arr, x) 代表最靠左的大于 x 的数。

2、查找某个数 x 最后出现的位置,如果不存在,返回-1。

将该问题转换为用 int ret = binarySearch(arr, x+1) - 1 来解决。 如果ret < 0,返回 -1;否则,如果nums[ret] == x,ret 就是答案;否则,返回 -1。

3、查找某个数 x 首次出现的位置,如果不存在 x,则求出适合插入 x 的位置。

binarySearch(arr, x)就是。

4、查找小于x的最后一个数。

转换为用binarySearch(arr, x) - 1来解决,注意判断存在性。

5、查找小于x的第一个数。

这是个伪问题。

6、查找大于x的第一个的数。

转换为用binarySearch(arr, x + 1)来解决,注意判断存在性。

7、查找大于x的最后一个的数。

这是个伪问题。

举例:

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4

提示:

  • 1 <= nums.length <= 1 0 4 10^4 104

  • - 1 0 4 10^4 104 <= nums[i] <= 1 0 4 10^4 104

  • nums 为 无重复元素升序 排列数组

  • - 1 0 4 10^4 104 <= target <= 1 0 4 10^4 104

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r =  nums.size() - 1; // 二分查找的左右初始边界
        while(l <= r){ // 注意这里不是 l < r
        int m = l + (r - l)/2;
        if(nums[m] >= target)r = --m;
        else l = ++m;
    }
    return l; // 此时,l 代表arr[i] >= tarhget的最小 i。
    }
};

五、参考资料

1、二分查找万能模板

2、【二分查找】详细图解


📝我的个人主页
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​
💬总结:希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🖊
✉️今天你做别人不想做的事,明天你就能做别人做不到的事♐


有关【二分查找】一文带你掌握二分法 (附万能模板)的更多相关文章

  1. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

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

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

  3. 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

  4. ruby-on-rails - Mandrill API 模板 - 2

    我正在使用Mandrill的RubyAPIGem并使用以下简单的测试模板:testastic按照Heroku指南中的示例,我有以下Ruby代码:require'mandrill'm=Mandrill::API.newrendered=m.templates.render'test-template',[{:header=>'someheadertext',:main_section=>'Themaincontentblock',:footer=>'asdf'}]mail(:to=>"JaysonLane",:subject=>"TestEmail")do|format|format.h

  5. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

  6. 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

  7. 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

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

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

  9. ruby - 如何通过Middleman安装和使用Slim模板引擎 - 2

    一般来说,我是Middleman和ruby​​的新手。我已经安装了Ruby我已经安装了Middleman和gem以使其运行。我需要使用slim而不是默认的模板系统。所以我安装了Slimgem。Slim的网站只说我需要'slim'才能让它工作。中间人网站说我只需要在config.rb文件中添加模板引擎,但是没有给出例子...对于没有ruby​​背景的人来说,这没有帮助。我在git上找了几个config.rb,它们都有:require'slim'和#Setslim-langoutputstyleSlim::Engine.set_default_options:pretty=>true#Se

  10. 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

随机推荐