34. 查找元素的首末位置

中等 含交互动画

学完这道你应该能: 说清它为什么用「二分边界」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

AI 私教 · 就着本题动画讲
卡住了不用硬扛,按 8 步把这题真正走通。
从读题、试解、找法到手写代码,边问边打勾,适合第一次系统学算法的同学。

题目描述

升序数组中找 target 的起始结束下标,不存在返回 [−1, −1]。

nums = [5, 7, 7, 8, 8, 10]
target = 8
输出 = [3, 4]

思路解析

先二分找到任意一个 8,再向左、向右一格格线性扩到边。可如果数组全是 8,向两边扩会退化成 O(n),等于白做了二分。题目给了有序,应该全程都用二分。

转折:把「找边界」改写成「找第一个满足某条件的位置」。不变量:数组有序,所以「≥8 的位置」是连续一段,第一个 ≥8 就是左界 3;「大于8 的位置」也是连续一段,第一个大于8 是 5,它前一位 4 就是右界。复用同一个下界二分两次,各 O(log n)。

找左界 · 准备 [0,5]:第一趟找左界,目标是「第一个 ≥ 8 的下标」。l=0、r=5,圈住整个数组。

找左界 · 第 1 轮取 mid:中点 mid=2,nums[2] = 7。判断它满不满足「≥ 8」。

找左界 · 判断(不满足·负例):7 比 8 小,不满足 ≥8。那 mid 以及它左边都不可能是左界,左界只能在 mid 右边。

找左界 · 收缩:下标 0、1、2(含 mid)全比 8 小,整段灰掉,l 跳到 mid+1 = 3。范围缩到 [3, 5]。

找左界 · 第 2 轮取 mid:新范围 [3, 5],中点 mid=4,nums[4] = 8。再判断「≥ 8」。

找左界 · 判断(满足):8 满足 ≥8,mid 可能就是左界,但它前面也许还有更早的 8。所以不能丢 mid,收右 r=mid=4,继续往左看。范围 [3, 4]。

找左界 · 第 3 轮收敛:范围 [3, 4],mid=3,nums[3]=8 也满足 ≥8,r 收到 3。现在 l == r == 3,收敛——这就是第一个 8,左界 = 3。

找右界 · 第一个大于 8:第二趟找右界。技巧:找「第一个大于 8 的下标」,它前一位就是最后一个 8。l、r 重新圈回整个数组。

找右界 · mid=2 → 收右:mid=2 是 7,不满足「大于8」,第一个大于8 在右边,l 跳到 3。范围 [3, 5]。

找右界 · mid=4 → 仍收右:mid=4 还是 8,等于不算大于,仍不满足,l 跳到 5。下标 3、4(两个 8)也灰掉。范围只剩 [5, 5]。

找右界 · 收敛 → 答案:l==r==5,nums[5]=10 是第一个大于 8 的位置。它前一位 4 就是最后一个 8。配上左界 3,最终答案 [3, 4]。

「某值的左右边界 / 出现次数」都能化成两次下界二分:第一个 ≥t 给左界,第一个 大于t 给右界+1,两者相减就是出现次数。

参考代码(Python)

Python
1def searchRange(self, nums, target):
2    def lower(t):                     # 第一个 >= t 的位置
3        l, r = 0, len(nums)           # 开区间右端取 n
4        while l < r:
5            mid = (l + r) // 2
6            if nums[mid] >= t: r = mid    # 满足,收右保留候选
7            else: l = mid + 1            # 不满足,往右
8        return l
9    lo = lower(target)
10    if lo == len(nums) or nums[lo] != target:
11        return [-1, -1]               # 没找到该值
12    return [lo, lower(target + 1) - 1]   # 右界 = 第一个 大于 t 再减1

复杂度分析

  • 时间复杂度:O(log n) —— 两次下界二分,每次都把区间砍半
  • 空间复杂度:O(1) —— 只用 l、r、mid 几个指针,没开额外结构

套路模板

记住骨架:写一个 lower(t),左界=lower(t)、右界=lower(t+1)−1、出现次数=lower(t+1)−lower(t)。一个函数解决一类边界题。

Python
1# 下界二分:第一个 >= t 的位置
2def lower(t):
3    l, r = 0, n
4    while l < r:
5        mid = (l + r) // 2
6        if nums[mid] >= t: r = mid
7        else: l = mid + 1
8    return l
9left, right = lower(t), lower(t + 1) - 1   # [左界, 右界]

易错点

  • 错误写法:直接返回 lower(t),不判存在正确写法:先看 lo<n 且 nums[lo]==target(lower(t) 只给「应插入位置」,target 不存在时它指向第一个比 t 大的数,不验证会把它当答案)
  • 错误写法:右界单独写一套找最后位置的二分正确写法:复用 lower(t+1) − 1(另写一套容易把 <=/< 和 +1/-1 搞反;用 lower(t+1)−1 只维护一份逻辑,不易错)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →416. 分割等和子集 ← 返回题库