第一部分介绍了二分查找的定义和实现原理(时间复杂度的计算),并给出适合一切二分查找题型的zui强模板,对的你没看错就是zui强!
第二部分一些常用的简单(也不简单)二分查找题,第三部分是较困难的题(后面做不出来就放弃了),第四部分是总体的回顾和总结。
[1]文章组织结构和题目均来自力扣二叉查找小卡片
[2]我用的zui强模板来源于开源模板
[3]找最接近的k个元素,参考了cur的题解
原创、保留权力;
1 二分查找的定义以及模板
1.1 定义
二分查找是计算机科学中最基本、最有用的算法之一。 它描述了在有序集合中搜索特定值的过程[1]。
二分查找的应用遍地都是,数值分析课上也讲过了是最基础的搜索算法并有一些针对的改进。这里只研究基础的二分算法:有序队列查找,找队首、队中、队尾,如果队中值大于中间值说明解在后半段、反之说明解在前半段,一次比较减少了一半的查找范围,继续循环二分,直到找到结果。
时间复杂度:查找0~k次之后,有序队列还剩下的元素个数为 $ n,\frac{n}{2},\frac{n}{4},…,\frac{n}{2^k}$ ,假设查找k次之后只剩下1个元素(表示找到了结果),即:
$$
\frac{n}{2^k} = 1, k = \log _2^n
$$
故其**时间复杂度为 $O(\log _2^n)$**。
1.2 最强模板[2]
可以找第一个、最后一个位置,可以处理有重复元素的有序队列。四点要素如下:
- 1、初始化:start=0、end=len-1
- 2、循环退出条件:start + 1 < end
- 3、比较中点和目标值:A[mid] ==、 <、> target
- 4、判断最后两个元素是否符合:A[start]、A[end] ? target
1 | // 二分搜索最常用模板 |
我也看了liweiwei1914
大佬写的推荐用模板一二的文章、但我还是觉得这个更好用一些,不用想直接上手就可以套模板、基本问题都能解决。
2 没那么简单的力扣题
69. x 的平方根
实现
int sqrt(int x)
函数。计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
考虑到结果位于0和x之前,用二分算法,判断条件是mid^2是否<=x
代码:
1 | class Solution { |
2.2 374. 猜数字大小
猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。返回我选出的数字。
思路:还是套模板,==时,左右都可
1 | public class Solution extends GuessGame { |
2.3 33. 搜索旋转排序数组
整数数组
nums
按升序排列,数组中的值 互不相同 。给你 旋转后 的数组
nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。

这题一定要画图解答,图来源力扣官方题解,其实只用花两条线就ok,因为不确定旋转元素的数量,所以mid可能出现在左边那条线、也可能出现在右边那条线,分开考虑即可。
- 一共有四种可能的情况
- mid位于左边,因target的大小取左边或右边集合
- mid位于右边,因target的大小取左边或右边集合
- (注意if判断中我们写更简单的那种情况、复杂的留给else)
- 模板就是一阵猛套,等不等于的没太大关系
代码:
1 | class Solution { |
2.4 278. 第一个错误的版本
假设你有
n
个版本[1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用
bool isBadVersion(version)
接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
思路:找一个出现的元素,直接上模板。while结束之后,因为是要找第一个出现的,注意先判断left再判断right
代码:
1 | public class Solution extends VersionControl { |
2.5 162. 寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
官方题解:
- 首先从数组 nums 中找到中间的元素 mid。若该元素恰好位于降序序列或者一个局部下降坡度中(通过将 nums[i]与右侧比较判断),则说明峰值会在本元素的左边。于是,我们将搜索空间缩小为 mid的左边(包括其本身),并在左侧子数组上重复上述过程。
- 若该元素恰好位于升序序列或者一个局部上升坡度中(通过将 nums[i]与右侧比较判断),则说明峰值会在本元素的右边。于是,我们将搜索空间缩小为 mid 的右边,并在右侧子数组上重复上述过程。
代码:
1 | class Solution { |
2.6 153. 寻找旋转排序数组中的最小值
给你一个元素值 互不相同 的数组
nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
思路:同样是两条直线,但因为最小值必然位于第二段直线的开头,所以选择判断的情况只有两种:
- mid位于左边时,left=mid
- mid位于右边时,right=mid
- 还要注意只有一条直线的特殊情况(就是没有旋转时),只有第二条直线,应该用right=mid。所以这里建议先判断mid位于右边的情况!!
代码
1 | class Solution { |
2.7 34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组
nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target
,返回 [-1, -1]。
思路:
套模板两次,第一次找开始位置,第二次找结束位置。注意第一次要判断是否存在target,如果不存在直接返回[-1.-1]
代码:
1 | class Solution { |
658. 找到 K 个最接近的元素
给定一个排序好的数组 arr ,两个整数 k 和 x ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。
整数 a 比整数 b 更接近 x 需要满足:
|a - x| < |b - x| 或者
|a - x| == |b - x| 且 a < b示例 1:
输入:arr = [1,2,3,4,5], k = 4, x = 3
输出:[1,2,3,4]
解法一:二分法找最左节点
解题思路来源于**[3]**:
我们要从数组中找k个连续的最靠近x的数字,如果我们找到x再向左右伸缩,那么边界问题将会很复杂;但是我们只考虑要取k个数字的左边界,问题就会变得相对简单。时间复杂度:O(logN+K)
1.确定左边界的范围
要取k个数字在数组中,那么左边界范围的最左边可以取到:0,左边界范围的最右边可以取到:数组长度-k;
这个很好理解,如果k与数组长度相等,那么左边界此时取到最小值0,如果x大于数组最后一个数字,此时全部从右侧取,左边界取到最大值:数组长度-k
2.二分法确定固定左边界
这里我自由发挥、直接用最强模板、while循环结束后,多一步判断应该取left还是right
- 判断mid的移动时,比较当前mid对应区间的[mid, mid+k-1]两个左右端点的值,如果mid处小就二分选左边right=mid;反之二分选右边
- 跳出循环之后判断取left还是right时,如果left对应区间的左端点
|arr[left]-x|
小于 下一个区间的右端点|arr[left+k]-x|
,说明left合适,否者用right;
代码:
1 | class Solution { |
解法二:双指针的排除法
解题思路:
left、right
指针分别指向数组首尾,每次比较移动指针”删除“掉离x更远的那个元素,一共需要”删除“arr.length-k
次;时间复杂度:O(N)
代码:
1 | class Solution { |
2.9 50. Pow(x, n)
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。
示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000
思路:
分治的思想,要计算n次方 先计算[n/2]次方,如果n为偶数,返回平方;如果n为奇数,返回*平方x**。如果n是负数, 先计算myPow(x,-n)
,再取倒数。(做一遍忘一遍的题)
代码:
1 | class Solution { |
2.10 367. 有效的完全平方数
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
进阶:不要 使用任何内置的库函数,如 sqrt 。
示例 1:
1
2 输入:num = 16
输出:true示例 2:
1
2 输入:num = 14
输出:false
思路:这题没有什么意思,比求x的平方根更容易一些,只要判断序列中存不存在就好
代码:
1 | class Solution { |
2.11 744. 寻找比目标字母大的最小字母
给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。
在比较时,字母是依序循环出现的。举个例子:
如果目标字母 target = ‘z’ 并且字符列表为 letters = [‘a’, ‘b’],则答案返回 ‘a’
1
2
3
4 输入:
letters = ["c", "f", "j"]
target = "a"
输出: "c"
思路:
因为小写字母按循环的方式比较大小,有一种特殊情况是整个列表的数都小于等于target,此时应该返回第一个元素。。排除此种情况后,字符按绝对的值大小找第一个大于的字符即可,不论第一个大字符位于数组首、中、尾部,二分搜索都可以找。
代码:
1 | class Solution { |
3 更多的练习
3.1 154. 寻找旋转排序数组中的最小值 II
思路:
相比没有重复的题,这次两边都可以画成一个三折的直线。同样只需要注意mid位于左边侧还是右边侧就ok.一般要与nums[right]
比较来判断位于左边还是右边
- 当mid位于左边侧且 不等于right时 可以直接动left指针
- 当mid位于右边侧且 不等于right时 可以直接动right指针
- 当nums[mid] == nums[right],两边都有可能,就只前移right一格
代码:
1 | class Solution { |
太离谱了 后面全是困难题,,借用力扣评论的一句话, “生不出人,我很抱歉”。
4 总结
实践之证明、那第三个模板几乎所有的题都可以做、我愿称之为最强。不需考虑边界反正就是left=mid
或者right=mid
无脑莽夫就行,仅仅只是多了一个步骤需要判断最后left和right究竟谁才真正指向结果。
- 一般要找第一个解答,就先用left尝试
- 如果要找最后一个解答,就先用right尝试
中等难度及以下的题,只需要根据题意考虑一些细节就基本没有问题。还是总结一下所有题目的Tips
- x的平方根:查找范围为[0,x],因为要找[sqrt(x)],所以判断条件是
mid^2<x
,因为要找最接近sqrt(x)的,所以等于是left前移、结果先判断right - 猜数字大小:简单题、大了就前移、小了就后移,等于无所谓
- 搜索旋转排序数组:旋转之后、数字的升序变成两条线,判断条件变成4种:(注意if判断中我们写更简单的那种情况、复杂的留给else)
- mid位于左边,因target的大小取左边或右边集合
- mid位于右边,因target的大小取左边或右边集合
- 第一个错误的版本:没什么特点、因为要找第一个,所以判断结果时、先判断left
- 寻找峰值:只需要找任意一个峰值点,二分搜索,如果n[mid]<n[mid+1],说明是上坡段继续向后找;反之说明到下坡段或者平段,向前找。
- 寻找旋转排序数组中的最小值:旋转后分为两端,但最小值永远位于第二段。所以一共只有两种情况,mid位于左边、或者右边。但要小心、如果数组没有旋转就之后第二段,所以while内、先判断第一段。
- 在排序数组中查找元素的第一个和最后一个位置:最强模板正适合处理这种情况、没意思
- 找到 K 个最接近的元素:这题的思路可以说是“惊才绝艳”,我只找这k个元素的最左边的元素,范围为[0,length-k]
- 如果最左边的元素与x之差 > 大于最右边元素与x之差,,表示需要后移,反之前移
- 当等于时,也前移(找第一小);
- 更重要的判断结果是left还是right,需要比较 left最左端的差 与 right最右边的差的大小、谁小用谁
- Pow(x, n)**:神nb的递归:要计算n次方 先计算[n/2]次方,如果n为偶数,返回平方;如果n为奇数,返回平方*x**。如果n是负数, 先计算
myPow(x,-n)
,再取倒数。 - 寻找旋转排序数组中的最小值 II:重要有重复元素了、左右两边可能都是三段的折线,除了两边重合的地方不好判断,用right–外,另外两种同样是移动指针
- 当mid位于左边侧且 不等于right时 可以直接动left指针
- 当mid位于右边侧且 不等于right时 可以直接动right指针
- 当nums[mid] == nums[right],两边都有可能,就只前移right一格
**”生不出人、我很抱歉”**,出现困难题、我直接投降好吧。