[刷题]从新审视二分查找算法的代码实现

从新审视二分查找

去年九月份投了人生第一份简历,百度地图公共出行部门的实习生,就抱着感觉面试+碰运气的态度去面试了(结果可想而知,哈哈哈...Emm..面试官都挺好的,只不过三个部门的老大轮流面,每位大佬都面了一个小时可把我面死了,<ps:我猜想由于每一个部门的老大都不想要我,因此都给别的部门来看一看> 并且我仍是人生第一次面试, T.T) 其中第二位面试官问个人算法题让我手撕 二分查找,当时还想,诶?这个面试官这么好,出的题好简单。如今刷了100道leetcode之后,再来审视这个问题发现,并不简单...

其实你们也可能有和我同样的想法,不就是二分查找吗,有什么可难的,
好,如今在没有复习的状况下我问你们几个问题,你们自查一下: 循环的条件是left<right仍是left<=right?二分时是left=mid+1仍是left=mid 一样的是right = mid-1仍是right=mid?对于上面全部的问题:为何这样作?面试

若是你可以对每一步的作法都道的清楚,那么恭喜你,这篇给像我这样的小菜菜看和复习的文章您能够跳过啦!算法

可是若是你和我同样,爆了粗口F&%^!k,好的,先握个手,咱们先一块儿去学习一下liwei大佬如何理解这个问题(篇幅较长,耐心消化):
https://www.liwei.party/2019/... 数组

正文

好了啦,其实我这篇文章也只是对他的一个自我消化,方便我往后复习,以及复习我失败的面试经历(卧薪尝胆啊懂不懂!)函数

liwei大佬的方法我以为有这么三点比较重要:学习

  1. 不断夹逼目标值,经过left<right,避免不知道循环结束后选哪一个边界的尴尬。
  2. 先写好判断的逻辑(即知足哪一个条件必定会怎么怎么样,这一步每每是最重要也最难的部分)
  3. 根据逻辑选择mid(偶数时选左边仍是右边)

多说无益,直接上题:测试

Leetcode704: 二分查找code

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,
写一个函数搜索 nums 中的 target,若是目标值存在返回下标,不然返回 -1。

示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出如今 nums 中而且下标为 4

学会了以后先练个手,这很简单吧,咱们这里把思考过程写清楚一点
这里目标值target也给出了(PS:有的题目须要本身思考目标值),
1.首先肯定左边界和右边界 left = 0; right = nums.length-1;
2.写出while(left<right){}
3.肯定分支逻辑.对于这一题咱们能够想到若是nums[mid]<target,那么mid及mid左侧的元素里必定不存在目标值,咱们缩减区间为left = mid+1;;那么相应的另外一个分支为num[mid]>=target,这个分支的条件下 mid及mid左侧的元素均可能是target(或者说mid右侧的元素都不多是target),咱们缩减区间right = mid;(对于这一题,其实先从nums[mid]>target的角度思考是同样的啦)
4.咱们的分支逻辑中包括了left=mid+1,为了不死循环,咱们须要定义mid = left + (right-left)/2 (也就是当剩下偶数个元素时,选择左边的那个mid,同理当你的二分逻辑为right=mid-1时,须要定义mid = left + (right-left+1)/2)
5.完成循环后,left==right,此时选left和right都是同样的.但因为target可能不存在数组中(能够简单思考一下这时候left指向了哪里),所以须要特判一下 return nums[left]==target?left:-1;
完整代码以下:排序

class Solution {
    public int search(int[] nums, int target) {
        int len = nums.length;
        int left =0;
        int right=len-1;
        if(len==0) return -1;
        
        while(left<right){
            int mid = left + (right-left)/2;
            if(nums[mid]<target){
                left = mid+1;
            }
            else{
                right = mid;
            }
        }
        return nums[left]==target?left:-1;

    }
}

来再作个应用题
Leetcode69: x的平方根索引

实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
因为返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:
输入: 4
输出: 2

示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 
     因为返回类型是整数,小数部分将被舍去。

这一题求x的平方根取整,你固然能够用库函数,或者遍历O(N)的复杂度,哈哈哈,可是咱不是在练习嘛,来来来,咱们用O(logN)时间复杂度,二分的方法作一下。
都是套路,继续上模板
1.首先肯定左边界和右边界 left = 1; right = x;这里right为啥能够等于能够取到x了呢,由于一个数的平方根多是它本身啊(不用看了,1,说的就是你)
2.写出while(left<right){}
3.肯定分支逻辑.注意,这里由于咱们要找平方根,因此咱们的目标是求一个mid使得( mid * mid ) == x .而且根据题意,小数部分被舍弃,即向下取整,那么使得i * i <=x 的 i 值都有多是所求,但能够肯定的是,使得i * i>x 的 i 值必定不是所求。据此,咱们能够写出分支逻辑:当
mid * mid > x时,mid及mid右侧的元素必定不包括目标值,缩减区间为right = mid - 1;;那么相似于上一个题目,稍(套)加(用)思(模)考(板)另外一else分支为left = mid
4.因为本题目的目标值必定存在,所以最后不须要特判返回便可.
完整代码以下:leetcode

