二分查找总结

二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,而在LeetCode中,要运用二分搜索法来解的题目也有很多,但是实际上二分查找法的查找目标有很多种,而且在细节写法也有一些变化。之前有网友留言希望博主能针对二分查找法的具体写法做个总结,博主由于之前一直很忙,一直拖着没写,为了树立博主言出必行的正面形象,不能再无限制的拖下去了,那么今天就来做个了断吧,总结写起来~ (以下内容均为博主自己的总结,并不权威,权当参考,欢迎各位大神们留言讨论指正)

根据查找的目标不同,博主将二分查找法主要分为以下五类:

 

第一类: 需查找和目标值完全相等的数

这是最简单的一类,也是我们最开始学二分查找法需要解决的问题,比如我们有数组[2, 4, 5, 6, 9],target = 6,那么我们可以写出二分查找法的代码如下:

int find(vector<int>& nums, int target) 
{int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] == target) return mid;else if (nums[mid] < target) left = mid + 1;else right = mid - 1;}return -1;
}

会返回3,也就是target的在数组中的位置。注意二分查找法的写法并不唯一,主要可以变动地方有四处:

第一处是right的初始化,可以写成 nums.size() 或者 nums.size() - 1

第二处是left和right的关系,可以写成 left < right 或者 left <= right

第三处是更新right的赋值,可以写成 right = mid 或者 right = mid - 1

第四处是最后返回值,可以返回left,right,或right - 1

但是这些不同的写法并不能随机的组合,像博主的那种写法,若right初始化为了nums.size(),那么就必须用left < right,而最后的right的赋值必须用 right = mid。但是如果我们right初始化为 nums.size() - 1,那么就必须用 left <= right,并且right的赋值要写成 right = mid - 1,不然就会出错。所以博主的建议是选择一套自己喜欢的写法,并且记住,实在不行就带简单的例子来一步一步执行,确定正确的写法也行。

第一类应用实例:

Intersection of Two Arrays

 

 

第二类: 查找第一个不小于目标值的数( >= ),可变形为查找最后一个小于目标值的数  

这是比较常见的一类,因为我们要查找的目标值不一定会在数组中出现,也有可能是跟目标值相等的数在数组中并不唯一,而是有多个,那么这种情况下nums[mid] == target这条判断语句就没有必要存在。比如在数组[2, 4, 5, 6, 9]中查找数字3,就会返回数字4的位置;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回第一个数字1的位置。我们可以使用如下代码:

int find(vector<int>& nums, int target) 
{int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] < target) left = mid + 1;else right = mid;}return right;
}

最后我们需要返回的位置就是right指针指向的地方。在C++的STL中有专门的查找第一个不小于目标值的数的函数lower_bound,在博主的解法中也会时不时的用到这个函数。但是如果面试的时候人家不让使用内置函数,那么我们只能老老实实写上面这段二分查找的函数。

这一类可以轻松的变形为查找最后一个小于目标值的数,怎么变呢。我们已经找到了第一个不小于目标值的数,那么再往前退一位,返回right - 1,就是最后一个小于目标值的数。

第二类应用实例:

Heaters, Arranging Coins, Valid Perfect Square,Max Sum of Rectangle No Larger Than K,Russian Doll Envelopes

 

第二类变形应用:Valid Triangle Number

 

第三类: 查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数

这一类也比较常见,尤其是查找第一个大于目标值的数,在C++的STL也有专门的函数upper_bound,这里跟上面的那种情况的写法上很相似,只需要添加一个等号,将之前的 nums[mid] < target 变成 nums[mid] <= target,就这一个小小的变化,其实直接就改变了搜索的方向,使得在数组中有很多跟目标值相同的数字存在的情况下,返回最后一个相同的数字的下一个位置。比如在数组[2, 4, 5, 6, 9]中查找数字3,还是返回数字4的位置,这跟上面那查找方式返回的结果相同,因为数字4在此数组中既是第一个不小于目标值3的数,也是第一个大于目标值3的数,所以make sense;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回坐标5,通过对比返回的坐标和数组的长度,我们就知道是否存在这样一个大于目标值的数。参见下面的代码:

int find(vector<int>& nums, int target) 
{int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) left = mid + 1;else right = mid;}return right;
}

这一类可以轻松的变形为查找最后一个不大于目标值的数,怎么变呢。我们已经找到了第一个大于目标值的数,那么再往前退一位,返回right - 1,就是最后一个不大于目标值的数。比如在数组[0, 1, 1, 1, 1]中查找数字1,就会返回最后一个数字1的位置4,这在有些情况下是需要这么做的。

