1.什么是二分查找?
二分查找,即折半查找,与顺序查找相比更加高效。但要求序列本身有序。
以升序数列为例,比较一个元素与数列中的中间位置的元素的大小,如果比中间位置的元素大,则继续在后半部分的数列中进行二分查找;如果比中间位置的元素小,则在数列的前半部分进行比较;如果相等,则找到了元素的位置。每次比较的数列长度都会是之前数列的一半,直到找到相等元素的位置或者最终没有找到要找的元素。
2.例题
.2.1 LeetCode704_二分查找
2.1.1问题描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:
输入: nums = [1], target = 0
输出: 0
2.1.2问题分析
使用二分查找的方法,用 left,right 指针控制 mid 指针的位置,若 target 大于 mid 位置的元素,说明 target 在 mid 之后且不含 mid ,故将 left 移动到 mid+1 的位置;反之,若 target 小于 mid 位置的元素,说明 target 在 mid 之前且不含 mid ,故将 right 移动到 mid-1 的位置。反复进行 上述操作并将对应 mid 位置的元素与target比较,直至找到 target 所在位置。
因为是查找索引,所以查找范围是 0 ~ len-1。
2.1.3代码实现
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int left = 0,right = len-1,mid = -1;
while(left<right){
mid = left+(right-left)/2;
if(nums[mid]==target) return mid;
else if(nums[mid]>target) right= mid-1;
else if(nums[mid]<target) left = mid+1;
}
if(nums[left]==target) return left;
else return -1;
}
}
2.1.4运行结果
2.2 LeetCode278_第一个错误的版本
2.2.1问题描述
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例 1:
输入:n = 5, bad = 4
输出:4解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
示例 2:
输入:n = 1, bad = 1
输出:1
2.2.2问题分析
这是二分查找的 01模型 ,只需要找到第一个1即可。使用 left,right 指针控制 mid 指针的位置,若 mid 位置的元素是 0 ,说明下一次查找范围应该在 mid 之后且不包含 mid ,故将 left 移动到 mid+1 位置;若 mid 位置的元素是 1 ,说明下一次查找范围应该在 mid 之前且包含 mid (可能这个 1 就是第一个 1),故将 right 移动到 mid 位置。反复执行上述操作直到查找范围为1个元素,即 left=right ,该元素就是“第一个1”。
因为版本号是从1开始,所以查找范围是 1~n。
2.2.3代码实现
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left = 1,right = n,mid = 0;
while(left<right){
mid = left+(right-left)/2;
if(isBadVersion(mid)) right = mid;
else left = mid+1;
}
return left;
}
}
2.2.4运行结果
2.3 LeetCode35_搜索插入位置
2.3.1问题描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:
输入: nums = [1], target = 0
输出: 0
2.3.2问题分析
这个问题是 01模型 的变体,要寻找序列中第一个 >=target 的元素的位置。使用 left,right 指针控制 mid 指针的位置,若 mid 位置的元素小于 target ,说明下一次查找范围应该在 mid 之后且不包含 mid ,故将 left 移动到 mid+1 位置;若 mid 位置的元素大于 target ,说明下一次查找范围应该在 mid 之前且包含 mid (可能这个元素就是第一个大于 target 的元素),故将 right 移动到 mid 位置。反复进行上述操作并将 mid 位置元素与 target 比较。
因为是要插入,所以可能会在队尾插入,范围是 0~len。
2.3.3代码实现
class Solution {
public int searchInsert(int[] nums, int target) {
int len = nums.length;
int left = 0,right = len,mid = 0;
while(left < right){
mid = left+(right-left)/2;
if(nums[mid]==target) return mid;
else if(nums[mid]>target) right = mid;
else left = mid+1;
}
return left;
}
}
2.3.4运行结果
3.总结
写二分查找时,要注意
- 查找范围是多少,left 取多少,right 取多少;
- 下一次的查找范围是多少,left 和 right 怎样移动,到底是 mid,还是mid+1,mid-1;
- 跳出循环的判断条件是什么(一般是left<right);
- mid 的计算方式:最好使用 mid = left+(right-left)/2 ,不要使用
mid = (right+left)/2,当数据量巨大时,后者会造成溢出。