目录
1.从算法角度看二分
(1)问题
考虑如下问题:
给你一个有序的含有1000个元素的数组arr[1000],求待查找元素target的下标idx。
直观地,用暴力法解决上述问题,用一个for循环遍历整个数组,用if判断来遍历的元素是否等于target。考虑最坏情况,我们需要遍历整个数组,时间复杂度是O(n)的。
(2)二段性
如何进行优化呢?
在上述暴力做法中,我们没有利用[数组有序]这一条件,如果我们取数组中某个元素x,通过比较这个元素x和target的大小,如果x>target,我们可以立刻排除x及右边的所有元素,这些元素都是大于target的,同理x<target,我们可以排除x及左边的元素,它们都小于target。即我们可以发现数组中有二段性,一部分是大于target的一部分是小于target的,因此我们要找到满足性质转换的那个点,也就是target。
当然「二分」不是单纯指从有序数组中快速找某个数,这只是「二分」的一个应用。
「二分」的本质是两段性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。
2.三种区间
二分查找的难点在于对于区间细节的处理,常见的写法有左闭右闭,左闭右开,左开右闭三种。
对查找有序数组中的某个元素的问题,我们在利用二分法进行判断的时候,需要检索区间,而初始待检索的区间有几种写法:[left,right],[left,right),(left,right],以上区间写法取决于我们初始化left和right时是如何赋值的。
(1)[left,right]左闭右闭
当我们进行如下初始化:
//len为数组长度
int left=0,right=len-1;
表示我们所用的是左闭右闭的写法, 初始化查找的区间是整个数组,并且左右端点都会取到,所以后面的代码应注意:
- 循环条件要使用while(left <= right),试想如果数组只有一个元素时,初始时left==right,为了保证能够进入循环,需要添加=
- if(arr[mid] > target) , right 要赋值为 mid - 1, 因为已经判断当前的 arr[middle] 一定不是 target ,需要把这个 mid 位置上面的数字丢弃,那么接下来需要查找范围就是[left, middle - 1]
- 求mid时,如果直接利用(right+left)/2来计算,如果left和rigth较大时,求和可能会溢出,所以改用left+(right-left)/2,后半部分做减法,防止溢出。
int search(int[] arr,int target){
int len=arr.length;
int left=0,right=len-1;
while(left<=right){
int mid=left+(right-left)/2;
if(arr[mid]==target) return mid;//找到了直接返回
else if(arr[mid]>target) right=mid-1;//说明target在mid的左边
else left=mid+1;//说明target在mid的右边
}
return -1;//未找到
}
(2)[left,right)左闭右开
当我们进行如下初始化:
//len为数组长度
int left=0,right=len;
表示我们所用的是左闭右开的写法,对比上述左闭右闭的写法,初始时由于right无法取到,我们查找的区间是[left,right),根据区间的定义,后续条件控制应该如下:
- 循环条件使用while (left < right),
- if (arr[middle] > target) right = middle;因为当前的 arr[middle] 是大于 target 的,不符合条件,不能取到 middle,并且区间的定义是 [left, right),刚好区间上的定义就取不到 right, 所以 right 赋值为 middle
int search(int[] arr,int target){
int len=arr.length;
int left=0,right=len;
while(left<right){
int mid=left+(right-left)/2;
if(arr[mid]==target) return mid;//找到了直接返回
else if(arr[mid]>target) right=mid;//说明target在mid的左边
else left=mid+1;//说明target在mid的右边
}
return -1;//未找到
}
(3)(left,right]左开右闭
与上述左闭右开类似,这种写法用的比较少,写法如下:
int search(int[] arr,int target){
int len=arr.length;
int left=-1,right=len;
while(left<right){
int mid=left+(right-left+1)/2;//防止溢出
if(arr[mid]==target) return mid;//找到了直接返回
else if(arr[mid]>target) right=mid-1;//说明target在mid的左边
else left=mid;//说明target在mid的右边
}
return -1;//未找到
}
3.二分模板
在数组arr[]中寻找第一个大于等于target的位置。
左闭右闭写法
int search(int[] arr,int target){
int len=arr.length;
int left=0,right=len-1;
while(left<=right){
int mid=left+(right-left)/2;
if(arr[mid]>=target)
//每次更新之后,arr[right+1]>=target一定成立
//也就是说最后的right+1始终指向大于等于target的位置
right=mid-1;
//同理。left-1始终指向小于target的位置
else left=mid+1;
}
//最后循环结束后,right+1指向第一个大于等于target的位置,而循环结束时left恰好在right右边
//即left=right+1,返回left即可
return left;
}
左闭右开写法
int search(int[] arr,int target){
int len=arr.length;
int left=0,right=len;
while(left<right){
int mid=left+(right-left)/2;
if(arr[mid]>=target)
//每次更新之后,arr[right+1]>=target一定成立
//也就是说最后的right始终指向大于等于target的位置
right=mid;
//同理。left-1始终指向小于target的位置
else left=mid+1;
}
//最后循环结束后,right指向第一个大于等于target的位置,而循环结束时left恰好等于right
//即left=right,返回left和right都可,这里返回left
return left;
}
拓展:
1.在数组arr[]中寻找第一个大于target的元素位置:大于target的第一个位置即第一个大于等于target+1元素的位置,调用search(arr,target+1)
2..在数组arr[]中寻找第一个小于target的元素位置:即第一个大于等于target元素的前一个位置,调用search(arr,target)-1
3.在数组arr[]中寻找第一个小于等于target的元素位置:即第一个大于target元素的前一个位置,调用search(arr,target+1)-1
以对于有序数组而言,下面列举一些可以套用模板的情况:
在数组中找到目标值target,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
解决方案:调用search(arr,target)即可
2. leetcode 34.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
查找开始位置,start=search(arr,target),这里得到了第一个大于等于target的位置。我们必须确保start不越界且start对应的元素正好是target,即nums[start]==target && start<=len-1;查找结束位置,如果start存在,那么end一定存在,找第一个大于target元素的前一个位置即可,end=search(arr,target+1)-1。
类似的有:统计目标成绩的出现次数
所给数组具有二段性,利用左闭右闭写法,判断目标值是否处在某个单调递增区间,不断二分范围。
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if(len == 0) return -1;
int left = 0, right = len - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target) return mid;
// 右边有序
if(nums[mid] < nums[right]){
// 目标值在右边
if(target > nums[mid] && target <= nums[right]){
left = mid + 1;
// 目标值在左边
}else{
right = mid - 1;
}
// 左边有序
}else{
// 目标值在左边
if(target >= nums[left] && target < nums[mid]){
right = mid - 1;
// 目标值在右边
}else{
left = mid + 1;
}
}
}
return -1;
}
}