第三类应用实例:

Kth Smallest Element in a Sorted Matrix

第三类变形应用示例:

Sqrt(x)

 

第四类: 用子函数当作判断关系

这是最令博主头疼的一类,而且通常情况下都很难。因为这里在二分查找法重要的比较大小的地方使用到了子函数,并不是之前三类中简单的数字大小的比较,比如Split Array Largest Sum那道题中的解法一,就是根据是否能分割数组来确定下一步搜索的范围。类似的还有Guess Number Higher or Lower这道题,是根据给定函数guess的返回值情况来确定搜索的范围。对于这类题目,博主也很无奈,遇到了只能自求多福了。

第四类应用实例:

Split Array Largest Sum, Guess Number Higher or Lower,Find K Closest Elements,Find K-th Smallest Pair Distance,Kth Smallest Number in Multiplication Table,Maximum Average Subarray II,Minimize Max Distance to Gas Station,Swim in Rising Water,Koko Eating Bananas

 

第五类: 其他

有些题目不属于上述的四类,但是还是需要用到二分搜索法,比如这道 Find Peak Element,求的是数组的局部峰值。由于是求的峰值,需要跟相邻的数字比较,那么 target 就不是一个固定的值,而且这道题的一定要注意的是right的初始化,一定要是nums.size() - 1,这是由于算出了mid后,nums[mid] 要和 nums[mid+1] 比较,如果right初始化为nums.size()的话,mid+1可能会越界,从而不能找到正确的值,同时 while 循环的终止条件必须是 left < right,不能有等号。

类似的还有一道 H-Index II,这道题的 target 也不是一个固定值,而是 len-mid,这就很意思了,跟上面的 nums[mid+1] 有异曲同工之妙,target 值都随着 mid 值的变化而变化,这里的right的初始化,一定要是nums.size() - 1,而 while 循环的终止条件必须是 left <= right,这里又必须要有等号,是不是很头大 -.-!!!

其实仔细分析的话,可以发现其实这跟第四类还是比较相似,目标值都不是固定的,第四类中虽然是用子函数来判断关系,但大部分时候 mid 也会作为一个参数带入子函数进行计算,这样实际上最终算出来但目标值还是受 mid 的影响,但是 right 却可以初始化为数组长度,循环条件也可以不带等号,大家可以对比区别一下~

第五类应用实例:

Find Peak Element

H-Index II

 

综上所述,博主大致将二分搜索法的应用场景分成了主要这五类,其中第二类和第三类还有各自的扩展。根据目前博主的经验来看,第二类和第三类的应用场景最多,也是最重要的两类。第一类,第四类,和第五类较少,其中第一类最简单,第四类最难,遇到这类,博主也没啥好建议,多多练习吧~

 

参考地址:LeetCode Binary Search Summary 二分搜索法小结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/385351.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

管道的读写行为

使用管道需要注意以下4种特殊情况&#xff08;默认都是阻塞I/O操作&#xff0c;没有设置O_NONBLOCK标志&#xff09;&#xff1a; 1. 如果所有指向管道写端的文件描述符都关闭了&#xff08;管道写端引用计数为0&#xff09;&#xff0c;而仍然有进程从管道的读端读数据&#…

【C++ Primer | 08】课后习题答案

文章目录练习8.13练习8.13 include <iostream> #include <sstream> #include <fstream> #include <string> #include <vector> using namespace std;struct PersonInfo {string name;vector<string> phones; };bool valid(const string&…

管道缓冲区大小

可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为&#xff1a;pipe size 4K&#xff0c;即一个页面大小。也可以使用fpathconf函数来查看&#xff1a; #include <unistd.h> long fpathconf(int fd, int name); 当需要查看管道的大…

FIFO(命名管道)

FIFO常被称为命名管道&#xff0c;以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO&#xff0c;不相关的进程也能交换数据。FIFO是Linux基础文件类型中的一种&#xff08;p,管道文件&#xff09;。但FIFO文件在磁盘上没有数据块&#xff0c;仅仅用来标…

文件进程间通信

使用文件也可以完成IPC&#xff0c;理论依据是&#xff0c;fork后&#xff0c;父子进程共享文件描述符。也就共享打开的文件。 //父子进程共享打开的文件。借助文件进行进程间通信&#xff08;可先打开文件&#xff0c;再创建子进程&#xff09; #include <unistd.h> #…

mmap内存映射、system V共享内存和Posix共享内存

