普通二分查找
二分法中left + (right - left) /2 就和 (left + right) / 2 的结果相同,但是有效防⽌了 left 和right 太⼤直接相加导致溢出。
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
1、为什么 while 循环的条件中是 <=,⽽不是 <?
答:因为初始化 right 的赋值是 nums.length - 1 ,即最后⼀个元素的索引,⽽不是 nums.length 。
这⼆者可能出现在不同功能的⼆分查找中,区别是:前者相当于两端都闭区间 [left, right] ,后者相当于左闭右开区间 [left, right) ,因为索引大小为 nums.length 是越界的。
我们这个算法中使⽤的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进⾏搜索的区间。
什么时候应该停⽌搜索呢?当然,找到了⽬标值的时候可以终⽌:
if(nums[mid] == target)
return mid;
但如果没找到,就需要 while 循环终⽌,然后返回 -1。那 while 循环什么时候应该终⽌?搜索区间为空的时候应该终⽌,意味着你没得找了,就等于没找到嘛。
while(left <= right) 的终⽌条件是 left == right + 1 ,写成区间的形式就是 [right + 1, right] ,或者带个具体的数字进去 [3, 2] ,可⻅这时候区间为空,因为没有数字既⼤于等于 3 ⼜⼩于等于 2 的吧。所以这时候while 循环终⽌是正确的,直接返回 -1 即可。
while(left < right) 的终⽌条件是 left == right ,写成区间的形式就是[left, right] ,或者带个具体的数字进去 [2, 2] ,这时候区间⾮空,还有⼀个数 2,但此时 while 循环终⽌了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。
此算法有什么缺陷?
答:⾄此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。
⽐如说给你有序数组 nums = [1,2,2,2,3] , target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是⽆法处理的。
寻找左侧边界的⼆分搜索
int left_bound(int[]nums,int target){
if(nums.length == 0) return -1;
int left = 0;
int right = nums.length;//注意这里
while(left < right){
int mid = (left + right)/2;
if(nums[mid] == target) right = mid;
else if(nums[mid] < target ) left = mid+1;
else if(nums[mid] > target) right = mid;
}
return nums[left] == target ? left : -1;
}
另一种写法:
int left_bound(int[]nums,int target){
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
if (nums[mid] == target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
// 如果退出循环了,也就是left = right + 1
// 判断下越界了不
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
1、为什么 while 中是 < ⽽不是 <= ?
答:⽤相同的⽅法分析,因为 right = nums.length ⽽不是 nums.length -1 。因此每次循环的「搜索区间」是 [left, right) 左闭右开。
while(left < right) 终⽌的条件是 left == right ,此时搜索区间 [left,left) 为空,所以可以正确终⽌。
PS:这⾥先要说⼀个搜索左右边界和上⾯这个算法的⼀个区别,也是很多读者问的:刚才的 right 不是 nums.length - 1 吗,为啥这⾥⾮要写成nums.length 使得「搜索区间」变成左闭右开呢?
因为对于搜索左右侧边界的⼆分查找,这种写法⽐较普遍,我就拿这种写法举例了,保证你以后遇到这类代码可以理解。你⾮要⽤两端都闭的写法反⽽更简单,我会在后⾯写相关的代码,把三种⼆分搜索都⽤⼀种两端都闭的写法统⼀起来,你耐⼼往后看就⾏了
2.为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target)
right = mid;
可⻅,找到 target 时不要⽴即返回,⽽是缩⼩「搜索区间」的上界right ,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的⽬的。
查找第一个大于等于给定值的元素
public static int search(int[] nums, int val) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
if (nums[mid] < val) {
low = mid + 1;
} else {
// 如果nums[mid]是第一个元素,或者nums[mid-1]小于val
// 说明nums[mid]就是第一个大于等于给定值的元素
if (mid == 0 || nums[mid - 1] < val) {
return mid;
}
high = mid - 1;
}
}
return -1;
}
或者这样:
public static int search(int[] nums, int val) {
int low = 0, high = nums.length - 1,index=0;
while (low <= high) {
int mid = (low + high) >>> 1;
if (nums[mid] < val) {
low = mid + 1;
} else {
// 如果nums[mid]是第一个元素,或者nums[mid-1]小于val
// 说明nums[mid]就是第一个大于等于给定值的元素
index=mid;
high = mid - 1;
}
}
return index;
}
查找最后一个小于等于给定值的元素
public class Code06_BSNearRight {
//在arr上,找满足<=value的最右位置 O(logn)
public static int nearestIndex(int[] arr,int value){
int L=0;
int R=arr.length-1;
int index = -1;//记录最左边的对号
while(L<=R){
int mid=L+((R-L)>>1);
//发现左侧有小于目标值的,抛弃左边,左边下标L=mid+1
if(arr[mid]<=value){
index=mid;
L=mid+1;
}else {
R=mid-1;
}
}
return index;
}
找到小于等于目标值的最大位置
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
if (nums[left] > target) {
return -1;
}
while (left < right) {
int mid = (right - left + 1) / 2 + left;
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
return left;
}