有了这套模板,再不担心刷不动LeetCode了
《十分好用的二分查找法模板》演示文稿
1、导读
本文介绍了我这半年以来,在刷题过程中使用“二分查找法”刷题的一个模板,包括这个模板的优点、使用技巧、注意事项、调试方法等。
2、历史上有关“二分查找法”的故事
- 算法和程序设计技术的先驱 Donald Ervin Knuth(中文名:高德纳):
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky …
- 同样是高德纳先生,在其著作《计算机程序设计的艺术 第 3 卷:排序和查找》中指出:
二分查找法的思想在 1946 年就被提出来了。但是第 1 个没有 Bug 的二分查找法在 1962 年才出现。
- 《编程珠玑》的作者 Jon Bentley:
When Jon Bentley assigned binary search as a problem in a course for professional programmers, he found that ninety percent failed to provide a correct solution after several hours of working on it, mainly because the incorrect implementations failed to run or returned a wrong answer in rare edge cases.
3、“传统的”二分查找法模板的问题
left
和 right
都比较大的时候,left + right
很有可能超过 int 类型能表示的最大值,即整型溢出,为了避免这个问题,应该写成:
int mid = left + (right - left) / 2
在 right
很大、 left
是负数且很小的时候, right - left
也有可能超过 int
类型能表示的最大值,只不过一般情况下 left
和 right
表示的是数组索引值,left
是非负数,因此 right - left
溢出的可能性很小。
使用“左边界索引 + 右边界索引”,然后“无符号右移 1 位”是推荐的写法。
1、如果目标值(严格)大于排序数组的最后一个数,返回这个排序数组的长度,否则进入第 2 点。2、返回排序数组从左到右,大于或者等于目标值的第 1 个数的索引。
public int searchInsert(int[] nums, int target) {
int len = nums.length;
if (nums[len - 1] < target) {
return len;
}
int left = 0;
int right = len - 1;
while (left <= right) {
int mid = (left + right) / 2;
// 等于的情况最简单,我们应该放在第 1 个分支进行判断
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
// 题目要我们返回大于或者等于目标值的第 1 个数的索引
// 此时 mid 一定不是所求的左边界,
// 此时左边界更新为 mid + 1
left = mid + 1;
} else {
// 既然不会等于,此时 nums[mid] > target
// mid 也一定不是所求的右边界
// 此时右边界更新为 mid - 1
right = mid - 1;
}
}
// 注意:一定得返回左边界 left,
// 如果返回右边界 right 提交代码不会通过
// 【注意】下面我尝试说明一下理由,如果你不太理解下面我说的,那是我表达的问题
// 但我建议你不要纠结这个问题,因为我将要介绍的二分查找法模板,可以避免对返回 left 和 right 的讨论
// 理由是对于 [1,3,5,6],target = 2,返回大于等于 target 的第 1 个数的索引,此时应该返回 1
// 在上面的 while (left <= right) 退出循环以后,right < left,right = 0 ,left = 1
// 根据题意应该返回 left,
// 如果题目要求你返回小于等于 target 的所有数里最大的那个索引值,应该返回 right
return left;
}
}
while (left <= right)
时,在写最后一句 return
的时候,如果不假思索,把左边界 left
返回回去,虽然写对了,但可以思考一下为什么不返回右边界 right
呢?
left
是有一定道理的,如果题目换一种问法,你可能就要返回右边界 right
,这句话不太理解没有关系,我也不打算讲得很清楚(在上面代码的注释中我已经解释了原因),因为实在太绕了,这不是我要说的重点。
传统二分查找法模板,当退出while
循环的时候,在返回左边界还是右边界这个问题上,比较容易出错。
4、“神奇的”二分查找法模板的基本思想
(1)首先把循环可以进行的条件写成 while(left < right)
,在退出循环的时候,一定有 left == right
成立,此时返回 left
或者 right
都可以
没有关系,我们就等到退出循环以后来看,甚至经过分析,有时都不用看,就能确定它是目标数值。
“排除法”即:在每一轮循环中排除一半以上的元素,于是在对数级别的时间复杂度内,就可以把区间“夹逼” 只剩下 1 个数,而这个数是不是我们要找的数,单独做一次判断就可以了。
while (left < right)
模板写法的 2 段参考代码,以下代码的细节部分在后文中会讲到,因此一些地方不太明白没有关系,暂时跳过即可。
[0, size]
。
public int searchInsert(int[] nums, int target) {
# 返回大于等于 target 的索引,有可能是最后一个
int len = nums.length;
if (len == 0) {
return 0;
}
int left = 0;
# 如果 target 比 nums里所有的数都大,则最后一个数的索引 + 1 就是候选值,因此,右边界应该是数组的长度
int right = len;
# 二分的逻辑一定要写对,否则会出现死循环或者数组下标越界
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
}
[0, size - 1]
内使用二分查找法进行搜索。
// 只会把比自己大的覆盖成小的
// 二分法
// 如果有一连串数跟 target 相同,则返回索引最靠前的
// 特例:3 5 5 5 5 5 5 5 5 5
// 特例:3 6 7 8
// System.out.println("尝试过的值:" + mid);
// 1 2 3 5 5 5 5 5 5 6 ,target = 5
// 1 2 3 3 5 5 5 6 target = 4
public int searchInsert(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return -1;
}
if (nums[len - 1] < target) {
return len;
}
int left = 0;
int right = len - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// nums[mid] 的值可以舍弃
left = mid + 1;
} else {
// nums[mid] 不能舍弃
right = mid;
}
}
return right;
}
public static void main(String[] args) {
int[] nums = {1, 2, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6};
int target = 4;
Solution2 solution2 = new Solution2();
int searchInsert = solution2.searchInsert(nums, target);
System.out.println(searchInsert);
}
}
5、细节、注意事项、调试方法
(1)前提:思考左、右边界,如果左、右边界不包括目标数值,会导致错误结果
实现int sqrt(int x)
函数。计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
因此左边界可以取 0 ,右边界可以取 x。
可以分析得再细一点,但这道题没有必要,因为二分查找法会帮你排除掉不符合的区间元素。
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
- 如果
left
和right
表示的是数组的索引,就要考虑“索引是否有效” ,即“索引是否越界” 是重要的定界依据; - 左右边界一定要包括目标元素,例如 LeetCode 第 35 题:“搜索插入位置” ,当
target
比数组中的最后一个数字还要大(不能等于)的时候,插入元素的位置就是数组的最后一个位置 + 1,即(len - 1 + 1 =) len
,如果忽略掉这一点,把右边界定为len - 1
,代码就不能通过在线测评。
(2)中位数先写 `int mid = (left + right) >>> 1 ;` 根据循环里分支的编写情况,再做调整
- 当数组的元素个数是偶数的时候:
int mid = left + (right - left) / 2 ;
得到左中位数的索引;
int mid = left + (right - left + 1) / 2 ;
得到右中位数的索引。
- 当数组的元素个数是奇数的时候,以上二者都能选到最中间的那个中位数。
left = 3
,右边界索引 right = 4
的时候,
mid2 = left + (right - left + 1) // 2 = 3 + (4 - 3 + 1) // 2 = 3 + 1 = 4
mid1
是索引 left
,右中位数 mid2
是索引 right
。
(right - left)
不加 1 选左中位数,加 1 选右中位数。
(3)先写逻辑上容易想到的分支逻辑,这个分支逻辑通常是排除中位数的逻辑;
实现int sqrt(int x)
函数。计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
(4)循环内只写两个分支,一个分支排除中位数,另一个分支不排除中位数,循环中不单独对中位数作判断
不用在每次循环开始单独考虑中位数是否是目标元素,节约了时间,我们只要在退出循环的时候,即左右区间压缩成一个数(索引)的时候,去判断这个索引表示的数是否是目标元素,而不必在二分的逻辑中单独做判断。
left
就至少是 mid + 1
;
mid
排除,因此右边界 right
至多是 mid
,此时右边界不向左边收缩。
(5)根据分支逻辑选择中位数的类型,可能是左中位数,也可能是右位数,选择的标准是避免死循环
1、如果分支的逻辑,在选择左边界的时候,不能排除中位数,那么中位数就选“右中位数”,只有这样区间才会收缩,否则进入死循环;2、同理,如果分支的逻辑,在选择右边界的时候,不能排除中位数,那么中位数就选“左中位数”,只有这样区间才会收缩,否则进入死循环。
# 不妨先写左中位数,看看你的分支会不会让你代码出现死循环,从而调整
mid = left + (right - left) // 2
# 业务逻辑代码
if (check(mid)):
# 选择右边界的时候,可以排除中位数
right = mid - 1
else:
# 选择左边界的时候,不能排除中位数
left = mid
- 在区间中的元素只剩下 $2$ 个时候,例如:
left = 3
,right = 4
。此时左中位数就是左边界,如果你的逻辑执行到left = mid
这个分支,且你选择的中位数是左中位数,此时左边界就不会得到更新,区间就不会再收缩(理解这句话是关键),从而进入死循环; - 为了避免出现死循环,你需要选择中位数是右中位数,当逻辑执行到
left = mid
这个分支的时候,因为你选择了右中位数,让逻辑可以转而执行到right = mid - 1
让区间收缩,最终成为 1 个数,退出while
循环。
(6)退出循环的时候,可能需要对“夹逼”剩下的那个数单独做一次判断,这一步称之为“后处理”。
left
或者 right
,无需再做判断;
nums[left]
或者 nums[right]
(此时 nums[left] == nums[right]
)单独作一次判断,看它是不是你要找的数即可,这一步操作常常叫做“后处理”。
- 如果你能确定候选区间里目标元素一定存在,则不必做“后处理”。
实现int sqrt(int x)
函数。计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
[0, x]
内一定存在,故退出 while (left < right)
循环以后,不必单独判断 left
或者 right
是否符合题意。
- 如果你不能确定候选区间里目标元素一定存在,需要单独做一次判断。
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
(7)取中位数的时候,要避免在计算上出现整型溢出;
int mid = (left + right) / 2;
的问题:在 left
和 right
很大的时候,left + right
会发生整型溢出,变成负数,这是一个 bug ,得改!
int mid = left + (right - left) / 2;
在 right
很大、 left
是负数且很小的时候, right - left
也有可能超过 int 类型能表示的最大值,只不过一般情况下 left
和 right
表示的是数组索引值,left
是非负数,因此 right - left
溢出的可能性很小。因此,它是正确的写法。下面介绍推荐的写法。
int mid = (left + right) >>> 1;
如果这样写, left + right
在发生整型溢出以后,会变成负数,此时如果除以 2 ,mid
是一个负数,但是经过无符号右移,可以得到在不溢出的情况下正确的结果。
>>>
和右移运算符 >>
的区别如下:
- 右移运算符
>>
在右移时,丢弃右边指定位数,左边补上符号位; - 无符号右移运算符
>>>
在右移时,丢弃右边指定位数,左边补上 0,也就是说,对于正数来说,二者一样,而负数通过>>>
后能变成正数。
int mid = (left + right) / 2
与 int mid = left + (right - left) / 2
两种写法都有整型溢出的风险,没有哪一个是绝对安全的,注意:这里我们取平均值用的是除以 2,并且是整除:
int mid = (left + right) / 2
在left
和right
都很大的时候会溢出;int mid = left + (right - left) / 2
在right
很大,且left
是负数且很小的时候会溢出;
left
和 right
一般都表示数组的索引,因此 left
在绝大多数情况下不会是负数并且很小,因此使用 int mid = left + (right - left) // 2
相对 int mid = (left + right) // 2
更安全一些,并且也能向别人展示我们注意到了整型溢出这种情况,但事实上,还有更好的方式;
int mid = (left + right) >>> 1
这种写法,其实是大有含义的:
JDK8 中采用int mid = (left + right) >>> 1
,重点不在+
,而在>>>
。
left
和 high
都是整型最大值的时候,注意,此时 32 位整型最大值它的二进制表示的最高位是 0,它们相加以后,最高位是 1 ,变成负数,但是再经过无符号右移 >>>
(重点是忽略了符号位,空位都以 0 补齐),就能保证使用 +
在整型溢出了以后结果还是正确的。
Collections
和 Arrays
提供的 binarySearch
方法,我们点进去看 left
和 right
都表示索引,使用无符号右移又不怕整型溢出,那就用 int mid = (left + right) >>> 1
好啦。位运算本来就比使用除法快,这样看来使用 +
和 <<<
真的是又快又好了。
int mid = (left + right) >>> 1
吧,反正更多的时候 left
和 right
表示索引。
(8)编码一旦出现死循环,输出必要的变量值、分支逻辑是调试的重要方法。
6、总结
(1)原因:
无脑地写while left < right:
,这样你就不用判断,在退出循环的时候你应该返回left
还是right
,因为返回left
或者right
都对;
(2)技巧:
先写分支逻辑,并且先写排除中位数的逻辑分支(因为更多时候排除中位数的逻辑容易想,但是前面我也提到过,这并不绝对),另一个分支的逻辑你就不用想了,写出第 1 个分支的反面代码即可(下面的说明中有介绍),再根据分支的情况选择使用左中位数还是右中位数;
- 如果第 1 个分支的逻辑是“左边界排除中位数”(
left = mid + 1
),那么第 2 个分支的逻辑就一定是“右边界不排除中位数”(right = mid
),反过来也成立; - 如果第 2 个分支的逻辑是“右边界排除中位数”(
right = mid - 1
),那么第 2 个分支的逻辑就一定是“左边界不排除中位数”(left = mid
),反之也成立。
(3)优点:
分支条数只有 2 条,代码执行效率更高,不用在每一轮循环中单独判断中位数是否符合题目要求,写分支的逻辑的目的是尽量排除更多的候选元素,而判断中位数是否符合题目要求我们放在最后进行,这就是第 5 点;
(4)注意事项1:
左中位数还是右中位数选择的标准根据分支的逻辑而来,标准是每一次循环都应该让区间收缩,当候选区间只剩下 2 个元素的时候,为了避免死循环发生,选择正确的中位数类型。如果你实在很晕,不防就使用有 2 个元素的测试用例,就能明白其中的原因,另外在代码出现死循环的时候,建议你可以将左边界、右边界、你选择的中位数的值,还有分支逻辑都打印输出一下,出现死循环的原因就一目了然了;
(5)注意事项 2:
如果能确定要找的数就在候选区间里,那么退出循环的时候,区间最后收缩成为 1 个数后,直接把这个数返回即可;如果你要找的数有可能不在候选区间里,区间最后收缩成为 1 个数后,还要单独判断一下这个数是否符合题意。
(right - left)
这个括号里面加 1 。
虽说是两个模板,区别在于选中位数,中位数根据分支逻辑来选,原则是区间要收缩,且不出现死循环,退出循环的时候,视情况,有可能需要对最后剩下的数单独做判断。
7、应用提升
◆
精彩推荐
◆
推荐阅读
刘群:华为诺亚方舟NLP预训练模型工作的研究与应用 | AI ProCon 2019
估值被砍700亿美元后,Waymo发重磅公开信:即将推出全自动驾驶打车服务
首届中文NL2SQL挑战赛:千支队伍参赛,国防科大夺冠
图灵奖得主Bengio再次警示:可解释因果关系是深度学习发展的当务之急
技术领域有哪些接地气又好玩的应用?
Python新工具:用三行代码提取PDF表格数据
国产嵌入式操作系统发展思考
2019 年诺贝尔物理学奖揭晓!三得主让宇宙“彻底改观”
公链故事难再续?
你点的每个“在看”,我都认真当成了AI
有了这套模板,再不担心刷不动LeetCode了相关推荐
- 有了这套模板,女朋友再也不用担心我刷不动 LeetCode 了
作者 | 李威 来源 | https://www.liwei.party/ 整理 | 五分钟学算法 全文包含 12000+ 字.30 张高清图片,预计阅读时间为 40 分钟,强烈建议先收藏再仔细阅读. ...
- 释梦代刷网八套模板+源码+教程 全部功能可用
简介: 修复发卡功能,分站功能,解密文件.完全无加密 内含八套模板 带有详细安装步骤,经测试全部功能可用 1.可作为个人发卡网使用 2.分站功能可能,无限分站 3.可对接各大社区或克隆各网站,简单操作 ...
- leetcode每日一题1609. 奇偶树 圣诞节你做奇偶数了没 BFS套模板一遍过
本文目录 leetcode每日一题1609. 奇偶树 圣诞节你做奇偶数了没 BFS套模板一遍过~ 写在前面 题目 示例 提示 思路 代码实现 执行结果 写在最后 leetcode每日一题1609. 奇 ...
- pr扫光转场插件_2020年最新pr转场特效:300套模板+200集视频教程+插件,送你参考...
作为职场小白,如何正确有效的学习好PR剪辑?这是很多新手在入门阶段所迷茫的,PR本身是一款专业的视频剪辑,后期转场,视频特效软件,有很多专业人士在使用也有很多爱好者或需求不是很大的朋友们使用. 学习p ...
- HTML五合一收款码网站源码(带35套模板)
简介: HTML五合一收款码网站源码(带35套模板)是一款基于HTML开发制作的多码合一收款码生成网站源码,支持wx支付,支付宝支付,手机扣扣支付,京东钱包,百度钱包,五合一收款,将其二维码合并为一个 ...
- 云商城云小店官网源码-修复30套模板支持一键对接各大系统
简介: 云小店商城源码修复30套模板支持一键对接各大系统,需PHP7.2以下 上传源码后提示数据库连接错误的,先安装一下 访问:http://你的域名/install进行安装 默认后台地址: 域名/a ...
- php和mysql防伪网站源码,2015年最新php+mysql防伪查询程序源码微信认证查询含7套模板...
系统品牌: 其他系统 开发语言: PHP 其他 数据库: Mysql 其他 前台页面共7套模板,不一一列举图片了,要的自己买回去试试吧! 四大功能特色: 1.防无意多次查询,查询第二次必须刷新页面才能 ...
- 最新易企秀 微场景制作源码 易企秀去版权源码 带几百套模板
分享一个最新易企秀 微场景制作源码 易企秀去版权源码,带几百套模板,一键轻松制作H5,开发组合:PHP+MySQL,含详细搭建教程. 企业及个人运用各种H5页面进行营销,用H5搭建的站点与应用可兼容P ...
- Nuxt - 自定义页面布局,<Nuxt /> 个性化多套模板(一个项目内既要有用户正常浏览的普通页面,又要存在后台管理系统布局的页面)
前言 使用 Nuxt.js 开发项目时,遇到了带 "后台管理" 的功能,即一个正常供用户浏览的页面,点击控制台或个人中心进入后台管理界面,里面为菜单和主体内容的左右布局,点击菜单跳 ...
最新文章
- Windows 日志高级筛选实践
- python第三方库大全win-Python标准库、第三方库和外部工具汇总
- 2015年你必须学习的编程语言和前端框架
- iOS开发 AVAudioPlayer
- Could not fetch URL https://pypi.org/simple/pip/: There was a problem confirming the ssl certificate
- 如何在PAI平台跑实验
- 《HBase权威指南》读书笔记3
- 大学算法分析与设计复习总结
- html怎么设置华文行楷,css如何修改字体为华文行楷
- dubbo源码分析23 -- provider 接收与发送原理
- 多目标优化系列(七)SPEA2
- Linux pwn入门教程,Linux PWN从入门到熟练
- Leco题目:回文数
- Janus videoroom 视频录制
- 与,或,非,异或,左移,右移,位运算符号总结
- 梅尔频谱图与音频相互转化
- 华大开发板SW失效,无法下载程序
- 【CentOS7联网】保姆级手把手解决CentOS7的上网问题
- kmp算法中字符串前后缀公共长度的总结
- 【翻译】西川善司的「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,后篇...