说到二分查找可能大家会想到二分查找的基本模板类似这样的:
/**
* 二分查找的基本实现
* 作用:对于一个已经从小到大排好序的数组找出目标元素的索引值
* 时间复杂度为O(logn)
* @param nums
* @param target
* @return
*/
public static int BinarySearch(int[] nums, int target){
if(nums.length == -1)
return -1;
int left = 0;
int mid = 0;
int right = nums.length-1;
while (left<=right){
mid = left+(right-left)/2;
if(nums[mid]==target)
return mid;
else if(nums[mid]<target)
left = mid+1;
else
right = mid-1;
}
return -1;
}
然而这篇博文我想和大家一起看看二分查找在一些题目里不一样的用法
题目一:
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n
个版本 [1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version)
接口来判断版本号 version
是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
这题一看到就能让人联想到要用二分,可是这是差第一个错误版本,不像以前查出某一个数的索引这该怎么办呢,下面给出一位博主对于该题使用二分查找的思路
思路部分如下:
因为一个版本是错误,其后面的所有版本都是错误的,所以我们可以用二分搜索,当取中点时,如果中点是错误版本,说明后面都是错误的,那第一个错误版本肯定在前面。如果中点不是错误版本,说明第一个错误版本肯定在后面。
这里直接使用min <= max的二分模板,因为我们其实要找的是好和坏的分界点,即这个点既不是好也不是坏,所以是找不到的,按照模板的特点,最后退出循环时,max指向小于目标的点,min指向大于目标的点,这里第一个坏的version较大,所以返回min
怎么样是不是感觉我去二分查找还能这么用有点意思吧
附上博文链接里大佬的java题解:
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int min = 1, max = n, mid = 0;
while(min <= max){
mid = min + (max - min) / 2;
if(isBadVersion(mid)){
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
}
题目二
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
- 不能更改原数组(假设数组是只读的)。
- 只能使用额外的 O(1) 的空间。
- 时间复杂度小于 O(n2) 。
- 数组中只有一个重复的数字,但它可能不止重复出现一次。
这题我一拿到是蒙蔽的,空间复杂度和时间复杂度都受到了限制,对于我这种业余算法选手是真的有点难受了,搜了下题解,又是二分查找,其实我该想到的,O(1)的空间,小于O(n2)的时间,这些都算是LeetCode对于使用二分查找的提示了
题解作者的思路:
这道题给了我们n+1个数,所有的数都在[1, n]区域内,首先让我们证明必定会有一个重复数,这不禁让我想起了小学华罗庚奥数中的抽屉原理(又叫鸽巢原理), 即如果有十个苹果放到九个抽屉里,如果苹果全在抽屉里,则至少有一个抽屉里有两个苹果,这里就不证明了,直接来做题吧。题目要求我们不能改变原数组,即不能给原数组排序,又不能用多余空间,那么哈希表神马的也就不用考虑了,又说时间小于O(n2),也就不能用brute force的方法,那我们也就只能考虑用二分搜索法了,我们在区别[1, n]中搜索,首先求出中点mid,然后遍历整个数组,统计所有小于等于mid的数的个数,如果个数大于mid,则说明重复值在[mid+1, n]之间,反之,重复值应在[1, mid-1]之间,然后依次类推,直到搜索完成,此时的low就是我们要求的重复值.
好吧小菜鸡拜倒在大佬的想法下了,行云流水,理所当然,题解的博主是用C++写的,下面附上我改的过了的java版代码:
class Solution {
public int findDuplicate(int[] nums) {
int low = 1, high = nums.length - 1;
while (low < high) {
int mid = low + (high - low) /2;
int cnt = 0;
for (int a : nums) {
if (a <= mid) ++cnt;
}
if (cnt <= mid) low = mid + 1;
else high = mid;
}
return low;
}
}
上面这两道题加深了我对二分查找的认识,它确实不只是查找那么简单,用好了的话可以很神奇的解决一些问题.
因为发现自己在和朋友说题时还没理解透这两个题目和其对于二分查找的运用所以写下这篇博客备查.