以前我所了解的二分查找就是最简单的在排序数组中找某个数的索引,而刷LeetCode时发现其实还有很多变形。例如:
其实以上这些问题都可以通过一遍遍历O(n)的时间复杂度解决,但题目中要求时间复杂度为O(logn),这个时候就需要使用到二分查找。
public static int biSearch(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int numsLen = nums.length;
int left = 0, right = numsLen - 1;
while (left <= right) {
int mid = (left + right) >> 1;
if (target > nums[mid]) {
left = mid + 1;
} else if (target < nums[mid]) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
其中[left, right]表示搜索的范围,范围内既包含left也包含right。以前写排序的时候,范围内至少有两个数才有必要排序,因此退出条件为while(left < right),现在搜索的话,只要有一个数就可以搜索,因此退出条件为while(left <= right)。
今天看leetcode评论区的时候,发现好多代码的退出条件都是while(left < right),才发现也可以认为搜索的范围是[left, right),也就是包含左边不包含右边,这时至少有一个数可以搜索的话条件就变成了left < right,退出时left == right或left > right表示已经没有数可以搜索。
public static int biSearch(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int numsLen = nums.length;
int left = 0, right = numsLen; // right是区别1
while (left < right) { // 条件是区别2
int mid = (left + right) >> 1;
if (target > nums[mid]) {
left = mid + 1;
} else if (target < nums[mid]) {
right = mid; // 搜索范围不包含right,这里是区别3
} else {
return mid;
}
}
return -1;
}
如果有序序列中存在多个某个值,那么这个值有一个左边界、有一个右边界,查找边界的情况其实和普通二分搜索是类似的,只不过查找到以后不要立即返回索引,而是继续收缩区间查找,查找左边界就继续往左收缩,查找右边界就继续往右收缩。
如上所述,对普通的二分搜索稍作修改即可。
// 1.右边界是nums.length-1,搜索范围包含right
public static int leftBoundary1(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target > nums[mid]) {
left = mid + 1;
} else if (target < nums[mid]) {
right = mid - 1;
} else {
right = mid - 1;
}
}
if (left == nums.length) return -1;
return nums[left] == target ? left : -1;
}
// 2.右边界是nums.length,搜索范围不包含right
public static int leftBoundary2(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
}
if (left == nums.length) return -1;
return nums[left] == target ? left : -1;
}
// 1.右边界是nums.length-1,搜索范围包含right
public static int rightBoundary1(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target > nums[mid]) {
left = mid + 1;
} else if (target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (left == 0) return -1;
return nums[left - 1] == target ? left - 1 : -1;
}
// 2.右边界是nums.length,搜索范围不包含right
public static int rightBoundary2(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
left = mid + 1;
}
}
if (left == 0) return -1;
return nums[left - 1] == target ? left - 1 : -1;
}
有了以上两个方法,直接调用即可获得某个数的范围
public int[] searchRange(int[] nums, int target) {
return new int[]{leftBoundary2(nums, target), rightBoundary2(nums, target)};
}
PS:
以上内容基本都来自评论区labuladong的题解,他的同名公众号也是labuladong,各种常用算法解释的都特别清楚,感谢!!!