class Solution {
    public int mySqrt(int x) {
        // 0 特判一下
        if(x==0) return 0;
        // 测试用例中存在大整数,所以用long
        long left = 1;
        long right = x;
        long mid;
        while (left<right){
            mid = left + (right-left+1)/2;
            if(mid*mid>x){
                right = mid-1;
            }
            else {
                left = mid;
            }
        }
        return (int)left;
    }
}

"老板!不过瘾,再来一碗酒(题)!"
"呦,客官,您看咱们的招牌上写了‘三碗不过冈’啊.."
行吧,既然你这么要求了

Leetcode35: 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,
并返回其索引。若是目标值不存在于数组中,返回它将会被按顺序插入的位置。

你能够假设数组中无重复元素。

示例 1:
输入: [1,3,5,6], 5
输出: 2

示例 2:
输入: [1,3,5,6], 2
输出: 1

其实这道题目和第一题大差不差,只不过这里数组里不存在目标值时还要求返回应该插入的位置.
由于逻辑判断的思路基本一致,因此再也不赘述思考过程了,这里只讨论两个不同的点
1.初始搜索区间为left=0; right=nums.lenght,由于须要找到插入的位置,所以须要把右边界扩展

你可能会想:诶?当right=nums.length难道这样不会越界吗?
答:可能会。区间收缩的过程当中,可能收缩到这:[nums.length-1,nums.length], 而此时若是你选择了nums.length为mid 那么就会出现越界.但若是你选择了左边的为mid,紧接着left=mid+1, 此时left==right==nums.length,跳出循环,因此你选择左边的mid就不会出现越界

2.若是数组中不存在目标值,跳出循环时,边界指向哪里?在第一题中我也让你们思考了这个问题。如今咱们来讨论一下,能够确定的是,此时两个边界值相同,且必定指向第一个小于目标值的元素或者第一个大于目标值的元素,那具体指向哪个呢?答案是包含了mid的那个分支条件,更具体来讲即left=midright=mid中的其一,对应的即第一个小于目标值的元素或第一个大于目标值的元素.由于mid一直包含在这个分支中。

完整代码以下:

public class Solution{
    public static int searchInsert(int[] nums, int target) {
        if(nums.length==0) return 0;
        int left = 0;
        int right = nums.length;
        while (left<right){
            int mid = left + (right-left)/2;
            // 若是mid 小于target ,mid及mid左侧必定不是
            if(nums[mid]<target){
                left = mid+1;
            }
            // 若是 mid 大于等于target, mid或mid左侧多是,但右侧必定不是
            else {
                right = mid;
            }
        }
        // 由于在变化的是左边界,而右边界包含了mid,
        // 最后边界会收缩到右边界的分支,即target的索引(找到了)或第一个>target的索引(没找到)
        // 而没找到时,这个索引恰好是应该插入的位置
        return left;
    }
}

这段代码等价于:

public int searchInsert(int[] nums, int target) {
        if(nums.length==0) return 0;
        if(target<nums[0]) return 0;
        if(target>nums[nums.length-1]) return nums.length;
        int left = 0;
        int right = nums.length-1;
        while (left<right){
            int mid = left + (right-left+1)/2;
            // 若是mid 大于target ,mid及mid右侧必定不是
            if(nums[mid]>target){
                right = mid-1;
            }
            // 若是 mid 小于等于target, mid或mid右侧多是,但左侧必定不是
            else {
                left = mid;
            }
        }
        // 由于在变化的是右边界 因此最后区间会收缩到左边界的条件
        // 所以咱们须要判断一下
        return nums[left]==target?left:left+1;
    }

第二段代码换了一个起始的思考过程,却同时带来了各类麻烦,咱们须要各类特判来解决麻烦。形成麻烦的缘由就在于跳出循环时,若该元素不存在,边界的指向位置在应插入位置的左侧。
你们能够经过nums=[1,2] target=0; nums=[1,2] target=3这个两个例子本身手动模拟一遍两种不一样的方式,深入理解边界的停留位置。

未完待续....

相关文章
相关标签/搜索