问题简介
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
题目描述
给你一个按照非递减顺序排列的整数数组 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]
解题思路
💡 核心思想: 由于数组已排序,我们可以使用二分查找 来达到 O(log n) 的时间复杂度。但标准二分查找只能找到任意一个目标值的位置,我们需要分别找到第一个位置 和最后一个位置。
方法一:两次二分查找(推荐)
📌 步骤分解:
-
查找第一个位置(左边界):
- 当
nums[mid] == target时,不立即返回,而是继续在左半部分查找 - 记录可能的左边界位置
- 当
-
查找最后一个位置(右边界):
- 当
nums[mid] == target时,不立即返回,而是继续在右半部分查找 - 记录可能的右边界位置
- 当
-
验证结果:
- 如果左边界有效且对应值等于 target,则返回
[left, right] - 否则返回
[-1, -1]
- 如果左边界有效且对应值等于 target,则返回
方法二:一次二分查找 + 线性扩展(不推荐)
❌ 为什么不推荐:
- 虽然平均情况可能不错,但最坏情况下(整个数组都是 target)时间复杂度为 O(n)
- 不满足题目要求的 O(log n) 时间复杂度
方法三:使用内置函数(语言相关)
💡 思路: 某些语言提供查找第一个/最后一个位置的内置函数
- Java:
Arrays.binarySearch()需要额外处理 - Go: 需要手动实现或使用第三方库
结论:方法一是最优解!
代码实现
java
class Solution {
public int[] searchRange(int[] nums, int target) {
int left = findFirst(nums, target);
int right = findLast(nums, target);
// 验证结果是否有效
if (left <= right && left >= 0 && right < nums.length &&
nums[left] == target && nums[right] == target) {
return new int[]{left, right};
}
return new int[]{-1, -1};
}
// 查找第一个位置
private int findFirst(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid; // 记录可能的位置
right = mid - 1; // 继续在左半部分查找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 查找最后一个位置
private int findLast(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid; // 记录可能的位置
left = mid + 1; // 继续在右半部分查找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
}
go
func searchRange(nums []int, target int) []int {
left := findFirst(nums, target)
right := findLast(nums, target)
// 验证结果是否有效
if left <= right && left >= 0 && right < len(nums) &&
nums[left] == target && nums[right] == target {
return []int{left, right}
}
return []int{-1, -1}
}
// 查找第一个位置
func findFirst(nums []int, target int) int {
left, right := 0, len(nums)-1
result := -1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
result = mid // 记录可能的位置
right = mid - 1 // 继续在左半部分查找
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return result
}
// 查找最后一个位置
func findLast(nums []int, target int) int {
left, right := 0, len(nums)-1
result := -1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
result = mid // 记录可能的位置
left = mid + 1 // 继续在右半部分查找
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return result
}
示例演示
让我们以 nums = [5,7,7,8,8,10], target = 8 为例:
查找第一个位置(findFirst)
| 步骤 | left | right | mid | nums[mid] | 操作 |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 7 | left = 3 |
| 2 | 3 | 5 | 4 | 8 | result = 4, right = 3 |
| 3 | 3 | 3 | 3 | 8 | result = 3, right = 2 |
| 结束 | 3 > 2 | - | - | - | 返回 3 |
查找最后一个位置(findLast)
| 步骤 | left | right | mid | nums[mid] | 操作 |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 7 | left = 3 |
| 2 | 3 | 5 | 4 | 8 | result = 4, left = 5 |
| 3 | 5 | 5 | 5 | 10 | right = 4 |
| 结束 | 5 > 4 | - | - | - | 返回 4 |
✅ 最终结果:[3, 4]
答案有效性证明
✅ 正确性证明
-
左边界查找正确性:
- 当找到
target时,继续向左搜索确保找到的是第一个位置 - 循环不变式:
result始终记录已找到的最左边的target位置
- 当找到
-
右边界查找正确性:
- 当找到
target时,继续向右搜索确保找到的是最后一个位置 - 循环不变式:
result始终记录已找到的最右边的target位置
- 当找到
-
边界情况处理:
- 空数组:直接返回
[-1, -1] - 不存在 target:两个查找都返回 -1
- 单个元素:左右边界相同
- 空数组:直接返回
✅ 循环终止性
- 每次迭代要么
left增加,要么right减少 - 区间
[left, right]严格缩小,最终必然终止
复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(log n) | 两次二分查找,每次 O(log n) |
| 空间复杂度 | O(1) | 只使用常数额外空间 |
📊 详细分析:
- 时间: 每次二分查找最多执行
log₂(n)次比较,两次查找总共2 × log₂(n) = O(log n) - 空间: 仅使用几个变量存储索引和结果,无递归调用栈
问题总结
📌 关键要点:
- 二分查找变种: 标准二分查找找到任意位置,而本题需要找到边界位置
- 边界处理技巧: 找到目标值后不立即返回,而是继续在相应方向搜索
- 结果验证: 必须验证找到的边界是否有效(防止空数组或不存在的情况)
💡 通用模板:
java
// 查找左边界
while (left <= right) {
if (nums[mid] == target) {
result = mid;
right = mid - 1; // 关键:继续向左
}
// ... 其他逻辑
}
// 查找右边界
while (left <= right) {
if (nums[mid] == target) {
result = mid;
left = mid + 1; // 关键:继续向右
}
// ... 其他逻辑
}
✅ 适用场景:
- 在有序数组中查找元素的边界位置
- 需要 O(log n) 时间复杂度的搜索问题
- 类似问题:查找插入位置、查找大于/小于某值的元素等
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions