二分搜索看起来简单,但在实际运用中却变化万千,容易出错。博主之前也是看到一遍就去学习查找,然而并没有进行总结,在边界条件,返回值等总是模糊,后来想着还是彻底把它搞清楚,这样一劳永逸,同时也能够灵活运用。这是这个专题的初衷。
二分搜索或者说二分查找应该注意的几个点:
-
区间的开闭 左闭右闭 还是左闭右开
-
循环条件 left<right 还是 left<=right 实际和第一点是呼应的
-
更新条件 左右区间的更新,同样要对应开闭原则,维持区间的循环不变性
-
返回值 返回left right 还是需要再判断
-
搜索条件 大于 等于 小于 大于等于 小于等于
对此进行了总结并给出了实例。首先明确一个方向问题,当寻找一个第一个大于等于t的数时,我们会从左向右进行搜索,如果开始mid就符合条件,即arr[mid]>=t 那么更新条件就是往这个方向的反方向,即回退,right应该更新,如果是闭区间 right=mid-1(不会错过这个数,如果mid就是第一个大于等于的数,最终循环退出会有left=right+1,此时right再去搜索时right的位置不会变,left会更新,并且left>right退出,回到原先mid的位置,返回right即可,如果arr[mid]<t,说明[0,mid]都不符合条件,向右搜索,left=mid+1
/**
* 总结:
* 1)开闭区间 统一闭区间 [left,right] left=0;right=arr.length-1; 如果用开区间[left=0,right=n) 那么下次递推的时候也要如此,类似数学归纳法
* 2)while循环退出条件 while(left<=right) 退出时有left>right left=right+1;
* 3)条件 大于等于 小于等于 等于 第一个 最后一个 方向
* 首先确定方向原数组默认从小到大, 给出条件,如第一个大于等于t的数,则方向为从左到右,循环语句如下
* if(arr[mid]>=t) //命中条件,这时再结合方向和所给出的条件做判断
* {
* //第一个大于等于 方向为从左到右,我们此时命中的条件 并不能保证是第一个,因此回退,往反方向(从右向左,去寻找第一个)走,即right指针回退
* right=mid-1;
* }else{
* //没有触发条件,需要进一步收缩空间
* left=mid+1;
* }
* 4)循环不变量 我们要找的数,如果存在则始终是在[left,right]这个闭区间中
* 5)循环退出返回
* 循环退出要么是在区间内找到了这个数,要么是整个数组中就不存在这个数,需判断
* 注意方向和上下界溢出的问题 方向指的是从哪个方向找到满足条件的第一个数的方向
* 如果是从左往右(大于等于)那么返回left,这时需要注意上界,即left==arr.length,这种情形,right一直向左逼近
* 如果是从右向左(小于等于)那个返回right,这时需要注意下界,即right==0; left会向右逼近,以至于越界
*
* 另外对于找只是等于的问题 可以转换为大于等于 或者小于等于, 只是在返回的时候需要注意,所找的数是否在数组中
* 对于大于等于 return (left==arr.length || arr[left]==t)?-1:left;
* 对于小于等于 return (right==0 || arr[right]==t)?-1:right;
*
* 6)给出条件 就看arr[mid]是否触发条件,再移动方向,不外乎两种 left=mid+1,right=mid-1,
* 看具体往哪个方向走,触发条件往反向走,
* 未触发继续将区间进行收敛
*
*
* !初始区间的表示法在选定后, 两条边界更新语句应该与之对应,否则可能会出现死循环
*其实死循环的导致,是因为区间的左右边界条件全部都写错了(也可以说是前面第一种错误情况的一种特例,第一种错误情况是只有 左边界或者右边界写错)
*
*
* 对于开区间 大于等于返回left 小于等于返回right-1 此时退出条件有left==right
* 循环条件while (left < right)
* 更新条件right = mid;
* left = mid + 1;
*
*/
下面给出各种实例,包括区间不同,搜索方向不同等。
/**
* 查找数组中与target相等的数,返回索引,没有找到返回-1
* @param nums
* @return
*/
public static int binaryFind(int[] nums, int target) {
int left = 0, right = nums.length - 1;
//取闭区间,在[left,right]中去寻找这个数,区间作为一个循环不变量
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
/****************************-----------------------------------------------************************/
/*
0,1,1,2,2,2,4,4,5,6 t=2
left=0 mid= 5 right= 10 命中2,arr[mid]<=t 则left=mid+1;可能此时left指向的数已经大于t了
left=6 mid= 8 right= 10
left=6 mid= 7 right= 8
left=6 mid= 6 right= 7
left=6
right=6
*/
//使用开区间查找第一个大于num的数
public static int findFirstGNumOpen(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
System.out.println("left=" + left + " mid= " + mid + " right= " + right);
if (arr[mid] > target) {//触发条件
right = mid;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (left == arr.length) ? -1 : left;
}
/**
left=0 mid= 5 right= 10
left=0 mid= 2 right= 5
left=3 mid= 4 right= 5
left=3 mid= 3 right= 4 [3,4)
left=3
right=3
二分查找第一个大于等于target=2返回下标3
*/
public static int findFirstGENumOpen(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
System.out.println("left=" + left + " mid= " + mid + " right= " + right);
if (arr[mid] >= target) {//触发条件 与上面相比等号的位置挪上来了,右边移动了
right = mid;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (left == arr.length) ? -1 : left;
}
/****************************-----------------------------------------------************************/
//使用开区间找第一个小于t的数
public static int findFirstLNumOpen(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] < target) {//触发条件
left = mid + 1;
} else {
right = mid;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (right == 0) ? -1 : right - 1;
}
public static int findFirstLENumOpen(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] <= target) {//触发条件
left = mid + 1;
} else {
right = mid;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (right == 0) ? -1 : right - 1;
}
/****************************-----------------------------------------------************************/
public static int findFirstEqualNumOpen(int[] arr, int t) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= t) {//找到满足flag的数,
right = mid;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断,超过右边界
return (left == arr.length || arr[left] != t) ? -1 : left;
}
//从右向左第一个小于等于t的数,触发条件,回退,向右,left=mid+1;没有触发,继续前进,向左,right=mid-1;
public static int findLastEqualNumOpen(int[] arr, int t) {
int left = 0, right = arr.length;
while (left <right) {
int mid = left + (right - left) / 2;
System.out.println("left=" + left + " mid= " + mid + " right= " + right);
//相当于从左向右找第一个小于等于t的数, 如果全部比t小不会溢出,返回最后一个元素下标,如果全部比t大,right位置在-1处
//如果数组不存在那个数,再做一步判断,right为指向第一个小于等于t的数判断它是否和t相等
if (arr[mid] <= t) {//找到满足flag的数,
left = mid + 1;
} else {
right = mid;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断
return (right == 0 || arr[right-1] != t) ? -1 : right-1;//注意是right-1 ******************
}
/****************************-----------------------------------------------************************/
//二分查找 第一个大于num的数,没找到返回-1;
public static int findFirstGreatNum(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > target) {//触发条件
right = mid - 1;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (left == arr.length) ? -1 : left;
}
//二分查找 第一个大于等于num的数,没找到返回-1;
public static int findFirstGqNum(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
return (left == arr.length) ? -1 : left;
}
public static int findFirstLessNum(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < t) {
left = mid + 1;
} else {
right = mid - 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断,求第一个小于t的数,t如果比最小的数即第0个数小,就返回-1
return (right < 0) ? -1 : right;
}
/**
* 找第一个小于等于t的数
*
* @param arr
* @param t
* @return
*/
public static int findFirstLessEqualNum(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] <= t) {//找到满足flag的数,
left = mid + 1;
} else {
right = mid - 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断,求第一个小于t的数,t如果比最小的数即第0个数小,就返回-1
return (right < 0) ? -1 : right;
}
/**
* 返回第一个等于t的数
* 从左向右查找第一个大于等于t的数,因为是查找从左边起第一个大于等于t的数,因此,right会不断的向左边移动,最后right和left都
* 指向第一个大于等于t的数时,right还会再移动,left代表返回的结果
*
* @param arr
* @param t
* @return
*/
public static int findFirstEqualNum(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= t) {//找到满足flag的数,
right = mid - 1;
} else {
left = mid + 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断,超过右边界
return (left == arr.length || arr[left] != t) ? -1 : left;
}
//从右向左第一个小于等于t的数,触发条件,回退,向右,left=mid+1;没有触发,继续前进,向左,right=mid-1;
public static int findLastEqualNum(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
//相当于从左向右找第一个小于等于t的数, 如果全部比t小不会溢出,返回最后一个元素下标,如果全部比t大,right位置在-1处
//如果数组不存在那个数,再做一步判断,right为指向第一个小于等于t的数判断它是否和t相等
if (arr[mid] <= t) {//找到满足flag的数,
left = mid + 1;
} else {
right = mid - 1;
}
}
System.out.println("left=" + left);
System.out.println("right=" + right);
//返回值判断
return (right < 0 || arr[right] != t) ? -1 : right;
}