【LeetCode Hot100 刷题日记 (65/100)】34. 在排序数组中查找元素的第一个和最后一个位置 —— 二分查找的边界艺术🧠

9 阅读5分钟

📌 题目链接: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 在极端情况下可能导致整数溢出(如 leftright 接近 INT_MAX)。因此更安全的写法是 mid = left + (right - left) / 2。虽然本题数据范围不会溢出,但在嵌入式或系统级面试中这是加分项!


🧩 解题思路(分步拆解)

  1. 处理空数组特例
    虽然我们的二分能处理 nums.size() == 0,但提前判断可提升可读性(非必需)。

  2. 调用通用二分函数两次

    • leftIdx = binarySearch(nums, target, true) → 第一个 ≥ target 的位置
    • rightIdx = binarySearch(nums, target, false) - 1 → 最后一个 ≤ target 的位置
  3. 合法性校验
    由于 target 可能不存在,必须验证:

    • leftIdx <= rightIdx(区间有效)
    • rightIdx < nums.size()(防止越界)
    • nums[leftIdx] == target && nums[rightIdx] == target(确保找到的是 target)
  4. 返回结果
    合法则返回 [leftIdx, rightIdx],否则 [-1, -1]


📊 算法分析

项目分析
时间复杂度O(log n):两次独立二分查找,每次 O(log n)
空间复杂度O(1):仅使用常数个额外变量
稳定性✅ 完全稳定,无递归栈开销
面试价值⭐⭐⭐⭐⭐ • 考察对二分查找边界的理解 • 考察代码抽象与复用能力 • 常作为“二分模板题”出现在大厂面试

💡 延伸思考

  • 若数组严格递增(无重复),本题退化为标准二分查找
  • 若允许 O(n),可用双指针从两端向中间收缩(但不符合题意)
  • 实际工程中,C++ STL 提供 std::lower_boundstd::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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!