问题描述
给定一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。要求找出目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回 [-1, -1]。
时间复杂度要求:O(log n)
示例
text
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
输入:nums = [], target = 0
输出:[-1,-1]
解题思路
题目中的“非递减顺序” + “O(log n)” 已经明确暗示我们使用二分查找。但普通的二分查找只能找到一个目标值的位置,无法直接得到第一个和最后一个。
因此,我们可以将问题拆解为:
- 找到目标值在数组中的第一个位置(左边界)
- 找到目标值在数组中的最后一个位置(右边界)
两次二分查找,分别控制查找方向:
- 找左边界:当
nums[mid] == target时,不立即返回,而是继续向左收缩右边界(right = mid - 1),直到锁定最左边的目标。 - 找右边界:当
nums[mid] == target时,继续向右收缩左边界(left = mid + 1),直到锁定最右边的目标。
这样,两次二分查找的时间复杂度均为 O(log n),整体仍为 O(log n)。
代码实现(JavaScript)
javascript
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
let left = 0;
let right = nums.length - 1;
let leftIndex = -1;
let rightIndex = -1;
// 1. 找左边界(第一个位置)
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
leftIndex = mid;
right = mid - 1; // 继续向左搜索
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 重置左右指针
left = 0;
right = nums.length - 1;
// 2. 找右边界(最后一个位置)
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
rightIndex = mid;
left = mid + 1; // 继续向右搜索
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return [leftIndex, rightIndex];
};
代码详解
1. 寻找左边界
- 初始化
left = 0,right = nums.length - 1。 - 当
nums[mid] === target时,记录当前位置leftIndex = mid,并设置right = mid - 1,强制在左半区继续查找,确保找到最左边的目标。 - 其他情况按照标准二分查找更新指针。
2. 寻找右边界
- 同样重置左右指针。
- 当
nums[mid] === target时,记录rightIndex = mid,并设置left = mid + 1,强制在右半区继续查找,确保找到最右边的目标。
3. 边界情况
- 数组为空:
left > right直接退出循环,返回[-1, -1]。 - 目标值不存在:两次查找中都不会记录有效索引,最终返回
[-1, -1]。
复杂度分析
- 时间复杂度:
O(log n),两次二分查找,每次都是对数级别。 - 空间复杂度:
O(1),只使用了常数个变量。
测试用例验证
| 输入 | 输出 |
|---|---|
[5,7,7,8,8,10], 8 | [3,4] |
[5,7,7,8,8,10], 6 | [-1,-1] |
[], 0 | [-1,-1] |
[1], 1 | [0,0] |
[2,2,2,2,2], 2 | [0,4] |
常见误区 & 进阶思考
误区1:找到任意一个 target 后向两边线性扩展
有些同学可能会想到先用二分找到任意一个 target,然后向左右线性扫描。在最坏情况下(全数组都是 target),线性扩展会使时间复杂度退化为 O(n),不满足题目要求。
误区2:边界更新条件写错
例如在找左边界时,不小心写成 left = mid + 1,会导致永远找不到最左值;或者循环条件忘记等号,导致遗漏边界元素。
进阶思考:能否用一次二分查找同时获得左右边界?
理论上可以,但需要额外处理边界逻辑,代码会变得复杂且易错。两次独立的二分查找思路清晰、不易出错,是工程上推荐的做法。
小结
这道题是二分查找的进阶应用,核心在于当找到目标时不立即停止,而是继续向一侧收缩,从而定位边界。掌握这种“二分查找 + 边界收缩”的思想,对解决很多有序数组中的查找问题(如寻找插入位置、统计目标出现次数等)都非常有帮助。
希望这篇博客能帮你彻底理解这道经典题目。如果对二分查找还不太熟悉,建议先练练基础版《704. 二分查找》,再回过头来体会边界的精妙控制。
Happy Coding! 🚀