去年九月份投了人生第一份简历,百度地图公共出行部门的实习生,就抱着感觉面试+碰运气的态度去面试了(结果可想而知,哈哈哈...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大佬的方法我以为有这么三点比较重要:学习
多说无益,直接上题:测试
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=mid
或right=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
这个两个例子本身手动模拟一遍两种不一样的方式,深入理解边界的停留位置。
未完待续....