二分查找-力扣题解

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)==0return left;
        if(guess(right)==0return 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==0return 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==0return 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<=0return 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>0return 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:搜索插入位置, 总结了二分查找的通用模板写法, 彻底解决几个易混淆问题

数组: 总结了二分查找的通用模板写法, 彻底解决几个易混淆问题, 力扣35:搜索插入位置

力扣704题 二分查找

力扣(LeetCode)平方数之和 个人题解

二分查找算法复习