linux内核支持多种共享内存方式&#xff0c;如mmap内存映射&#xff0c;Posix共享内存&#xff0c;以system V共享内存。当内核空间和用户空间存在大量数据交互时&#xff0c;共享内存映射就成了这种情况下的不二选择。它能够最大限度的降低内核空间和用户空间之间的数据拷贝&a…

mmap、munmap函数

#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); int munmap(void *addr, size_t length); void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); 返回&#xff1a;成功&…

mmap和munmap对文件进行操作(读写等)

//mmap、munmap函数的使用 #include <stdio.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h>void sys_err(char *str) {perror(str);exit(1); }…

1017. A除以B (20)

本题要求计算A/B&#xff0c;其中A是不超过1000位的正整数&#xff0c;B是1位正整数。你需要输出商数Q和余数R&#xff0c;使得A B * Q R成立。 输入格式&#xff1a; 输入在1行中依次给出A和B&#xff0c;中间以1空格分隔。 输出格式&#xff1a; 在1行中依次输出Q和R&#…

mmap父子进程间通信

父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags&#xff1a;MAP_PRIVATE&#xff1a;&#xff08;私有映射&#xff09;父子进程各自独占映射区&#xff1b;MAP_SHARED&#xff1a;&#xff08;…

匿名映射

通过使用我们发现&#xff0c;使用映射区来完成文件读写操作十分方便&#xff0c;父子进程间通信也较容易。但缺陷是&#xff0c;每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件&#xff0c;创建好了再unlink、close掉&#xff0c;比较麻烦。…

mmap无血缘关系进程间通信

实质上mmap是内核借助文件帮我们创建了一个映射区&#xff0c;多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享&#xff0c;因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享&#xff0c;当然应该使用MAP_SHA…

【C++ Primer | 13】课后习题答案

文章目录13.1.4节目练习13.2节练习13.2.2练习13.1.4节目练习 练习13.14 #include <iostream> using namespace std;class numbered { private: static int seq; public:numbered() { mysn seq; }int mysn; };int numbered::seq 0;void f(numbered s) { cout <…

信号的概念与机制

信号的共性&#xff1a;1. 简单&#xff08;开销小&#xff0c;且在用或者不用的情况下&#xff0c;开销是一样的&#xff09;&#xff1b;2. 不能携带大量信息&#xff08;如程序执行过程中&#xff0c;出现段错误时&#xff0c; 就会发送一个相关的信号&#xff08;编号为11&…

信号的产生和状态

信号的产生&#xff1a;1.按键产生&#xff0c;如&#xff1a;Ctrlc&#xff08;内核向进程发送信号&#xff0c;杀死该进程&#xff09;、Ctrlz、Ctrl\&#xff1b;2.系统调用产生&#xff0c;如&#xff1a;kill、raise、abort&#xff1b;3.软件条件产生&#xff0c;如&…

【C++ Priemr | 15】虚函数常见问题

1. 在成员函数中调用虚函数&#xff1a; #include <iostream> using namespace std; class CBase { public:void func1(){func2();}virtual void func2() { cout << "CBase::func2()" << endl; } }; class CDerived : public CBase { public:virt…

965. 单值二叉树

如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回 true&#xff1b;否则返回 false。 示例 1&#xff1a; 输入&#xff1a;[1,1,1,1,1,null,1] 输出&#xff1a;true示例 2&#xff1a; 输入&#…

信号四要素

与变量三要素&#xff08;类型、名字、值&#xff09;类似的&#xff0c;每个信号也有其必备4要素&#xff0c;分别是&#xff1a;1.编号&#xff1b;2.名称&#xff08;即编号的宏定义&#xff09; &#xff1b;3.事件&#xff08;引起信号产生的事件&#xff0c;如段错误&…

958. 二叉树的完全性检验

给定一个二叉树&#xff0c;确定它是否是一个完全二叉树。 百度百科中对完全二叉树的定义如下&#xff1a; 若设二叉树的深度为 h&#xff0c;除第 h 层外&#xff0c;其它各层 (1&#xff5e;h-1) 的结点数都达到最大个数&#xff0c;第 h 层所有的结点都连续集中在最左边&a…

信号的产生

&#xff08;1&#xff09;终端按键产生信号&#xff08;与终端交互的进程&#xff09; Ctrl c → 2) SIGINT&#xff08;终止/中断&#xff09; "INT" ----Interrupt Ctrl z → 20) SIGTSTP&#xff08;暂停/停止&#xff09; "T" ----Termin…