要想使用二分查找,必须满足一下条件:
二分查找看起来非常好理解,但是如果想把代码实现出来,还是有一定难度的。最大的难度在于边界的控制。
while(left < right)
还是 while(left <= right)
right = middle
呢,还是要right = middle - 1
呢要回答上述的问题,就必须搞明白二分查找中的两种方法,一种是左闭右闭区间,还有一种是左闭右开区间。我们说的这个区间,就是二分查找的区间范围。举个例子:有一个数组[1, 2, 3, 4],它的下标集合为[0, 1, 2, 3]。此时如果使用左闭右闭区间搜索,那么搜索的区间为[0, arr.length-1]。如果采用左闭右开的区间,那么搜索范围是[0, arr.length]。
这两种不同的搜索区间对应了二分查找的两种代码形式。
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
while (left <= right)
要使用 <= ,因为left == right
是有意义的,所以使用 <=if (nums[middle] > target)
right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1/**
* 搜索区间是左闭右闭的情况
* @param nums
* @param target
* @return
*/
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
// 这里使用的是左闭右闭区间
int left = 0, right = nums.length - 1;
// 对于左闭右闭区间 因为left == right 也是合法的,因此需要<=
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target) {
// mid已经不满足条件,需要排出掉
// 为什么要-1 而不是right = mid
// 如果right=mid,那么实际上下一次的搜索区间为[left, mid]
// 下一次搜索区间依然包含mid,而实际上mid已经不满足要求了
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:
while (left < right)
,这里使用 < ,因为left == right
在区间[left, right)是没有意义的
if (nums[middle] > target)
right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
/**
* 搜索区间是左闭右开的情况
* @param nums
* @param target
* @return
*/
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
// 这里使用的是左闭右开区间
int left = 0, right = nums.length;
// 对于左闭右闭区间 因为left == right 是不合法的,因此需要<
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] > target) {
// mid已经不满足条件,需要排出掉
// 为什么要right = mid 而不是right = mid -1
// 如果right=mid,那么实际上下一次的搜索区间为[left, mid)
// [left, mid)实际上已经把mid排出在外了
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
如何判断
right = mid
还是right = mid -1
?通俗的来说,因为我们通过
nums[middle] > target
条件已经判断了middle下标对应的元素实际上是不满足我们要查找的条件的,因此我们需要在下一次查找的区间中不能包含middle下标对应的元素,那么
- 如果我们的搜索区间是左闭右闭的,如果
mid = right
,则搜索区间为[left, mid],显然包含了mid下标对应的元素,不符合题意,所以mid = right - 1
- 如果我们的搜索区间是左闭右开的,如果
mid = right
,则搜索区间为[left, mid),搜索区间实际上已经把mid下标对应的元素排出在外了,满足题意
求第一个大于等于target的下标(等价于求第一个等于target的下标)
// 查找第一个大于等于target的下标 === 查找第一个等于target的下标
public int equalAndGreater(int[] array, int k) {
if (array == null || array.length == 0) return -1;
int left = 0, right = array.length - 1;
while (left <= right) {
int mid = (right + left) / 2;
if (array[mid] < k) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
求第一个大于target的下标(等价于求最后一个target元素下标+1)
// 查找第一个大于target的下标-1 === target的最后下标
public int greater(int[] array, int k) {
if (array == null || array.length == 0) return -1;
int left = 0, right = array.length - 1;
while (left <= right) {
int mid = (right + left) / 2;
if (array[mid] <= k) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
查找第一个小于target的值的下标
public int binarySearchOne() {
int[] array = {0,1,2,3,4,5,6};
int target = 3;
int left = 0, right = array.length-1;
while(left <= right) {
int mid = (left + right)/2;
//只修改了判断的条件,相当于将大于等于归为一类。
if(array[mid] >= target)
right = mid - 1;
else
left = mid + 1;
}
return right;
}
查找第一个小于等于target值的下标
public int binarySearchOne() {
int[] array = {0,1,2,3,4,5,6};
int target = 3;
int left = 0, right = array.length-1;
while(left <= right) {
int mid = (left + right)/2;
//只修改了判断的条件,相当于将大于等于归为一类。
if(array[mid] > target)
right = mid - 1;
else
left = mid + 1;
}
return right;
}
我们可以看出来,这几个代码实际修改的地方就只有array[mid]和target的判断条件。实际上,这个条件和要求的条件正好为补集。
- 求大于target的,就是array[mid]小于等于target
- 求大于等于target的,就是array[mid]小于target
- 求小于target的,就是array[mid]大于等于target
- 求小于等于target的,就是array[mid]大于target
相关博客资料https://blog.csdn.net/qq_29611345/article/details/103080287