LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置

48 阅读3分钟

二分查找进阶:为什么要做两次?


一、题目描述

给你一个按照 非递减顺序排列 的整数数组 nums,以及一个目标值 target
请你找出 target 在数组中的 开始位置和结束位置

如果数组中不存在 target,返回 [-1, -1]

要求算法的时间复杂度为 O(log n)


二、示例

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

三、核心思路概览

这道题是二分查找的经典进阶题,核心难点不在“能不能找到 target”,而在于:

  • 找到 target 的 最左边界
  • 找到 target 的 最右边界

关键结论先给出:

一次二分查找,只能维护一种单调性
而左右边界,维护的是两种不同的单调性
所以必须使用 两次二分查找


四、为什么一次二分不够?

nums[mid] == target 时:

  • 无法确定它是不是第一个出现的位置
  • 无法确定它是不是最后一个出现的位置

比如:

nums = [5,7,7,8,8,8,8,10]
                ↑
               mid

此时 mid 左右都可能还有 target
所以必须 继续向某一个方向收缩区间

问题来了:

  • 找左边界 → 需要“尽量往左缩”
  • 找右边界 → 需要“尽量往右缩”

这两个方向是 相反的决策逻辑,无法在同一个二分循环中完成。


五、整体解法设计

解法结构

  1. 第一次二分:找 第一个等于 target 的位置
  2. 第二次二分:找 最后一个等于 target 的位置

时间复杂度:

  • 两次二分查找
  • 总复杂度仍然是 O(log n)

六、第一次二分:查找左边界

目标:

找到数组中 第一个 ≥ target 的位置

核心思想

  • nums[mid] >= target,答案可能在左边
  • 不断向左收缩区间

代码实现

int left = 0, right = nums.length - 1;
while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] >= target) {
        right = mid - 1;
    } else {
        left = mid + 1;
    }
}

循环结束后:

  • left 指向 第一个 ≥ target 的位置

需要额外判断:

if (left == nums.length || nums[left] != target) {
    return new int[]{-1, -1};
}

防止数组中根本不存在 target


七、第二次二分:查找右边界

目标:

找到数组中 最后一个 ≤ target 的位置

核心思想

  • nums[mid] <= target,答案可能在右边
  • 不断向右收缩区间

代码实现

left = 0;
right = nums.length - 1;
while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] <= target) {
        left = mid + 1;
    } else {
        right = mid - 1;
    }
}

循环结束后:

  • right 指向 最后一个 ≤ target 的位置
  • 即 target 的右边界

八、完整代码

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] res = {-1, -1};
        if (nums.length == 0) return res;

        // 查找左边界
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        if (left == nums.length || nums[left] != target) {
            return res;
        }
        res[0] = left;

        // 查找右边界
        left = 0;
        right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        res[1] = right;

        return res;
    }
}

九、左右边界二分的本质区别总结

类型判断条件收缩方向最终指针含义
左边界nums[mid] >= target向左收缩第一个 target
右边界nums[mid] <= target向右收缩最后一个 target

十、总结

  • 二分查找不仅是“找数”,更重要的是 找边界
  • 左右边界维护的是 不同的单调性
  • 一次二分只能解决一个方向的问题
  • 这类题是二分查找从“入门”到“熟练”的分水岭

如果你能真正吃透这道题,
LeetCode 上 80% 的二分题目都会变得很顺眼。