LeetCode 34:在排序数组中查找元素的首尾位置——Java 两次二分查找详解

一、题目描述

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

如果数组中不存在 target,返回 [-1,-1]。题目要求算法的时间复杂度必须为 O(log n)

例如:

复制代码
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

数字 8 出现了两次,第一次出现的位置是下标 3,最后一次出现的位置是下标 4

如果目标值为 6,数组中不存在该数字,则返回:

复制代码
[-1,-1]

数组已经有序,并且时间复杂度要求为 O(log n),因此这道题应该使用二分查找。问题在于:普通二分查找只能找到目标值的某一个位置,如何进一步找到它的左右边界?

二、普通二分查找为什么不够

标准二分查找在发现 nums[mid] == target 时会立即返回 mid

复制代码
if (nums[mid] == target) {
    return mid;
}

但是,当数组中存在多个相同元素时,这个 mid 不一定是第一个位置,也不一定是最后一个位置。

例如:

复制代码
nums = [5,7,7,8,8,10]
target = 8

二分查找可能先找到下标 4,但我们还不能确定下标 3 是否也是 8。同理,即使第一次找到的是下标 3,也不能确定它右侧是否还有目标值。

因此,本题找到 target 后不能立即返回,而是应该:

  1. 先记录当前找到的下标;

  2. 根据要查找的边界,继续向左或向右搜索;

  3. 直到搜索区间为空,最后一次记录的位置就是对应边界。

三、整体思路:执行两次二分查找

目标值的开始位置和结束位置是两个不同的问题,可以分别执行一次二分查找:

  • 第一次查找左边界,即目标值第一次出现的位置;

  • 第二次查找右边界,即目标值最后一次出现的位置。

两个搜索过程的大部分逻辑完全相同,只有在命中目标值后的搜索方向不同。因此,可以编写一个 findMost 方法,并通过布尔参数 isLeft 区分搜索目标:

复制代码
findMost(nums, target, true);   // 查找左边界
findMost(nums, target, false);  // 查找右边界

最终将两次搜索的结果组合起来:

复制代码
return new int[] {
    findMost(nums, target, true),
    findMost(nums, target, false)
};

如果目标值不存在,两次搜索都会返回初始值 -1,自然得到 [-1,-1]

四、tempIndex 为什么必不可少

在二分查找中定义一个临时变量:

复制代码
int tempIndex = -1;

每当发现 nums[mid] == target 时,就把当前下标记录下来:

复制代码
tempIndex = mid;

之所以只记录而不立即返回,是因为当前的 mid 只是一个候选边界,真正的左边界可能还在左侧,真正的右边界也可能还在右侧。

如果后续搜索又找到了更靠近目标方向的相同元素,就再次更新 tempIndex。当循环结束时,tempIndex 保存的就是最终边界。

如果整个搜索过程中一次都没有找到 targettempIndex 会保持为 -1

五、如何查找左边界

查找左边界时,遇到 nums[mid] == target,说明当前下标可能是第一个位置,但它的左边仍可能存在相同元素。

因此先记录当前位置,再继续搜索左半部分:

复制代码
tempIndex = mid;
right = mid - 1;

例如:

复制代码
nums = [5,7,7,8,8,10]
target = 8

如果当前找到下标 4,先把 4 记录下来,然后将右边界移动到 3,继续检查左侧。若之后发现下标 3 也是 8,就用 3 更新 tempIndex

循环结束后,得到左边界 3

可以把查找左边界的规则记为:

命中后记录答案,并继续向左搜索。

六、如何查找右边界

查找右边界的逻辑与左边界相反。

nums[mid] == target 时,当前下标可能是最后一个位置,但右侧仍可能存在相同元素。因此先记录当前位置,再继续搜索右半部分:

复制代码
tempIndex = mid;
left = mid + 1;

如果当前先找到下标 3,就将 3 记录下来,然后继续搜索它的右侧。后续找到下标 4 时,再将 tempIndex 更新为 4

循环结束后,得到右边界 4

对应的记忆规则是:

命中后记录答案,并继续向右搜索。

七、未命中时如何更新区间

除了命中目标值后的特殊处理,其余情况与标准二分查找完全相同。

如果中间元素小于目标值,目标值只能出现在右侧:

复制代码
if (nums[mid] < target) {
    left = mid + 1;
}

如果中间元素大于目标值,目标值只能出现在左侧:

复制代码
else if (nums[mid] > target) {
    right = mid - 1;
}

只有在 nums[mid] == target 时,才需要根据 isLeft 决定继续搜索的方向。

八、完整 Java 代码

