二分查找-力扣题解
Posted 落风的落
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找-力扣题解相关的知识,希望对你有一定的参考价值。
[1]文章组织结构和题目均来自力扣二叉查找小卡片
[2]我用的zui强模板来源于开源模板
[3]找最接近的k个元素,参考了cur的题解
原创、保留权力;
1 二分查找的定义以及模板
1.1 定义
二分查找是计算机科学中最基本、最有用的算法之一。它描述了在有序集合中搜索特定值的过程[1]。
二分查找的应用遍地都是,数值分析课上也讲过了是最基础的搜索算法并有一些针对的改进。这里只研究基础的二分算法:有序队列查找,找队首、队中、队尾,如果队中值大于中间值说明解在后半段、反之说明解在前半段,一次比较减少了一半的查找范围,继续循环二分,直到找到结果。
时间复杂度:查找0~k次之后,有序队列还剩下的元素个数为 ,假设查找k次之后只剩下1个元素(表示找到了结果),即:
故其时间复杂度为 。
1.2 最强模板[2]
可以找第一个、最后一个位置,可以处理有重复元素的有序队列。四点要素如下:
-
1、初始化:start=0、end=len-1 -
2、循环退出条件:start + 1 < end -
3、比较中点和目标值:A[mid] ==、 <、> target -
4、判断最后两个元素是否符合:A[start]、A[end] ? target
// 二分搜索最常用模板
public int search(int[] nums, int target) {
// 1、初始化 left right
int left = 0, right = nums.length-1;
// 2、处理while循环
while(left+1<right){
int mid = left+(right-left)/2;
// 3、比较nums[mid]和target值
if(nums[mid]<target){
left = mid;
}else if(nums[mid]>target){
right =mid;
}else{
//这里找第一个元素
right = mid;
}
}
// 4、最后剩下两个元素,手动判断
if(nums[left]==target) return left;
if(nums[right]==target) return right;
return -1;
}
我也看了liweiwei1914
大佬写的推荐用模板一二的文章、但我还是觉得这个更好用一些,不用想直接上手就可以套模板、基本问题都能解决。
2 没那么简单的力扣题
69. x 的平方根
实现
int sqrt(int x)
函数。计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
考虑到结果位于0和x之前,用二分算法,判断条件是mid^2是否<=x
代码:
class Solution {
public int mySqrt(int x) {
//二分查找 k^2<=x ,,找到第一个小于等于x的mid^2
int left =0, right=x;
while(left+1<right){
int mid = left+(right-left)/2;
if((long)mid*mid<=x){
left = mid;
}else{
right = mid;
}
}
if((long)right*right<=x) return right;
if((long)left*left<=x) return left;
return -1;
}
}
2.2 374. 猜数字大小
猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。请你猜选出的是哪个数字。如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
返回我选出的数字。
思路:还是套模板,==时,左右都可
public class Solution extends GuessGame {
public int guessNumber(int n) {
int left=0, right=n;
while(left+1<right){
int mid = left+(right-left)/2;
if(guess(mid)==-1){
right=mid;
}else if(guess(mid)==1){
left=mid;
}else{
left=mid;
}
}
if(guess(left)==0) return left;
if(guess(right)==0) return right;
return -1;
}
}
2.3 33. 搜索旋转排序数组
整数数组
nums
按升序排列,数组中的值 互不相同 。给你 旋转后 的数组
nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。
这题一定要画图解答,图来源力扣官方题解,其实只用花两条线就ok,因为不确定旋转元素的数量,所以mid可能出现在左边那条线、也可能出现在右边那条线,分开考虑即可。
-
一共有四种可能的情况 -
mid位于左边,因target的大小取左边或右边集合 -
mid位于右边,因target的大小取左边或右边集合 -
(注意if判断中我们写更简单的那种情况、复杂的留给else) -
模板就是一阵猛套,等不等于的没太大关系
代码:
class Solution {
public int search(int[] nums, int target) {
int left=0, right = nums.length-1;
while(left+1<right){
int mid = left+(right-left)/2;
//因为有两段 mid在左右要分开判断
if(nums[mid]>nums[left]){ //mid位于左边
//if判断简单的情况
if(target<=nums[mid]&&target>=nums[left]){
right = mid;
}else{
left = mid;
}
}else if(nums[mid]<nums[right]){ //mid位于右边
//if判断简单的情况
if(target>=nums[mid]&&target<=nums[right]){
left = mid;
}else{
right =mid;
}
}
}
if(nums[left]==target) return left;
if(nums[right]==target) return right;
return -1;
}
}
2.4 278. 第一个错误的版本
假设你有
n
个版本[1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用
bool isBadVersion(version)
接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
思路:找一个出现的元素,直接上模板。while结束之后,因为是要找第一个出现的,注意先判断left再判断right
代码:
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
//就是找第一个满足条件的值呗 最强模板搞起来
int left=1, right = n;
while(left+1<right){
int mid = left+(right-left)/2;
if(isBadVersion(mid)){
right=mid;
}else{
left = mid;
}
}
if(isBadVersion(left)) return left;
if(isBadVersion(right)) return right;
return -1;
}
}
2.5 162. 寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
官方题解:
-
首先从数组 nums 中找到中间的元素 mid。若该元素恰好位于降序序列或者一个局部下降坡度中(通过将 nums[i]与右侧比较判断),则说明峰值会在本元素的左边。于是,我们将搜索空间缩小为 mid的左边(包括其本身),并在左侧子数组上重复上述过程。 -
若该元素恰好位于升序序列或者一个局部上升坡度中(通过将 nums[i]与右侧比较判断),则说明峰值会在本元素的右边。于是,我们将搜索空间缩小为 mid 的右边,并在右侧子数组上重复上述过程。
代码:
class Solution {
public int findPeakElement(int[] nums) {
//比较mid 与 mid+1处的值,,将上升段全部覆盖掉
int left=0,rigth =nums.length-1;
while(left+1<rigth){
int mid = left+(rigth-left)/2;
if(nums[mid]<nums[mid+1]){
left = mid;
}else{
rigth =mid;
}
}
if(nums[rigth]>=nums[left]) return rigth;
else return left;
}
}
2.6 153. 寻找旋转排序数组中的最小值
给你一个元素值 互不相同 的数组
nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
思路:同样是两条直线,但因为最小值必然位于第二段直线的开头,所以选择判断的情况只有两种:
-
mid位于左边时,left=mid -
mid位于右边时,right=mid -
还要注意只有一条直线的特殊情况(就是没有旋转时),只有第二条直线,应该用right=mid。所以这里建议 先判断mid位于右边的情况!!
代码
class Solution {
public int findMin(int[] nums) {
int left=0, right=nums.length-1;
while(left+1<right){
int mid = left+(right-left)/2;
if(nums[mid]<=nums[right]){
right =mid;
}else{
left = mid;
}
//下面这个判断不适用于全升序的情况 错误
// if(nums[mid]>=nums[left]){
// left = mid;
// }else{
// right =mid;
// }
}
if(nums[left]<nums[right]) return nums[left];
return nums[right];
}
}
2.7 34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组
nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target
,返回 [-1, -1]。
思路:
套模板两次,第一次找开始位置,第二次找结束位置。注意第一次要判断是否存在target,如果不存在直接返回[-1.-1]
代码:
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[]{-1,-1};
if(nums.length==0) return res;
//先找开始位置
int left = 0, right = nums.length-1;
while(left+1<right){
int mid = left+(right-left)/2;
if(nums[mid]<target){
left =mid;
}else if(nums[mid]>target){
right = mid;
}else{
right = mid;//找第一个
}
}
if(nums[left]==target||nums[right]==target){
res[0] = (nums[left]==target? left:right);
}else{
return res;
}
//找最后一个元素
left = 0; right = nums.length-1;
while(left+1<right){
int mid = left+(right-left)/2;
if(nums[mid]>target){
right = mid;
}else if(nums[mid]<target){
left = mid;
}else{
left = mid;
}
}
res[1] = (nums[right]==target? right:left);
return res;
}
}
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;
代码:
class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
//这题挺难顶的,二分法找左结点!!一个伟大的思路
//左节点的范围在[0,length-k]之间
int left =0, right = arr.length-k;
while(left+1<right){
int mid = left+(right-left)/2;
if(x-arr[mid]>arr[mid+k-1]-x){
left = mid;
}else if(x-arr[mid]<arr[mid+k-1]-x){
right = mid;
}else{
right = mid;//要找第一个小值
}
}
//判断选left还是right
if(left+k<arr.length){ //防止数组长度为1时越界
if( x-arr[left]<=arr[left+k]-x) left =left;
else left =right;
}
//保存结果
List<Integer> res = new ArrayList<>();
for(int i=0;i<k;i++){
res.add(arr[left+i]);
}
return res;
}
}
解法二:双指针的排除法
解题思路:
left、right
指针分别指向数组首尾,每次比较移动指针”删除“掉离x更远的那个元素,一共需要”删除“arr.length-k
次;时间复杂度:O(N)
代码:
class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
int left = 0, right = arr.length-1;
int deleteNums = arr.length-k;
while(deleteNums>0){
if(Math.abs(x-arr[left])>Math.abs(x-arr[right])){
left++;
}else{
right--;
}
deleteNums--;
}
List<Integer> res = new LinkedList<>();
for(int i=left;i<=right;++i){
res.add(arr[i]);
}
return res;
}
}
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)
,再取倒数。(做一遍忘一遍的题)
代码:
class Solution {
public double myPow(double x, int n) {
//分治的思想,要计算n次方 先计算[n/2]次方,如果n为偶数,返回平方;如果n为奇数,返回平方*x
//如果n是负数, 先计算my(x,-n),再取倒数
return n>=0? powN(x,n): 1.0/powN(x,-n);
}
private double powN(double x, int n){
//递归出口 n=0 返回1.0
if(n==0) return 1.0;
double y = powN(x,n/2);
return n%2==0? y*y: y*y*x;
}
}
2.10 367. 有效的完全平方数
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
进阶:不要 使用任何内置的库函数,如 sqrt 。
示例 1:
输入:num = 16
输出:true示例 2:
输入:num = 14
输出:false
思路:这题没有什么意思,比求x的平方根更容易一些,只要判断序列中存不存在就好
代码:
class Solution {
public boolean isPerfectSquare(int num) {
//跟求x的平方根类似 这里只需要找是否存在一个数的平方等于num
int left = 0, right = num;
while(left+1<right){
int mid = left +(right-left)/2;
if((long)mid*mid<num){
left=mid;
}else if((long)mid*mid>num){
right=mid;
}else{
right =mid;
}
}
if(left*left==num) return true;
if(right*right==num) return true;
return false;
}
}
2.11 744. 寻找比目标字母大的最小字母
给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。
在比较时,字母是依序循环出现的。举个例子:
如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a'
输入:
letters = ["c", "f", "j"]
target = "a"
输出: "c"
思路:
因为小写字母按循环的方式比较大小,有一种特殊情况是整个列表的数都小于等于target,此时应该返回第一个元素。。排除此种情况后,字符按绝对的值大小找第一个大于的字符即可,不论第一个大字符位于数组首、中、尾部,二分搜索都可以找。
代码:
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
//因为循环比较大小 首先要把整个列表都小于等于target的特殊情况区分出来 比如 z
//如果 按正常的值比较 target位于首、中、尾部,二分都能找到
int n = letters.length;
if(letters[0]-target<0 && letters[n-1]-target<=0) return letters[0];
int left =0, right = n-1;
while(left+1<right){
int mid = left+(right-left)/2;
if(letters[mid]-target>0){
right = mid;
}else if(letters[mid]-target<0){
left = mid;
}else{
left = mid;//找大于target的值,所以等于时要往后找
}
}
//System.out.println(left+","+right);
if(letters[left]-target>0) return letters[left];
return letters[right];
}
}
3 更多的练习
3.1 154. 寻找旋转排序数组中的最小值 II
思路:
相比没有重复的题,这次两边都可以画成一个三折的直线。同样只需要注意mid位于左边侧还是右边侧就ok.一般要与nums[right]
比较来判断位于左边还是右边
-
当mid位于左边侧且 不等于right时 可以直接动left指针 -
当mid位于右边侧且 不等于right时 可以直接动right指针 -
当nums[mid] == nums[right],两边都有可能,就只前移right一格
代码:
class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length-1;
while(left+1<right){
int mid = left+(right-left)/2;
if(nums[mid]>nums[right]){
//当mid位于左边侧且 不等于right时 可以直接动left指针
left = mid;
}else if(nums[mid]<nums[right]){
//当mid位于右边侧且 不等于right时 可以直接动right指针
right =mid;
}else{
//当nums[mid] == nums[right],两边都有可能,就只前移right一格
right--;
}
}
if(nums[right]<=nums[left]) return nums[right];
return nums[left];
}
}
太离谱了 后面全是困难题,,借用力扣评论的一句话, “生不出人,我很抱歉”。
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一格
"生不出人、我很抱歉",出现困难题、我直接投降好吧。
以上是关于二分查找-力扣题解的主要内容,如果未能解决你的问题,请参考以下文章
力扣35:搜索插入位置, 总结了二分查找的通用模板写法, 彻底解决几个易混淆问题
力扣35:搜索插入位置, 总结了二分查找的通用模板写法, 彻底解决几个易混淆问题