二分查找的时间复杂度:O(logn)
假设如果数组有n个元素,切分的次数为k,每次都切一半,也就是 n / (2^k) = 1,转换公式为 2^k = n,那么k就是log2N,所以时间复杂度为O(logn)。
public int search(int nums[], int start, int end, int target) {
if(end >= start) {
int mid = start + (end - start) / 2;
if(nums[mid] == target) {
return mid;
} else if (target < nums[mid]) {
return search(nums, start, mid - 1, target);
}
return search(nums, mid + 1, end, target);
}
return -1;
}
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
分析二分查找技巧:尽量不要出现 else,把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。“……”的地方是要注意的细节
int mid = left + (right - left) / 2
与(left + right) / 2
相同,但可防止left与right相加太大导致溢出
right
赋值为nums.length - 1
,区间[left,right]与数组长度相等while (left <= right)
要使用 <= ,因为left == right
是有意义的,循环的终止条件是left == right + 1
所以使用 <=if (nums[middle] > target) right
要赋值为 middle - 1
,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 定义target在左闭右闭的区间里,[left, right]
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid; // 数组中找到目标值,直接返回下标
else if (nums[mid] < target)
left = mid + 1; // target 在右区间,所以[middle + 1, right]
else if (nums[mid] > target)
right = mid - 1; // target 在左区间,所以[left, middle - 1]
}
// 未找到目标值,返回-1
return -1;
}
right
赋值为nums.length
,区间[left,right)与右端已超出数组长度,所以是开区间while (left < right)
要使用 < ,因为left == right
在区间[left, right)是没有意义的,循环的终止条件就是left == right
所以使用 <if (nums[middle] > target) right
要赋值为 middle
,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle](nums[middle]处于开区间边界)int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length; // 定义target在左闭右开的区间里,[left, right)
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid; // 数组中找到目标值,直接返回下标
else if (nums[mid] < target)
left = mid + 1; // target 在右区间,所以[middle + 1, right]
else if (nums[mid] > target)
right = mid; // target 在左区间,在[left, middle)中
}
// 未找到目标值,返回-1
return -1;
}
当有重复元素时,如果有多个目标元素,找到目标元素的边界位置(返回最大/最小的索引)
找边界关键在于对于 nums[mid] == target
这种情况的处理:找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
由于 while 的退出条件是 left == right + 1
,所以当 target 比 nums 中所有元素都大时,会存在left > nums.length,因此,最后返回结果的代码应该检查越界情况
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况、未找到情况,返回-1
if (left >= nums.length || nums[left] != target)
return -1;
//查找到结果,最后返回输出
return left; //或返回right+1
}
int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
// 检查出界,
if (left >= nums.length) return -1;
//
return nums[left] == target ? left : -1;
}
缩小左侧边界,检查right越界
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
//检查出界,left == 0即right<0(由循环退出条件得)
if (left == 0) return -1;
//找到返回left-1,未找到返回-1
return nums[left-1] == target ? (left-1) : -1;
}
需要从题目中抽象出一个自变量 x(x的取值范围即为二分查找的搜索区间),一个关于 x 的函数 f(x),以及一个目标值 target。x, f(x), target 满足以下条件:
f(x) 必须是在 x 上的单调函数(单调增单调减都可以)。
题目要求计算满足约束条件 f(x) == target 时的 x 的值。
eg:有一个升序排列的有序数组 nums 以及一个目标元素 target,请你计算 target 在数组中的索引位置,如果有多个目标元素,返回最小的索引。
x ——数组元素的索引下标
f(x) ——升序排列的有序数组,即f(x)单调递增
target ——目标值
要求计算:满足 f(x) == target 的 x 的最小值是多少
可以使用二分查找
// 函数 f 是关于自变量 x 的单调函数
int f(int x) {
// ...
}
// 主函数,在 f(x) == target 的约束下求 x 的最值
int solution(int[] nums, int target) {
if (nums.length == 0) return -1;
// 自变量 x 的最小值是多少?
int left = ...;
// 自变量 x 的最大值是多少?
int right = ... + 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (f(mid) == target) {
// 题目是求左边界还是右边界?
// ...
} else if (f(mid) < target) {
// 怎么让 f(mid) 大一点?
// ...
} else if (f(mid) > target) {
// 怎么让 f(mid) 小一点?
// ...
}
}
return ...; //左边界left、右边界left - 1
}
题目一般都会要求算法的时间复杂度,如果是 O ( n l o g n ) O(nlogn) O(nlogn)这样存在对数的复杂度,一般都要往二分查找的方向上靠
想用二分查找技巧优化算法,首先要把 for 循环形式的暴力算法写出来
// func(i) 是 i 的单调函数(递增递减都可以)
int func(int i);
int target;
// 形如这种 for 循环可以用二分查找技巧优化效率
for (int i = 0; i < n; i++) {
if (func(i) == target)
return i;
}
注意观察 for 循环形式,不一定是 func(i) == target 作为终止条件,可能是 <= 或者 >= 的关系,这个可以根据具体的题目意思来推断,使用查找左侧/右侧边界的变体