几种二分查找的变形问题。
🧨🧨“十个二分九个错”。注意:终止条件、区间上下界更新方法、返回值选择。🧨🧨
下述变形问题的前提是数据均以从小到大排序。
变形一:查找第一个值等于给定值的元素🎈
二分查找最简单的一种即有序数据集合中不存在重复的数据,在其中查找值等于某个给定值的数据。 将这个问题修改为:有序数据集合中存在重复的数据,找到第一个值等于给定值的数据,如下有序数组,其中,a[5],a[6],a[7]的值都等于 8,是重复的数据,要查找第一个等于 8 的数据,也就是下标是 5 的元素。—— 最简单的二分查找需要进行修改
// 方法一
/*
目标:找到第一个元素为value的值
具体的实现逻辑:当 mid == value 时,高位还会往左移继续去找第一个,那么势必会带来的问题是即使找到了第一个值为value的元素,
高位还是会往左移动,但是下一次判断的时候数组中已经没有值为value的值了,这时候代码会继续循环一直到低位加一后大于高位,这时
候会跳出循环,而此时的低位正好是高位上一个经过的值为value的第一个元素,最后的 >n 判断是为了处理第一次就找到value的边界问
题。
二分查找在最差情况下的时间复杂度为logn,这段代码因为会一直循环直到low>high,所以它的时间复杂度稳定是logn。
边界控制需要注意。
*/
public int binarySearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] >= value) {
high = mid - 1;
} else {
low = mid + 1;
}
}
if (low < n && a[low] == value) return low;
else return -1;
}
// 方法二
/*
求解中间位置的值不对两个位置值相加防止数据溢出,同时使用位运算而不是用除法来加快处理速度。
*/
public int binarySearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == 0) || (a[mid - 1] != value)) return mid;
else high = mid - 1;
}
}
return -1;
}
- a[mid]跟要查找的 value 的大小关系有三种情况:大于、小于、等于。
- 对于 a[mid] > value 的情况,需要更新 high= mid-1;
- 对于 a[mid]<value 的情况,需要更新 low=mid+1。
- 当 a[mid] = value 时进行如下操作:
- 如果查找的是任意一个值等于给定值的元素,当 a[mid]等于要查找的值时,a[mid]就是要找的元素。
- 如果求解的是第一个值等于给定值的元素,当 a[mid]等于要查找的值时,需要确认当前位置是不是第一个值等于给定值的元素。
- 如果 mid == 0,该元素已经是数组的第一个元素,则肯定是要找的结果;
- 如果 mid != 0,但 a[mid] 的前一个元素 a[mid-1] != value,则说明 a[mid] 是要找的第一个值等于给定值的元素。
- 如果 a[mid] 前面的一个元素 a[mid-1] == value,那说明此时的 a[mid] 不是要查找的第一个值等于给定值的元素。则更新 high=mid-1,因为要找的元素会出现在 [low, mid-1] 之间。
变形二:查找最后一个值等于给定值的元素🎈
public int binarySearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1); // 求中值
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
/*
如果 a[mid] 这个元素已经是数组中的最后一个元素了,那它肯定是要找的结果;
如果 a[mid] 的后一个元素 a[mid+1] != value,说明 a[mid]就是要找的最后一个值等于给定值的元素。
如果a[mid]后面的一个元素 a[mid+1] == value,说明当前的这个 a[mid]并不是最后一个值等于给定值的元素,更新 low=mid+1,因为要找的元素肯定出现在[mid+1, high]之间。
*/
if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
else low = mid + 1;
}
}
return -1;
}
变形三:查找第一个大于等于给定值的元素🎈
在有序数组中,查找第一个大于等于给定值的元素。比如,数组序列为:3,4,6,7,10。如果查找第一个大于等于 5 的元素,那就是 6。
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
/*
对于 a[mid] < value,那要查找的值在 [mid+1, high] 之间,更新 low=mid+1。
对于 a[mid] >= value,要查看下这个 a[mid] 是不是 第一个值大于等于给定值的元素 。
如果 a[mid] 前面已经没有元素,或者 前面一个元素 < value,那 a[mid]就是要找的元素。
如果 a[mid-1] >= value,则要查找的元素在[low, mid-1]之间,更新high = mid-1。
*/
if (a[mid] >= value) {
if ((mid == 0) || (a[mid - 1] < value)) return mid;
else high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
变形四:查找最后一个小于等于给定值的元素🎈
在有序数组中,查找最后一个小于等于给定值的元素。比如,数组序列为:3,5,6,8,9,10。最后一个小于等于 7 的元素就是 6。
public int bsearch7(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else {
if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
else low = mid + 1;
}
}
return -1;
}
补充题目🧨🧨
- 由于数组已经排序,整个数组是单调递增的,可以利用二分法来加速查找的过程。
- 考虑target 开始和结束位置,要找的就是数组中「第一个等于 target 的位置」(记为 startIndex)和「第一个大于target 的位置减一」(记为 endIndex)。
- 二分查找中,startIndex 即为在数组中寻找第一个大于等于 target 的下标,寻找 endIndex 即为在数组中寻找第一个大于target 的下标,然后将下标减一。
- 两者的判断条件不同,为了代码的复用,定义 binarySearch(nums, target, isFirstIndex) 表示在 nums 数组中二分查找target 的位置,如果 isFirstIndex为 true,则查找第一个大于等于 target 的下标,否则查找第一个大于 target 的下标。
- 因为target 可能不存在数组中,需要重新校验得到的两个下标 startIndex 和 endIndex,查看是否符合条件,如果符合条件就返回 [startIndex, endIndex],不符合就返回 [−1,−1]。
class Solution {
public int[] searchRange(int[] nums, int target) {
// 要降低查找的复杂度,则选取二分查找的方法
// 要找到开始和结束的位置,使用布尔值控制是否是查找第一个出现的位置
int startIndex = binarySearch(nums, target, true);
int endIndex = binarySearch(nums, target, false) - 1;
// 判定数据是否合法
if((startIndex <= endIndex) && (endIndex < nums.length) && (nums[startIndex] == target) && (nums[endIndex] == target)){
return new int[] {startIndex, endIndex};
}
return new int[] {-1,-1};
}
// 对于&,&操作符两端的表达式都要执行。
// 对于&&, 假如说&&左端的表达式的值为false,那么&&右端的表达式就不会执行,此时已经能够判断整个表达式的结果为false,
// 这样做可以少执行一些语句,提高效率;只有当左端的表达式的为真时,才需要判断右端的表达式。
// 利用一个布尔型值来控制是否是查找第一个target出现的位置
public int binarySearch(int[] nums, int target, boolean isFirstIndex){
// 初始化起始,末尾和中间值的下标
int mid = 0;
int start = 0;
int end = nums.length - 1;
int indexAns = nums.length;
while(start <= end){
mid = start + ((end - start) >> 1);
if(nums[mid] > target || (isFirstIndex && (nums[mid] >= target))){
end = mid - 1;
indexAns = mid;
}else{
start = mid + 1;
}
}
return indexAns;
}
}