题目描述
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
解题思路
- 二分查找变形:
- 使用两次二分查找分别定位起始和结束位置
- 第一次查找左边界:当
nums[mid] == target时继续向左搜索 - 第二次查找右边界:当
nums[mid] == target时继续向右搜索
- 边界条件处理:
- 查找左边界后验证有效性
- 若左边界无效则直接返回 [-1, -1]
- 优化终止条件:
- 左边界找到后,右边界查找只需从左边界开始
- 减少无效搜索区域
- 二分模板统一:
- 循环条件
while (low <= high)确保完整覆盖 - 中点计算避免溢出:
mid = low + ((high - low) >> 1)
- 循环条件
关键洞察
- 边界分离原理:
- 起始和结束位置需要独立查找
- 标准二分查找无法同时获取两端边界
- 查找策略差异:
- 左边界优先考虑左侧区域(收缩右边界)
- 右边界优先考虑右侧区域(收缩左边界)
- 顺序依赖特性:
- 右边界查找可基于左边界结果优化
- 减少约50%搜索范围
- 无效结果处理:
- 左边界无效时直接返回失败
- 避免无效的右边界搜索
复杂度分析
| 指标 | 说明 |
|---|---|
| 时间复杂度 | O(log n):两次二分查找 |
| 空间复杂度 | O(1):仅使用指针变量 |
代码实现
- JavaScript(递归版本)
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function (nums, target) {
const search = (nums, target, leftRight) => {
let ans = -1;
let left = 0, right = nums.length - 1;
while (left <= right) {
const middle = left + Math.floor((right - left) / 2);
if (nums[middle] === target) {
ans = middle;
leftRight ? right = middle - 1 : left = middle + 1;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return ans;
}
return [
search(nums, target, true),
search(nums, target, false)
]
};
- 变种:单次遍历
const searchRangeOptimized = (nums, target) => {
let left = -1, right = -1;
let low = 0, high = nums.length - 1;
while (low <= high) {
const mid = low + ((high - low) >> 1);
if (nums[mid] < target) {
low = mid + 1;
} else if (nums[mid] > target) {
high = mid - 1;
} else {
// 找到目标时向两侧扩展
left = right = mid;
while (left > 0 && nums[left - 1] === target) left--;
while (right < nums.length - 1 && nums[right + 1] === target) right++;
return [left, right];
}
}
return [left, right];
};
// 时间复杂度:最坏O(n)(全相同元素时)
实际应用场景
- 日志时间范围查询:在有序时间戳中查找特定事件的首次和末次发生时间`
- 数据库索引检索:B+树索引中定位索引键的起始位置`
- 基因位置定位: DNA序列中查找特定碱基序列的起止位置`
- 版本控制系统: 提交历史中定位特定修改的首次和末次出现`