二分查找进阶:为什么要做两次?
一、题目描述
给你一个按照 非递减顺序排列 的整数数组 nums,以及一个目标值 target。
请你找出 target 在数组中的 开始位置和结束位置。
如果数组中不存在 target,返回 [-1, -1]。
要求算法的时间复杂度为 O(log n)。
二、示例
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
三、核心思路概览
这道题是二分查找的经典进阶题,核心难点不在“能不能找到 target”,而在于:
- 找到 target 的 最左边界
- 找到 target 的 最右边界
关键结论先给出:
一次二分查找,只能维护一种单调性
而左右边界,维护的是两种不同的单调性
所以必须使用 两次二分查找
四、为什么一次二分不够?
当 nums[mid] == target 时:
- 你无法确定它是不是第一个出现的位置
- 也无法确定它是不是最后一个出现的位置
比如:
nums = [5,7,7,8,8,8,8,10]
↑
mid
此时 mid 左右都可能还有 target,
所以必须 继续向某一个方向收缩区间。
问题来了:
- 找左边界 → 需要“尽量往左缩”
- 找右边界 → 需要“尽量往右缩”
这两个方向是 相反的决策逻辑,无法在同一个二分循环中完成。
五、整体解法设计
解法结构
- 第一次二分:找 第一个等于 target 的位置
- 第二次二分:找 最后一个等于 target 的位置
时间复杂度:
- 两次二分查找
- 总复杂度仍然是
O(log n)
六、第一次二分:查找左边界
目标:
找到数组中 第一个 ≥ target 的位置
核心思想
- 当
nums[mid] >= target,答案可能在左边 - 不断向左收缩区间
代码实现
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
循环结束后:
left指向 第一个 ≥ target 的位置
需要额外判断:
if (left == nums.length || nums[left] != target) {
return new int[]{-1, -1};
}
防止数组中根本不存在 target。
七、第二次二分:查找右边界
目标:
找到数组中 最后一个 ≤ target 的位置
核心思想
- 当
nums[mid] <= target,答案可能在右边 - 不断向右收缩区间
代码实现
left = 0;
right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
循环结束后:
right指向 最后一个 ≤ target 的位置- 即 target 的右边界
八、完整代码
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = {-1, -1};
if (nums.length == 0) return res;
// 查找左边界
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (left == nums.length || nums[left] != target) {
return res;
}
res[0] = left;
// 查找右边界
left = 0;
right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
res[1] = right;
return res;
}
}
九、左右边界二分的本质区别总结
| 类型 | 判断条件 | 收缩方向 | 最终指针含义 |
|---|---|---|---|
| 左边界 | nums[mid] >= target | 向左收缩 | 第一个 target |
| 右边界 | nums[mid] <= target | 向右收缩 | 最后一个 target |
十、总结
- 二分查找不仅是“找数”,更重要的是 找边界
- 左右边界维护的是 不同的单调性
- 一次二分只能解决一个方向的问题
- 这类题是二分查找从“入门”到“熟练”的分水岭
如果你能真正吃透这道题,
LeetCode 上 80% 的二分题目都会变得很顺眼。