跳到主要内容

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

题目描述

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

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

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

解题思路

第一步:理解问题本质

这是一个二分查找的变体问题。普通的二分查找找到一个目标值就返回,但这里需要找到目标值的范围(第一个和最后一个位置)。

第二步:暴力解法

思路:先找到任意一个 target 的位置,然后向两边扩展。

class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
# 找到任意一个 target
try:
idx = nums.index(target)
except ValueError:
return [-1, -1]

# 向两边扩展
left = right = idx
while left > 0 and nums[left - 1] == target:
left -= 1
while right < len(nums) - 1 and nums[right + 1] == target:
right += 1

return [left, right]

缺点

  • index() 方法是 O(n)
  • 向两边扩展最坏也是 O(n)
  • 整体时间复杂度不满足 O(log n) 的要求

第三步:最优解法 —— 两次二分查找

核心洞察

  • 找左边界:当 nums[mid] == target 时,不立即返回,而是收缩右边界,继续向左查找
  • 找右边界:当 nums[mid] == target 时,收缩左边界,继续向右查找

算法步骤

  1. 第一次二分查找:找左边界

    • nums[mid] == target:记录位置,继续向左找(right = mid
    • nums[mid] < target:目标在右半区(left = mid + 1
    • nums[mid] > target:目标在左半区(right = mid
  2. 第二次二分查找:找右边界

    • nums[mid] == target:记录位置,继续向右找(left = mid + 1
    • nums[mid] < target:目标在右半区(left = mid + 1
    • nums[mid] > target:目标在左半区(right = mid

为什么正确

  • 每次找到 target 时不立即返回,而是继续向一边查找,确保找到的是最左/最右的位置
  • 两次二分查找都是 O(log n),满足题目要求

完整代码实现

from typing import List

class Solution:
"""
在排序数组中查找元素的第一个和最后一个位置 - 二分查找

核心思想:
排序数组中找 target 的范围,等价于找 target 的左边界和右边界。
用两次二分查找分别定位左边界和右边界。

找左边界的技巧:
当 nums[mid] == target 时,不立即返回,而是收缩右边界(right = mid - 1),
继续向左查找,看是否还有更小的索引也是 target。

找右边界的技巧:
当 nums[mid] == target 时,收缩左边界(left = mid + 1),
继续向右查找,看是否还有更大的索引也是 target。

时间复杂度:O(log n),两次二分查找
空间复杂度:O(1)
"""

def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
result_left, result_right = -1, -1

# 第一次二分:找左边界
left, right = 0, n # 左闭右开区间
while left < right: # 不使用 <=:左闭右开写法更统一
mid = left + (right - left) // 2
if nums[mid] == target:
result_left = mid # 记录当前位置
right = mid # 继续向左查找
elif nums[mid] < target:
left = mid + 1 # 目标在右半区
else:
right = mid # 目标在左半区

# 第二次二分:找右边界
left, right = 0, n
while left < right: # 不使用 <=:左闭右开写法更统一
mid = left + (right - left) // 2
if nums[mid] == target:
result_right = mid # 记录当前位置
left = mid + 1 # 继续向右查找
elif nums[mid] > target:
right = mid
else:
left = mid + 1

return [result_left, result_right]

示例推演

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

找左边界

步骤leftrightmidnums[mid]操作
105277 < 8,left = 3
23548找到!result_left=4,继续向左,right=3
33338找到!result_left=3,继续向左,right=2
432--left > right,结束

左边界 = 3

找右边界

步骤leftrightmidnums[mid]操作
105277 < 8,left = 3
23548找到!result_right=4,继续向右,left=5
35551010 > 8,right = 4
454--left > right,结束

右边界 = 4

结果:[3, 4]


复杂度分析

解法时间复杂度空间复杂度说明
暴力O(n)O(1)线性查找
两次二分(最优)O(log n)O(1)满足题目要求

易错点总结

1. 找左边界时,找到后还要向左找

if nums[mid] == target:
result_left = mid # 记录
right = mid - 1 # 继续向左!

不要直接返回,要继续向左查找是否有更小的索引也是 target。

2. 两次二分查找要独立进行

不能在一次二分中同时找左右边界,因为找到目标后的搜索方向不同。

3. mid 的计算要防溢出

mid = left + (right - left) // 2  # 正确
mid = (left + right) // 2 # 可能溢出

扩展思考

1. 如果只需要找第一个 >= target 的位置?

这就是 lower_bound,是找左边界问题的简化版。

2. 如果只需要找第一个 > target 的位置?

这就是 upper_bound,可以在找右边界的基础上稍作修改。

3. 二分查找的通用模板

def binary_search(nums, target):
left, right = 0, len(nums) # 左闭右开区间
while left < right: # 不使用 <=:左闭右开写法更统一
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid # 收缩右边界到 mid
return -1 if left >= len(nums) or nums[left] != target else left

相关题目

加载评论中...