下面的代码严格使用同一个 findMost 方法完成左右边界查找:

复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        return new int[] {
            findMost(nums, target, true),
            findMost(nums, target, false)
        };
    }

    // isLeft 为 true 时查找左边界,否则查找右边界
    private int findMost(int[] nums, int target, boolean isLeft) {
        int left = 0;
        int right = nums.length - 1;
        int tempIndex = -1;

        while (left <= right) {
            int mid = left + ((right - left) >>> 1);

            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else {
                // 先记录当前命中的位置
                tempIndex = mid;

                if (isLeft) {
                    // 查找左边界:继续向左搜索
                    right = mid - 1;
                } else {
                    // 查找右边界:继续向右搜索
                    left = mid + 1;
                }
            }
        }

        return tempIndex;
    }
}

这里使用下面的方式计算中间下标:

复制代码
int mid = left + ((right - left) >>> 1);

它与 (left + right) / 2 的作用相同,但可以避免 left + right 过大时出现整数溢出。

九、示例推演

nums = [5,7,7,8,8,10]target = 8 为例。

1. 查找左边界

初始区间为 [0,5]

复制代码
mid = 2,nums[2] = 7 < 8
left = 3

搜索区间变为 [3,5]

复制代码
mid = 4,nums[4] = 8
tempIndex = 4
right = 3

由于查找左边界,命中后继续向左。此时区间为 [3,3]

复制代码
mid = 3,nums[3] = 8
tempIndex = 3
right = 2

循环结束,左边界为 3

2. 查找右边界

前两步同样会找到下标 4

复制代码
tempIndex = 4
left = 5

因为查找右边界,命中后继续向右。接下来 nums[5] = 10 > 8,搜索结束,右边界为 4

最终返回:

复制代码
[3,4]

十、复杂度分析

查找左边界和右边界分别执行一次二分查找,每次的时间复杂度都是 O(log n)。两次相加仍然是:

复制代码
O(log n)

算法只使用了 leftrightmidtempIndex 等变量,没有创建与数组长度相关的额外空间,因此空间复杂度为:

复制代码
O(1)

十一、常见错误

1. 找到目标值后立即返回

这样只能得到目标值的任意一个位置,无法保证它是左边界或右边界。

2. 命中后没有保存当前位置

继续搜索可能会导致最终区间为空,因此必须先使用 tempIndex 保存当前候选答案。

3. 左右边界的搜索方向写反

查找左边界时应执行 right = mid - 1;查找右边界时应执行 left = mid + 1

4. 使用线性扫描寻找边界

先二分找到目标值,再向左右逐个扫描,最坏情况下需要遍历整个数组,时间复杂度会退化为 O(n),不符合题目要求。

5. 使用 left < right 配合闭区间边界

本文采用闭区间 [left,right],因此循环条件必须是 left <= right。如果混用不同二分模板,容易漏掉只剩一个元素的情况。

十二、总结

这道题是在标准二分查找基础上增加了"边界搜索"。由于数组中可能出现多个连续的目标值,命中目标值后不能立即返回,而要记录当前位置并继续向对应方向搜索。

为了避免编写两套重复代码,可以通过 isLeft 参数复用一个 findMost 方法:isLefttrue 时继续向左收缩,寻找第一次出现的位置;为 false 时继续向右收缩,寻找最后一次出现的位置。

相关推荐
@卓越俊逸_角立杰出@1 小时前
java实现Agent+ReAct demo(Spring Boot + Spring AI Alibaba ReAct Agent)
java·spring·react.js
看浪的路人1 小时前
第6讲:SQL 解析器
java·开发语言·数据库
海拥✘1 小时前
Python 实战:用 IPPEAK 代理 IP 构建稳定的公开数据采集链路
网络·python·tcp/ip
weixin_440784111 小时前
【OkHttp实现原理】
android·java·okhttp
头茬韭菜1 小时前
第 4 篇:「Fluss + Flink 集成实战」—— Catalog、Source 与 Sink
大数据·python·flink·fluss
a3535413821 小时前
C++项目如何架构优化
java·c++·架构
天天爱吃肉82182 小时前
【工程师硬件学习笔记:串口‑总线体系与三电试验室异构系统集成深度实战】
大数据·笔记·python·嵌入式硬件·学习·汽车
AC赳赳老秦2 小时前
官方技术文档聚合实践:用 OpenClaw 批量抓取开源项目文档,构建离线可检索技术知识库
java·运维·服务器·python·信息可视化·deepseek·openclaw
Zenova EdgeOS2 小时前
工业边缘 SDK 设计实战:从 API 到 Python/Go 多语言工程落地
python·golang·php