1. 题目解析与核心需求
这道题目要求我们在一个已排序的数组中,找到目标值的第一个和最后一个出现位置。乍看简单,但实际考察的是对二分查找算法的深入理解和灵活运用能力。
给定一个按照非递减顺序排列的整数数组 nums 和一个目标值 target,我们需要实现一个函数,返回 target 在 nums 中的开始位置和结束位置。如果数组中不存在该目标值,则返回 [-1, -1]。
注意:算法时间复杂度必须控制在 O(log n) 级别,这意味着我们不能简单地遍历整个数组。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 暴力解法与优化方向
最直观的解法是顺序遍历数组,记录target第一次和最后一次出现的位置。这种方法虽然简单,但时间复杂度是O(n),不符合题目要求。
我们需要利用数组已排序的特性,采用二分查找来优化。标准的二分查找可以找到target是否存在,但无法直接确定其边界位置。因此,我们需要对标准二分查找进行改造。
2.2 二分查找变种思路
我们可以通过两次二分查找来分别确定左右边界:
- 第一次查找左边界:找到第一个等于target的元素
- 第二次查找右边界:找到最后一个等于target的元素
这种方法的优势在于每次查找都是O(log n),整体复杂度保持为O(log n)。
3. 详细实现步骤
3.1 查找左边界的实现
查找左边界的核心在于:即使找到了target,也要继续向左搜索,直到确认没有更早的target。
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的位置(如果存在)
3.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的位置
3.3 完整解决方案
将两个边界查找函数组合起来,并处理边界情况:
python复制def searchRange(nums, target):
left_idx = find_left_bound(nums, target)
right_idx = find_right_bound(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]
4. 复杂度分析与优化
4.1 时间复杂度分析
两次二分查找的时间复杂度都是O(log n),因此整体复杂度为O(log n),满足题目要求。
4.2 空间复杂度分析
算法只使用了常数级别的额外空间,空间复杂度为O(1)。
4.3 可能的优化点
- 如果第一次查找左边界失败(返回的left_idx对应值不等于target),可以直接返回[-1, -1],避免第二次查找
- 可以合并两个边界查找函数,通过参数控制查找方向,减少代码重复
5. 常见问题与调试技巧
5.1 边界条件处理
常见错误包括:
- 空数组输入
- target小于数组所有元素
- target大于数组所有元素
- target存在于数组但只有一个
- target不存在但值在数组范围内
调试技巧:针对这些边界情况设计测试用例,确保函数在各种情况下都能正确返回。
5.2 无限循环问题
二分查找容易出现无限循环,特别是在处理边界时。关键检查点:
- 循环条件应为left <= right而非left < right
- mid计算应使用left + (right - left) // 2而非(left + right) // 2,防止整数溢出
5.3 实际编码中的易错点
- 混淆left和right的更新逻辑:查找左边界和右边界时,条件判断和更新方向是相反的
- 最终结果验证不足:找到的边界索引需要验证是否确实等于target
- 忽略数组越界检查:right_idx可能等于len(nums),需要特别处理
6. 扩展思考与应用场景
6.1 算法在实际中的应用
这种边界查找算法在数据处理中非常实用,例如:
- 日志分析中查找特定时间范围内的事件
- 数据库查询优化
- 统计某个值在有序数据集中的出现频率
6.2 变种题目练习
为了深入掌握这个技巧,可以尝试以下变种题目:
- 统计target在数组中出现的次数
- 查找第一个大于target的元素位置
- 查找最后一个小于target的元素位置
- 在旋转排序数组中查找target的范围
6.3 主定理与时间复杂度
这个问题是理解算法时间复杂度主定理的绝佳案例。主定理形式为:
T(n) = aT(n/b) + f(n)
在二分查找中:
a = 1(每次递归调用一次)
b = 2(问题规模减半)
f(n) = O(1)(每次操作的常数时间)
根据主定理第二种情况,时间复杂度为O(n^log_b a * log n) = O(log n)
7. 代码优化与最佳实践
7.1 更简洁的实现
可以将两个边界查找合并为一个函数,通过参数控制查找方向:
python复制def searchRange(nums, target):
def binary_search(nums, target, left_flag):
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target or (left_flag and nums[mid] == target):
left = mid + 1
if nums[mid] == target:
result = mid
else:
right = mid - 1
if nums[mid] == target:
result = mid
return result
left = binary_search(nums, target, True)
right = binary_search(nums, target, False)
if left != -1 and right != -1:
return [left, right]
return [-1, -1]
7.2 测试用例设计
完善的测试用例应包括:
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,3,5,7], 1, [0,0]), # 单个元素
([1,1,1,1], 1, [0,3]), # 全部相同
([1,2,3,4], 5, [-1,-1]), # 大于所有
([1,2,3,4], 0, [-1,-1]) # 小于所有
]
7.3 性能对比
为了验证优化效果,可以比较不同实现的运行时间:
- 线性扫描法:平均O(n),最坏O(n)
- 标准二分查找+线性扩展:平均O(log n),最坏O(n)
- 双二分查找法:始终O(log n)
在大数据量下(如1e6个元素),双二分查找法的优势会非常明显。
