目录
二分查找算法简介:
二分查找算法算是一个比较经典的算法,它的特点体现在找出题目的二段性(不一定要数组有序,数组无序只要能找到二段性也可以使用二分查找算法),细节多并且二分查找算法可以说是最容易写出死循环的算法,但是如果你理解了原理之后它就会变成最简单的算法,思考时只要找到其二段性,和左右区间要哪一个就可以直接上手写代码。
下面我会给出二分查找算法的3个模板😎😎😎(不要死记硬背),它只是一个参考而已。
模板:
重点要记2,3两个模板,是万能的,第一个模板只是为了让文章更加完整(很简单都会写)。
其实2,3模板的名字不用记题做多了你看题就知道要用那个了。
int mid = left + (right - left) / 2;这是二分求中间值的常见写法,可以有效防止直接right + left 出现溢出的情况(有些题目很恶心😭😭😭)
1.朴素的二分模板
该模板的特点是简单且没有什么细节要处理,但是它比较局限一般只能用来查找元素不重复的情况下。可能有的友友看别的地方求mid时要加个1再除以2,这是声明在朴素二分模板加不加都行,区别主要在2,3两个模板(没处理好会出现死循环的情况)。
2.查找左边界的二分模板
特点:求mid不用加1,while的循环条件不用加上等于条件,把左边区间跳过。
3.查找右边界的二分模板
特点:求mid要加1,while的循环条件不用加上等于条件,把右区间跳过。
总结:记忆技巧下面mid出现减1上面求mid就要加1,否则就不用👌👌👌,2,3模板如果对它的名字感到奇怪的话没有关系,后面通过例题就很清楚如果还是不理解的话(名字不记也罢😎😎😎)。特别注意:模板2,3的求mid加不加1一定要区分清楚否则会出现死循环。
朴素的二分模板例题:
这个简单来一个大家练练手。
参考代码如下:
这个真的很简单就不水文章(不解释了😎😎😎)
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right= nums.length - 1;
while(right >= left){
int mid = left + (right - left) / 2;
if(target > nums[mid]){
left = mid + 1;
}else if(target < nums[mid]){
right = mid - 1;
}else{
return mid;
}
}
return -1;
}
}
2,3模板就不分开了具体情况根据原理来写模板只是参考(规范吧)。
查找左边界&&查找右边界例题:
例题1:
二分查找算法解题思路:
如果要想使用二分查找算法的话那么我们第一步一定是找二段性,我们可以看到数组显然被target分成两端这就是我们的二段性。本题要我们找到target在数组中的开始位置和结束位置,那么显然我们2,3模板都要用到。
查找区间左端点:
通过下图我们可以看见小于t(3)的区间可以全部舍去因为答案不在那个区间,大于等于t的区间就不能舍去答案,在哪不确定。
模板如下:
while(right > left){
int mid = left + (right - left) / 2;
if(target <= nums[mid]){
right = mid;
}else{
left = mid + 1;
}
}
查找区间右端点:
通过下图我们可以看见大于t(3)的区间可以全部舍去因为答案不在那个区间,小于等于t的区间就不能舍去,答案在哪不确定。
模板代码如下:
while(right > left){
int mid = left + (right - left + 1) / 2;
if(target >= nums[mid]){
left = mid;
}else{
right = mid - 1;
}
}
本题细节:如果不存在目标值的话直接返回【-1,-1】数组。
参考代码如下:
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] array = new int[2];
array[0] = array[1] = -1;
if(nums.length == 0){
return array;
}
int left = 0;
int right = nums.length - 1;
while(right > left){
int mid = left + (right - left) / 2;
if(target <= nums[mid]){
right = mid;
}else{
left = mid + 1;
}
}
if(nums[left] != target){
return array;
}
array[0] = left;
left = 0;right = nums.length - 1;
while(right > left){
int mid = left + (right - left + 1) / 2;
if(target >= nums[mid]){
left = mid;
}else{
right = mid - 1;
}
}
array[1] = left;
return array;
}
}
时间复杂度:O(log N)
空间复杂度:O(1)
例题2:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
其实看到时间复杂度要求就知道这题要用二分来做,因为二分的时间复杂度的特点就是logN。
二分算法分析:
首先先找二分性,我们通过分析可以知道答案下标对应的数组值始终大于等于target,例如在
nums = [1,3,5,6],target = 5时答案下标为2对应数组值等于target,当nums = [1,3,5,6],target = 2时答案下标为1对应数组值为3大于2,只有这两种情况。那么我们的二段性就找到了,左边那段小于target,右边那段大于等于target,我们每次二分都舍去左边那一段故就是left = mid + 1,由于右边那一段存储这我们所求的结果故不能舍去right = mid;在分析完left 和right 后由于mid是+1(记忆技巧)故求mid时不用+1就是mid = left + (right - left) / 2;
最终代码如下:
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
if(nums[right] < target){
return right + 1;
}
while(right > left){
int mid = left + (right - left) / 2;
if(nums[mid] >= target){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
时间复杂度:O(log N)
空间复杂度:O(1)
接下来就上点难度了😎😎😎
例题3:
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
说是上点难度,其实就是在找其二段性上了点难度,当然这题可以遍历找最小值,也可以排序后返回最小值但如果实在面试呢,用遍历的话就可以提前回去吃饭了😭😭😭 。
二分算法思路:
说实话我第一做的时候我是完全看不出来这题有什么二分性😭😭😭,直到看了解释才发现有多巧妙,如下图:
我们可以看到在红点处(数组最后一个位置的数)可以将整个数组划分为两端,一段大于红点值一段小于等于红点值,我们的答案在第二段的最左边,故left = mid + 1;把左边那段全部舍去,right = mid;右边不能全部舍去因为答案在那不确定舍去要是把答案给舍去那就。。。悲!,所以mid = left + (right - left) / 2;到这我们的代码就写完了,是不是觉得二分其实还是挺简单的只要把细节处理好代码就几行。
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
int left = 0,right = nums.length - 1;
while(right > left){
int mid = left + (right - left) / 2;
if(nums[mid] > nums[n - 1]){
left = mid + 1;
}else{
right = mid;
}
}
return nums[left];
}
}
面试题:
某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records
。假定仅有一位同学缺席,请返回他的学号。
这题的解法有很多,面试题基本都这样一题都有多解这样可以看看你的水有多深,这题可以使用遍历求解当然如果你面试时使用遍历的话那么offer也就遍历给了别人😭😭😭,这题的解法有(只说思路)
1.哈希表把1~n - 1个数存储在哈希表中然后遍历数组那个哈希表额和时间复杂度和空间都不如直接遍历主要就是炫技当然面试不建议这么写只是面试官问还有什么解法时可以说说。
2.就是暴力直接遍历啦。
3.位运算,两个相同的数进行一次异或( ^ )结果就为0,故我们可以先遍历一次数组把所有数都异或再遍历一次题目数组(少了一个值)最后那个值就是我们的答案,同样这个也是炫技解法比较新颖一点但是时间复杂度还是O(n)。
4.数学(等差数列求和公式),这题可以用等差数列求和公式求出sum再遍历一次数组sum减去数组的每一个值最后剩下的就是我们的答案同样遍历数组我们要的时间复杂度为O(n)
5.二分查找算法:时间复杂度为O(logN)。
综上我们二分查找算法是解决本题的最优算法。
题意也是很简单,一样本题考的在于如何找到其二分性?建议大家先自己想一想再向下看吧!
二分查找思路:
第一步找到其二段性为了方便理解看下图:
蓝色的为数组下标我们可以清楚的看到在前面那一段数组值和其对应的下标是相等的,后面那段是不相等的那么我们是不是就找到了二段性,且右边那段的左端点下标就是我们的答案,那么这题是不是就已经解决了呢,循环条件right > left 这个不用等于用2,3模板不用等于,因为答案在右区间,故左区间我们可以直接舍去故left = mid + 1;右区间有我们要的答案故right = mid;根据上面教大家的记忆法或者友友有别的方法都行我们可以很简单的得出mid = left + (right - left) / 2;
最后这题有个非常奇葩的测试点给我恶心坏了😫😫😫
我真的是无语死了,最后一号的人正好旷课了,那么要这么解决这种情况呢,一般我们的right是取数组下标的最后一个,这题取数组的长度即可(题目给的数组是少了一个人之后的。取数组长度可以正好对应没少人时最后一个元素的下标)。
终于完结撒花🎉🎉🎉
参考代码如下:
class Solution {
public int takeAttendance(int[] records) {
int left = 0,right = records.length;
while(right > left){
int mid = left + (right - left) / 2;
if(records[mid] == mid){
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
}
时间复杂度O(logN)
空间复杂度O(1)
小结:
二分查找算法是一种优秀的算法,时间复杂度为O(logN)可以说是非常快了,重点记忆2,3模板,理解它的特性,名字无所谓会用就行,一道题如果想要使用二分算法的话一定要找到二段性(有二段性并不等于数组有序,例如例题3),找到后看是要那一段配合2,3模板就可以把代码写出来了。
注意:
声明:下面注意点都是2,3模板,1模板太简单没有什么注意的。下面的问号❓表示要不要加一。
1.求mid = left + (right - left + ?) / 2;。这个很重要,不然代码会死循环。
2.循环条件为while(right > left) 最后就是left和right会相等是我们的答案(返回left 或 right都行)。
3.left = mid + ?和right = mid - ? 这个就根据我们要的结果来选择。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。