1. 题目解析与核心思路
这道力扣hot100题目要求我们在一个已排序的数组中,找到目标值的第一个和最后一个出现位置。看似简单的二分查找问题,实则暗藏多个考察点。我们先来看题目具体要求:
给定一个按照升序排列的整数数组nums,和一个目标值target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回[-1, -1]。
关键提示:算法时间复杂度必须为O(log n)级别
1.1 问题本质分析
这道题的核心在于理解"排序数组"和"时间复杂度O(log n)"这两个关键信息。排序数组意味着我们可以使用二分查找这种高效算法,而O(log n)的时间复杂度要求直接锁定了二分查找作为基础解法。
但普通的二分查找只能找到目标值的任意一个位置,而题目要求的是目标值的边界位置。这就需要我们对标准二分查找进行改造,实现"左边界查找"和"右边界查找"两种变体。
1.2 算法选择考量
为什么选择二分查找而不是线性扫描?对于长度为n的数组:
- 线性扫描:时间复杂度O(n),不符合题目要求
- 二分查找:时间复杂度O(log n),符合要求
但标准二分查找在遇到重复元素时,无法保证返回的是第一个或最后一个出现的位置。例如在数组[5,7,7,8,8,10]中查找8,标准二分可能返回索引3或4,但我们需要的是[3,4]。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二分查找变体实现
2.1 左边界查找实现
左边界查找是指找到目标值第一次出现的位置。实现要点:
python复制def find_left_bound(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
这个实现的关键在于:
- 当nums[mid] == target时,仍然执行right = mid - 1,继续向左搜索
- 循环结束时,left指向的就是第一个等于target的位置
- 需要检查left是否越界以及nums[left]是否确实等于target
2.2 右边界查找实现
右边界查找是指找到目标值最后一次出现的位置。实现要点:
python复制def find_right_bound(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right
这个实现的关键在于:
- 当nums[mid] == target时,仍然执行left = mid + 1,继续向右搜索
- 循环结束时,right指向的就是最后一个等于target的位置
- 需要检查right是否越界以及nums[right]是否确实等于target
3. 完整解决方案
3.1 Python实现代码
结合左右边界查找,完整的解决方案如下:
python复制def searchRange(nums, target):
def find_left(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
def find_right(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right
left_idx = find_left(nums, target)
right_idx = find_right(nums, target)
if left_idx <= right_idx and right_idx < len(nums) and nums[left_idx] == target and nums[right_idx] == target:
return [left_idx, right_idx]
return [-1, -1]
3.2 边界条件处理
在实际编码中,需要特别注意以下几种边界情况:
- 空数组输入:nums = [], target = 0
- 目标值不存在:nums = [1,2,3], target = 4
- 目标值在数组两端:nums = [1,1,2,2], target = 1
- 数组中所有元素都相同:nums = [1,1,1,1], target = 1
我们的解决方案通过最后的条件判断left_idx <= right_idx和数组越界检查,能够正确处理所有这些边界情况。
4. 复杂度分析与优化
4.1 时间复杂度分析
我们进行了两次二分查找:
- 查找左边界:O(log n)
- 查找右边界:O(log n)
总时间复杂度为O(log n) + O(log n) = O(log n),符合题目要求。
4.2 空间复杂度分析
只使用了常数级别的额外空间,空间复杂度为O(1)。
4.3 可能的优化方向
虽然当前解法已经满足题目要求,但仍有优化空间:
- 可以合并左右边界的查找过程,在一次二分查找中同时记录左右边界
- 对于某些特殊情况(如目标值在数组中大量重复),可以考虑其他优化策略
5. 常见问题与调试技巧
5.1 典型错误模式
在实现二分查找变体时,常见的错误包括:
- 循环条件错误:使用while left < right而不是while left <= right
- 边界更新错误:在左边界查找中错误地更新left或right
- 返回值选择错误:混淆了left和right的最终指向
5.2 调试方法建议
当实现出现问题时,建议:
- 使用小规模测试用例手动模拟执行过程
- 打印每次循环的left, mid, right值
- 特别注意当nums[mid] == target时的处理逻辑
例如,对于输入nums = [5,7,7,8,8,10], target = 8:
-
左边界查找过程:
left=0, right=5, mid=2 (nums[2]=7 < 8) → left=3
left=3, right=5, mid=4 (nums[4]=8 == 8) → right=3
left=3, right=3, mid=3 (nums[3]=8 == 8) → right=2
循环结束,left=3 -
右边界查找过程:
left=0, right=5, mid=2 (nums[2]=7 <= 8) → left=3
left=3, right=5, mid=4 (nums[4]=8 <= 8) → left=5
left=5, right=5, mid=5 (nums[5]=10 > 8) → right=4
循环结束,right=4
5.3 测试用例设计
全面的测试用例应该包括:
python复制test_cases = [
([5,7,7,8,8,10], 8, [3,4]), # 常规情况
([5,7,7,8,8,10], 6, [-1,-1]), # 目标不存在
([], 0, [-1,-1]), # 空数组
([1], 1, [0,0]), # 单元素数组
([1,1,1,1,1], 1, [0,4]), # 全相同元素
([1,3,5,7,9], 5, [2,2]), # 无重复目标
([1,2,3,4,5], 6, [-1,-1]), # 目标大于所有元素
([1,2,3,4,5], 0, [-1,-1]) # 目标小于所有元素
]
6. 算法扩展与应用
6.1 类似题目推荐
掌握这个二分查找变体后,可以解决一系列类似问题:
- 查找插入位置(力扣35题)
- 在排序数组中查找元素的第一个和最后一个位置(本题)
- x的平方根(力扣69题)
- 寻找峰值(力扣162题)
- 寻找旋转排序数组中的最小值(力扣153题)
6.2 实际应用场景
这种边界查找算法在实际开发中有广泛应用:
- 日志系统中按时间范围查询日志
- 数据库索引的范围查询优化
- 统计分析中的数值区间统计
- 游戏开发中的分数排名系统
6.3 算法思想延伸
二分查找变体的核心思想可以扩展到:
- 三分查找(用于寻找极值点)
- 指数查找(用于无界数组)
- 插值查找(针对均匀分布数据)
在实际编码中,我发现理解二分查找的各种变体关键在于明确循环不变量。对于左边界查找,循环不变量是"左边界一定在[left, right]区间内或刚好在right+1位置"。保持这个不变量,就能正确更新指针。另一个实用技巧是在纸上画出每次循环后left和right的位置变化,这比单纯在脑子里想象要直观得多。
