1.二分查找简介
我们以前对于二分查找的理解主要是运用在有序数组中,但是更合理的说法是,当数组中存在一种规律,根据这个规律,在数组中找一个点,使数组有"二段性",我们就可以使用二分查找算法。
二段性:根据数组中的规律,在数组中找到一个点,这个点将数组划分为两段,其中有一段是可以根据规律舍去的,另一段是要根据题目要求继续寻找的一段,这样的说法,我们称为二段性。
2.二分查找
题目解析:在一个升序数组nums中找到一个与target相等的目标值,如果该目标值存在,则返回其坐标,如果在数组中没有找到该目标值,就返回-1。
算法原理
解法一:暴力枚举
我们可以遍历nums中的每一个数据,一 一与target的只进行比较,该时间复杂度是O(N)。
解法二:二分查找
如上图,如果我们一开始就是找4,由于4小于target值,根据数组升序的规律,我们就可以将4前面的数据给舍弃掉,不用在对4之前的数据进行分析了,而4后面的一段还是未知的,我们还有在4后面的那段继续寻找。
如下图
此时,我们就根据二段性来使用二分查找
我们定义一个left指针指向数组的开头,定义一个right指针指向数组的末端,定义一个mid变量来存储left和right之间的中间值。
mid变量的理解
这个mid变量在我看来,是能将数组分为二段性的一个点,这个mid可以是left和right中二分之一的值,也可以是三分之一,也可以是四分之一,但是总体上来说,我们直接用二分之一就好了,因为二分之一是最快的。
在该题使用二分查找中,我们的主要步骤是找到这个将数组划分为二段性的点。
此时,我们会遇到三中情况。
第一种情况:mid的值小于target值,此时,我们根据二段性,将mid左端的一段干掉,然后让left=mid+1。如下图
第二种情况:mid的值大于target,此时根据二段性,将mid右端一段干掉,然后让right=mid-1。
第三种情况:mid的值等于target,此时,我们直接返回mid就行了。
编写代码:
细节问题
1.结束循环的条件:left>right
因为根据二段性,有一段我们是可以明确干掉的,但是,没有干掉的那段是未知的,即使right和left同时指向一个数时,我们也要对这个数进行判断。
也就是说,循环的基本条件是left<=right
java
public int search(int[] nums, int target) {
int left=0,right=nums.length-1;
while(left<=right){
//int mid=(left+right)/2;//这种写法数据可能会溢出
int mid=left+(right-left)/2;//防止溢出
if(nums[mid]>target) right=mid-1;
else if(nums[mid]<target) left=mid+1;
else return mid;
}
return -1;
}
3.在排序数组中查找元素的第一个位置和最后一个位置
题目链接:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
题目描述:在一个非递减的数组中,找出target值第一次出现和最后一次出现的位置,返回其下标。
算法原理
解法一:暴力枚举
遍历nums数组,当遇到第一个与target值相等的数据时,用一个begin变量存储其下标,接着遍历,如果后面再次遇到想等的值,则定义一个end变量来存储该下标,在接着遍历,如果在遇到符合条件的值,更新end的值就行了,直至遍历完数组。
解法二:二分查找
此时不能直接使用朴素二分查找的模版,因为朴素二分查找的目的就是为了找到一个合法的点,如果我们使用朴素二分查找,找到一个合法的点时,此时,无法确定该点前面和后面是否还有合法的点,则还要分别以这个点为起点,分别向前和向后去遍历,这样时间复杂度会扩展到O(N),这样就和暴力解法没区别了。
此题,我们分为两个步骤,分别为寻找左端点和寻找右端点。
步骤一:寻找左端点
如上图,此时mid指向的值会遇到两种情况。
当mid<target时,说明[left,mid]之间的数据都是不合法的,我们直接让left=mid+1。
当mid>=target时,我们直接让right=mid。
注意事项:我们不能让right=mid-1,因为当第一次mid刚好为左端点时,如果让right=mid-1了,那么right就会跳过左端点,接着在新的[left,right]之间寻找时,就不会找到左端点了。
如下图
细节问题:
1.循环条件:left<right
循环条件是left<right,而不是left<=right**。**
当我们left和right按照上面提到的走法时,left和right最终的结果一定是相遇的,也就是循环的最终结果就是right和left同时指向同一个位置,此时right和left指向的数据就是最终结果。
且如果left==right进入循环时,可能会导致死循环,分析如下:
当数组中存在左端点时,如果当left==right也进入循环时,此时如果触发条件mid<t,left=mid+1,此时,循环就结束,这时候就没啥异常。但是如果触发了mid>=t这个条件,就会让right=mid,由于right的位置没有发生变化,还是符合left==right,此时还是会循环下去,这样就导致了死循环。
2.找中间节点:left+(right-left)/2
找中间节点的公式应该是left+(right-left)/2,而不是left+(right-left+1)/2。
当我们使用left+(right-left+1)/2找中间点时,会导致死循环。如下图
当我们使用left+(right-left)/2找中间点时,就不会导致死循环了,如下图
步骤二:寻找右端点
寻找右端点的分析和寻找左端点的思路差不多,这里简单介绍。
当mid>target时,让right=mid-1,当mid<=target时,让left=mid。
细节处理
1.循环条件
和寻找右端点的分析差不多,循环条件是left<right.
2.找中间节点
此时,我们使用left+(right-left+1),当我们使用left+(right-left+1)找中间节点时,会导致死循环。
代码实现:
java
public int[] searchRange(int[] nums, int target) {
int[] ret=new int[2];
ret[0]=ret[1]=-1;
//处理边界情况
if(nums.length==0) return ret;
//寻找左端点
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else right=mid;
}
if(nums[left]==target) ret[0]=left;
else return ret;
//寻找右端点
left=0;
right=nums.length-1;
while(left<right){
int mid=left+(right-left+1)/2;
if(nums[mid]>target) right=mid-1;
else left=mid;
}
ret[1]=left;
return ret;
}
4.x的平方和
题目链接:69. x 的平方根 - 力扣(LeetCode)
题目分析:就算x的算术平方根,且只保留整数部分。
算法原理:
解法一:暴力枚举
我们可以从0到x都尝试一遍,知道一个数的平方根大于或等于x,如果该数的平方大于x,则返回前面的一个数,如果刚好等于x,则返回这个数。
解法二:二分查找
如下图,如果mid*mid<=x,那么此时mid是有可能刚好是题目要求的数,所以此时让left=mid
如果,mid*mid>x,那么代表mid(包括mid在内)之后的数都不符合题意,则让right=mid-1.
代码实现:
java
public int mySqrt(int x) {
long left=0,right=x;
while(left<right){
long mid=left+(right-left+1)/2;//使用long,是为了防止数据溢出
if(mid*mid<=x) left=mid;
else right=mid-1;
}
return (int)left;
}
5.搜索插入位置
题目链接:35. 搜索插入位置 - 力扣(LeetCode)
题目分析:在一个排序数组中,找回目标值的索引或者目标值按顺序插入的位置。
算法原理:
解法:二分查找
二段性分析
根据题意,返回目标值的下标就是数组中等于目标值的下标或者数组中第一个大于目标值的下标,这样数组就有了二段性。
如下图
根据这两中情况分析
当nums[mid]<target,让left=mid+1,当nums[mid]>=target,让right=mid。
细节处理:
上述的解法只能解决返回的目标值在数组中的情况下,当返回的下标值是数组中最后一个数据的后一个位置,这种情况我们要特殊处理,也就是nums[left]<target的情况。
代码实现:
java
public int searchInsert(int[] nums, int target) {
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else right=mid;
}
//处理插入的位置在数组外面的情况
if(nums[left]<target) return left+1;
return left;
}
6.山脉数组的峰顶索引
题目链接:852. 山脉数组的峰顶索引 - 力扣(LeetCode)
山脉数组
山脉数组就是数组中有一个位置上的数据,该位置之前的数据是一个递增的数据排列,在这个位置之后的数据是一个递减的数据排列。 这个位置我们称为"峰顶"
题目分析:该题就是要求我们找出峰顶的下标。
解法一:暴力枚举
我们可以遍历这个山脉数组,直到一个位置且该位置之后的数据第一次小于这个位置上的数据,这个位置就是峰顶,直接返回即可。
解法二:二份查找
二段性分析:
所以,当arr[mid]>arr[mid-1]时,left=mid,当arr[mid]<arr[mid-1]时,right=mid-1。
代码实现:
java
public int peakIndexInMountainArray(int[] arr) {
int left=0,right=arr.length-1;
while(left<right){
int mid=left+(right-left+1)/2;
if(arr[mid]>arr[mid-1]) left=mid;
else right=mid-1;
}
return left;
}
7.寻找峰值
算法原理:
解法一:暴力枚举
从数组中的第一个数据开始遍历,我们分3中情况讨论,当数组一直是下降的时候,我们就放回数组中第一个数据的位置,如果数组中出现递增右递减,就返回第一次出现递减的时候的前一个位置,如果数组一直是上升的时候,我们就返回n-1.
解法二:二分查找
二段性分析:
此时,如果我们找的点是i,那么此时会出现两种情况。
当nums[i]>nums[i+1]的时候,那么i的左边一定有一个峰值,但是i的右边不一定有峰值。
当nums[i]<nums[i+1]的时候,那么i的右边一定有一个峰值,因为这是一个升序,所以此时i也有可能是峰值,所以在后面些代码的时候,我们要让right=mid,而不是right=mid-1.
有了这个二段性,就可以使用二分查找。
当nums[mid]>nums[mid+1]时,left=mid+1,当nums[mid]<nums[mid+1]时,right=mid
代码实现:
java
public int findPeakElement(int[] nums) {
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>nums[mid+1]) right=mid;
else left=mid+1;
}
return left;
}
8.寻找旋转排序数组中的最小值
题目链接:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
题目解析:
1.数组旋转一次就代表将数组的尾元素放在数组的首位
2.在一个升序数组经过多次旋转后,找到数组中的最小值
3.数组中元素个不相同。
解法:二分查找
当数组经过多次旋转后,会是下面这种状态或者一个完全递增的状态。
解题思路:
数组经过旋转后,我们就可以视为按照上图分为两段,两段都是完全递增的,此时,我们就可以使用nums[nums.length-1]或者nums[0]位参考物。
第一种,以nums[nums.length-1]为参考物
此时,nums[mid]就会出现两种情况
当nums[mid]>参考物时,就是在数组段1,所以让left=mid+1,当nums[mid]<=参考物时,就是在数组段2,就让right=mid。
代码实现:
java
public int findMin(int[] nums) {
int left=0,right=nums.length-1;
int x=nums[right];
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>x) left=mid+1;
else right=mid;
}
return nums[left];
}
以nums[0]为参考物时,当nums[mid]大于等于参考物时,mid就是在数组段1,且在这数组段1中就不会有目标值了,就让left=mid+1,当nums[mid]小于参考物时,mid就是在数组段2,就让right=mid。
代码实现:
java
public int findMin(int[] nums) {
int left=0,right=nums.length-1;
int x=nums[0];
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>=x) left=mid+1;
else right=mid;
}
if(nums[left]>nums[0]) left=0;//处理数组完全递增的情况
return nums[left];
}