二分查找
-
二分查找的也称为折半查找,由于每次都能够将查找区间缩小为原来一半,这种算法的时间复杂度为O(logN)。
-
计算中值的方法有两种
m = (low+ hight) / 2 m = low + (hight - low) / 2 推荐使用第二种,因为减法不会涉及到数据因为相加导致的溢出问题。
-
查找的返回值一般返回为low,但是实际情况需要从实际的题目出发。
-
可以多举例子,便于理解边界问题。
Question 1
x 的平方根(https://leetcode-cn.com/problems/sqrtx/)(长按复制到浏览器即可到leetcode对应的题目)
算法的逻辑在具体的代码注释里
/**
* 计算并返回 x 的平方根,其中 x 是非负整数。
* 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
*/
public class Question1 {
public int mySqrt(int x) {
if (x <= 1) {
return x;
}
// 二分
int low = 1;
int hight = x;
// 结合双指针
while (low <= hight) {
// 定义中间值 这样定义middle不会出现溢出问题
int middle = low + (hight - low) / 2;
// 计算是否能够直接算出开方
int sqrt = x / middle;
if (sqrt == middle) {
return middle;
} else if (middle > sqrt) {
// 大了 调整hight
hight = middle - 1;
} else {
// 小了 调整low
low = middle + 1;
}
}
return hight;
}
}
Question 2
寻找比目标字母大的最小字母(https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/)
/**
* 给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。
* 数组里字母的顺序是循环的。举个例子,如果目标字母target = 'z' 并且有序数组为 letters = ['a', 'b'],则答案返回 'a'。
* 关键词:有序、循环
*/
public class Question2 {
public char nextGreatestLetter(char[] letters, char target) {
// 定义头尾
int hight = letters.length-1;
int low = 0;
while (hight>=low){
int middle = low + (hight-low)/2;
if(target>=letters[middle]){
// middle小了
low = middle+1;
}else {
// middle大了
hight = middle-1;
}
}
// 最终得到的low是接近结果的(因为只有low大于hight的时候才会跳出循环)
return low<letters.length?letters[low]:letters[0];
}
}
Question3
** 有序数组中的单一元素(https://leetcode-cn.com/problems/single-element-in-a-sorted-array/)**
/**
* 给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
* 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
* <p>
* 因为要求时间复杂度和空间复杂度,所以不能够使用直接遍历的方法。
*/
public class Question3 {
public int singleNonDuplicate(int[] nums) {
int low = 0;
int hight = nums.length - 1;
while (hight > low) {
int middle = low + (hight - low) / 2;
// 判断中间的数是偶数还是奇数
if (middle % 2 == 0) {
// 偶数 说明middle前面有偶数个数字 此时判断middle 和 middle+1的关系
if (nums[middle] == nums[middle + 1]) {
// 说明middle前面数字都是出现两次的
low = middle + 2;
} else {
// 说明middle后面的数字都是正确的
hight = middle;
}
} else {
// 奇数 说明前后都只有奇数个数字 判断middle 和middle+1的关系
if (nums[middle] == nums[middle + 1]) {
// 说明middle之后的都是正确的
hight = middle - 1;
} else {
// 说明middle+1之后的是有问题的
low = middle + 1;
}
}
}
return nums[low];
}
}
Question4
第一个错误的版本(https://leetcode-cn.com/problems/first-bad-version/)
/**
* 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。
* 由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
* <p>
* 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
* <p>
* 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。
* 实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
*/
public class Question4 {
public int firstBadVersion(int n) {
int low = 1;
int hight = n;
while (hight > low) {
int middle = low + (hight - low) / 2;
// 二分查找 提高效率
if(isBadVersion(middle)){
// 版本出现错误 可能是在之前就出错了,所以需要往前回溯
hight = middle;
}else {
// 版本正确 往后推进
low = middle+1;
}
}
return low;
}
/**
* leetcode系统提供的方法
*
* @param version
* @return true:错误版本
* false:正确版本
*/
// boolean isBadVersion(int version) {
// return false;
// }
}
Question 5
寻找旋转排序数组中的最小值(https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)
/**
* 旋转数组的最小数字问题
* 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
* ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
* 请找出其中最小的元素。
* 你可以假设数组中不存在重复元素。
*/
public class Question5 {
public int findMin(int[] nums) {
int low = 0;
int hight = nums.length - 1;
while (hight > low) {
int middle = low + (hight - low) / 2;
// 旋转数组的特征就是:发生旋转的话,就会有一个旋转点
// 我们的目标就是找到这个旋转点
// (通过二分法比对middle和hight的值)
// hight>middle的话 说明后半部分没哟旋转点 反之同理!
if(nums[middle]<nums[hight]){
// 说明后半部分是正常的顺序的数组,没有旋转点,将目标缩小到前半部分
hight = middle;
}else {
// 说明后半部分数组有旋转点,继续调查后半部分的数组
low = middle+1;
}
}
return nums[low];
}
Question 6
**在排序数组中查找元素的第一个和最后一个位置(https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) **
/**
* 题目描述:
* 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
* 你的算法时间复杂度必须是 O(log n) 级别。
* 如果数组中不存在目标值,返回 [-1, -1]。
* <p>
* 关键词:升序数组、时间复杂度要求O(log n) 所以需要使用二分查找算法
*/
public class Question6 {
public int[] searchRange(int[] nums, int target) {
// 寻找第一次出现target的地方
int first = binarySearch(nums, target);
// 寻找第一次出现比target大1的数字的地方
int last = binarySearch(nums, target + 1) - 1;
if (first == nums.length || nums[first] != target) {
return new int[]{-1, -1};
} else {
return new int[]{first, Math.max(first, last)};
}
}
/**
* 封装成方法 用于寻找target第一次出现的位置
* @param nums
* @param target
* @return
*/
private int binarySearch(int[] nums, int target) {
int low = 0;
int hight = nums.length;
while (hight > low) {
int middle = low + (hight - low) / 2;
if (nums[middle] >= target) {
// 说明目标在上半部分
hight = middle;
} else {
// 说明目标在下半部分
low = middle + 1;
}
}
return low;
}
}