二分查找法的区间定义是重中之重。一般定义为两种,左闭右闭[left, right],或者左闭右开[left, right);为什么这么分呢?前者是 left == right 有意义;后者则是 left == right 没有意义。
二分法适用范围 (前提)
1.有序数组中寻找是否存在一个目标值
2.数组中无重复元素
时间复杂度
二分法查找时间复杂度:O(log n)
左闭右闭 [left, right]
- 定义 left、right、middle(直接记住写法即可);
- 判断 “数组中间值” 和 target 的关系(>、 <、 =);
- 在最后不要忘记没有找到 target 的情况,返回 -1。
输入:nums[], target
输出:target 在数组中的位置;(PS:在二分法查找中,输出的就是 middle)
代码:
int left = 0;
int right = nums.size() - 1; //left是从0开始的,因此right这里要减1;
while (left <= right){
//middle这样写是防止越界
int middle = (right - left) / 2 + left;//或者写成:(right + left) / 2
if(nums[middle] > target)
right = middle - 1;
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return -1; //未找到目标值
左闭右开 [left, right)
- 定义 left、right、middle(直接记住写法即可);
- 判断 “数组中间值” 和 target 的关系(>、 <、 =);
- 在最后不要忘记没有找到 target 的情况,返回 -1。
输入:nums[], target
输出:target 在数组中的位置;(PS:在二分法查找中,输出的就是 middle)
代码:
int left = 0;
int right = nums.size(); //left是从0开始的,因此right这里要减1;
while (left < right){
//middle这样写是防止越界
int middle = (right - left) / 2 + left;//或者写成:(right + left) / 2
if(nums[middle] > target)
right = middle;
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return -1; //未找到目标值
注意
我们划分区间的本质是区间包含与不包含的关系。包含就是闭,不包含就是开。
我自己的口诀是 “闭紧凑”,“开宽松”。
-
left初始定义为0,因为一直是“左闭”,后面被赋值时两种情况都是要 middle+1;
-
right初始定义和后面被赋值情况都根据 “闭”、“开”决定
// 初始定义
“闭”:right = 数组大小 - 1; //要紧凑,就要 -1;
“开”:right = 数组大小; //要宽松就不必减//后面被赋值
“闭”:right = middle - 1; //要紧凑,就要 -1;
“开”:right = middle; //要宽松就不必减
题目练习
练习一:搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 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
分析条件:时间复杂度为 O(log n) ,有序数组找值。锁定二分法查找
根据上述代码改,结果如下。运行时间为3ms.
int searchInsert(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
while(left <= right){
int middle = (left + right) / 2;
if(nums[middle] > target)
right = middle - 1;
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return left; //如果没有值,则返回left,也就是插入位置
}
更好的解法是在上述代码上再稍改一下,运行时间为0ms:
int searchInsert(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
while(left <= right){
int middle = (left + right) / 2;
if(nums[middle] >= target)
right = middle - 1;
else
left = middle + 1;
}
return left; //如果没有值,则返回left,也就是插入位置
}
练习二:在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
根据题意,我们需要查找左边界与右边界。
写代码之前明确所有情况:
一、target 在有序数组中范围内(如 4 在{1,2,5}范围中),但不等于数组中任何一个值,返回-1;
二、target 不在有序数组中范围内,返回-1;
三、target 等于有序数组中某个值,这是我们重点处理的情况
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int searchleft(int* nums, int numsSize, int target){
int left = 0, right = numsSize - 1, leftBoard = -1;
while(left <= right){
int middle = (left + right) / 2;
if(nums[middle] == target){
leftBoard = middle; //先将暂时找到的左边界赋值,后面再深入找
right = middle - 1; //向左继续寻找左边界
}else if(nums[middle] < target)
left = middle + 1;
else
right = middle - 1;
}
return leftBoard;
}
int searchright(int* nums, int numsSize, int target){
int left = 0, right = numsSize - 1, rightBoard = -1;
while(left <= right){
int middle = (left + right) / 2;
if(nums[middle] == target){
rightBoard = middle; //先将暂时找到的右边界赋值,后面再深入找
left = middle + 1; //向右继续寻找右边界
}else if(nums[middle] < target)
left = middle + 1;
else
right = middle - 1;
}
return rightBoard;
}
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
int left_ret = searchleft(nums, numsSize, target);
int right_ret = searchright(nums, numsSize, target);
*returnSize = 2;
int *retnum = (int*)malloc(sizeof(int) * 2);
retnum[0] = left_ret;
retnum[1] = right_ret;
return retnum;
}
练习三:第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
测试程序通过 11/24 ,主要原因是这行代码:
nt middle = (left + right) / 2;
导致整数溢出!!!
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
int firstBadVersion(int n) {
int left = 0;
int right = n-1;
int falseVersion = 1;
while(left <= right){
int middle = (left + right) / 2;
if(isBadVersion(middle)){
right = middle - 1; // [0, mid - 1]
}else{
left = middle + 1; // [mid + 1, right]
}
}
return left; //此时,left==right,return任何一个即可
}
换成
int middle = left + (right - left) / 2
后全部通过! 即如下所示:
int firstBadVersion(int n) {
int left = 0;
int right = n-1;
int falseVersion = 1;
while(left <= right){
int middle = left + (right - left) / 2;
if(isBadVersion(middle)){
right = middle - 1; // [0, mid - 1]
}else{
left = middle + 1; // [mid + 1, right]
}
}
return left;
}
这样可以确保不会发生整数溢出,因为 (right - left) 总是小于或等于 right 的,而 (right - left) / 2 也就会小于或等于 right / 2,因此加上 left 也不会超出 int 的范围。
所以,以后我们编程为避免溢出问题,定义middle使用代码:
int middle = left + (right - left) / 2;