📌 题目链接:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、二分查找
⏱️ 目标时间复杂度:O(log n)
💾 空间复杂度:O(1)
本题要求我们在一个非递减排序数组中,高效地找出目标值 target 的起始与结束位置。如果不存在,则返回 [-1, -1]。关键约束是:必须实现 O(log n) 时间复杂度——这直接排除了暴力遍历的可能,将我们引向二分查找的变种应用。
🔍 题目分析
给定:
- 一个非递减(即升序,允许重复)整数数组
nums - 一个目标值
target
要求:
- 返回
target在数组中第一次出现和最后一次出现的下标 - 若不存在,返回
[-1, -1] - 时间复杂度必须为 O(log n)
💡 关键洞察:
-
数组已排序 → 适合使用二分查找
-
但标准二分只能判断“是否存在”,而我们需要定位边界
-
因此,需分别查找:
- 左边界:第一个
== target的位置 - 右边界:最后一个
== target的位置
- 左边界:第一个
⚙️ 核心算法及代码讲解
✅ 核心思想:两次二分查找 + 边界统一处理
我们不写两个独立的二分函数,而是通过一个通用的二分模板,用一个布尔参数 lower 控制查找逻辑:
- 当
lower = true时:查找 第一个 ≥ target 的位置 → 即左边界 - 当
lower = false时:查找 第一个 > target 的位置 → 右边界 = 该位置 - 1
📌 为什么这样设计?
因为“最后一个等于 target 的位置”等价于“第一个大于 target 的位置的前一个”。这种转换避免了单独处理右边界,极大提升代码复用性与鲁棒性。
🧾 C++ 核心代码详解(带行注释)
int binarySearch(vector<int>& nums, int target, bool lower) {
int left = 0;
int right = (int)nums.size() - 1;
int ans = (int)nums.size(); // 初始化为数组长度(越界安全值)
while (left <= right) {
int mid = left + (right - left) / 2; // 防止 (left + right) 溢出
// 关键判断条件:
// - 如果找左边界(lower=true):遇到 >= target 就尝试往左
// - 如果找右边界(lower=false):只在 > target 时往左
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1; // 缩小右边界
ans = mid; // 记录当前可能的答案
} else {
left = mid + 1; // 否则往右搜索
}
}
return ans; // 返回第一个满足条件的位置
}
⚠️ 注意:
mid = (left + right) / 2在极端情况下可能导致整数溢出(如left和right接近 INT_MAX)。因此更安全的写法是mid = left + (right - left) / 2。虽然本题数据范围不会溢出,但在嵌入式或系统级面试中这是加分项!
🧩 解题思路(分步拆解)
-
处理空数组特例
虽然我们的二分能处理nums.size() == 0,但提前判断可提升可读性(非必需)。 -
调用通用二分函数两次
leftIdx = binarySearch(nums, target, true)→ 第一个 ≥ target 的位置rightIdx = binarySearch(nums, target, false) - 1→ 最后一个 ≤ target 的位置
-
合法性校验
由于target可能不存在,必须验证:leftIdx <= rightIdx(区间有效)rightIdx < nums.size()(防止越界)nums[leftIdx] == target && nums[rightIdx] == target(确保找到的是 target)
-
返回结果
合法则返回[leftIdx, rightIdx],否则[-1, -1]
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(log n):两次独立二分查找,每次 O(log n) |
| 空间复杂度 | O(1):仅使用常数个额外变量 |
| 稳定性 | ✅ 完全稳定,无递归栈开销 |
| 面试价值 | ⭐⭐⭐⭐⭐ • 考察对二分查找边界的理解 • 考察代码抽象与复用能力 • 常作为“二分模板题”出现在大厂面试 |
💡 延伸思考:
- 若数组严格递增(无重复),本题退化为标准二分查找
- 若允许
O(n),可用双指针从两端向中间收缩(但不符合题意)- 实际工程中,C++ STL 提供
std::lower_bound和std::upper_bound,可直接调用(见文末彩蛋)
💻 代码
✅ C++ 完整实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int binarySearch(vector<int>& nums, int target, bool lower) {
int left = 0, right = (int)nums.size() - 1, ans = (int)nums.size();
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
vector<int> searchRange(vector<int>& nums, int target) {
int leftIdx = binarySearch(nums, target, true);
int rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.size() && nums[leftIdx] == target && nums[rightIdx] == target) {
return vector<int>{leftIdx, rightIdx};
}
return vector<int>{-1, -1};
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> nums1 = {5,7,7,8,8,10};
auto res1 = sol.searchRange(nums1, 8);
cout << "[" << res1[0] << "," << res1[1] << "]" << endl; // [3,4]
// 测试用例 2
vector<int> nums2 = {5,7,7,8,8,10};
auto res2 = sol.searchRange(nums2, 6);
cout << "[" << res2[0] << "," << res2[1] << "]" << endl; // [-1,-1]
// 测试用例 3
vector<int> nums3 = {};
auto res3 = sol.searchRange(nums3, 0);
cout << "[" << res3[0] << "," << res3[1] << "]" << endl; // [-1,-1]
return 0;
}
✅ JavaScript 完整实现
const binarySearch = (nums, target, lower) => {
let left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
var searchRange = function(nums, target) {
let ans = [-1, -1];
const leftIdx = binarySearch(nums, target, true);
const rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
ans = [leftIdx, rightIdx];
}
return ans;
};
// 测试
console.log(searchRange([5,7,7,8,8,10], 8)); // [3,4]
console.log(searchRange([5,7,7,8,8,10], 6)); // [-1,-1]
console.log(searchRange([], 0)); // [-1,-1]
🎁 彩蛋:STL 大法好(C++ 工程实践)
在实际开发中,完全不必手写二分!C++ STL 提供了现成的边界查找函数:
vector<int> searchRange(vector<int>& nums, int target) {
auto left = lower_bound(nums.begin(), nums.end(), target); // 第一个 >= target
auto right = upper_bound(nums.begin(), nums.end(), target); // 第一个 > target
if (left == nums.end() || *left != target) {
return {-1, -1};
}
return {(int)(left - nums.begin()), (int)(right - nums.begin() - 1)};
}
✅ 面试建议:
- 如果面试官允许使用 STL,优先展示此写法(体现工程素养)
- 但必须能手写二分!因为面试重点是考察你对算法的理解,而非 API 调用
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!