关于二分的细节二分的细节你真的懂了吗
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。
暴力解法
直接使用数组的遍历,从第一个元素开始比对,直到找到和target值相等的元素下标。
public int binarySearch(int[] nums,int target){
if(nums==null||target<nums[0]||target>nums[nums.length-1]){
return -1;
}
for (int i = 0; i < nums.length; i++) {
if(nums[i]==target){
return i;
}
}
return -1;
}
- 时间复杂度: O(n);
- 空间复杂度: O(1);
思路和解题
-
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
-
然后就是边界的问题了,到底什么二分查询才跳出while的问题:while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?
-
我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
-
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
代码:
public int binarySearch(int[] nums,int target){
if(nums==null||target<nums[0]||target>nums[nums.length-1]){
return -1;
}
int left = 0;
int mid;
int right = nums.length-1;
while (left<=right){
mid = left+((right-left)>>1);
if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid+1;
}else if(nums[mid]==target){
return mid;
}
}
return -1;
}
以前看到过的一个发散题目
- 数组中存在重复数据,如果给出的target是多个,返回数组中第一个出现该元素的下标。
- 那么我想的是如果是重复,那么我通过二分查找得到的元素可能是第一个,也可能是重复元素中的某一个,那么这个时候再拿前一个元素和当前元素比对是否相等。
- 边界值是找到的下标是第0位元素就肯定也不用判断是否和前一个元素相等了,因为前面没有元素了。
代码:
public int binarySearch1(int[] nums,int target){
if(nums==null||target<nums[0]||target>nums[nums.length-1]){
return -1;
}
int left = 0;
int mid;
int right = nums.length-1;
while (left<=right){
mid = left+((right-left)>>1);
if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid+1;
}else if(nums[mid]==target){
int prev = mid;
while (mid!=0&&nums[prev]==nums[--prev]){}
// 因为上面减了一次再跳出来,所以返回的时候需要+1
return ++prev;
}
}
return -1;
}
- 数组中存在重复数据,如果给出的target是多个,返回数组中最后一个出现该元素的下标。
- 那么我想的是如果是重复,那么我通过二分查找得到的元素可能是第一个,也可能是重复元素中的某一个,那么这个时候再拿后一个元素和当前元素比较是否相等。
- 边界值是找到的下标是第最后一位元素就肯定也不用判断是否和最后一个元素等了,因为前面没有元素了。
代码:
public int binarySearch1(int[] nums,int target){
if(nums==null||target<nums[0]||target>nums[nums.length-1]){
return -1;
}
int left = 0;
int mid;
int right = nums.length-1;
while (left<=right){
mid = left+((right-left)>>1);
if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid+1;
}else if(nums[mid]==target){
int prev = mid;
while (mid!=nums.length-1&&nums[prev]==nums[++prev]){ }
// 因为上面加了一次再跳出来,所以返回的时候需要-1
return --prev;
}
}
return -1;
}
总结
其实主要就是每次二分的时候要对区间理解清楚,区间范围搞清楚后,才知道每次砍半的区间,下次二分的区间范围;然后就是区间的边界值,关注边界值,让逻辑不出现临界值的问题。