1. 题目解析与核心需求
这道题目要求我们在一个已排序的数组中,找到目标值的第一个和最后一个出现位置。如果目标值不存在于数组中,则返回[-1, -1]。题目要求算法时间复杂度必须为O(log n),这意味着我们必须使用二分查找或其变种来实现。
1.1 问题示例分析
假设我们有以下排序数组:
code复制nums = [5,7,7,8,8,10]
target = 8
期望的输出是[3,4],因为8第一次出现在索引3,最后一次出现在索引4。
如果目标值不存在:
code复制nums = [5,7,7,8,8,10]
target = 6
则返回[-1,-1]。
1.2 时间复杂度要求
题目明确要求时间复杂度为O(log n),这直接指向了二分查找算法。普通的线性扫描虽然也能解决问题,但时间复杂度为O(n),不符合题目要求。因此,我们需要设计一个基于二分查找的解决方案。
2. 二分查找算法回顾
在深入解决这个问题之前,让我们先回顾一下标准的二分查找算法。标准的二分查找用于在有序数组中查找特定元素的位置,如果找到则返回其索引,否则返回-1。
2.1 标准二分查找实现
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
2.2 二分查找的变种需求
标准二分查找只能找到一个匹配元素的位置,而我们需要找到第一个和最后一个匹配位置。因此,我们需要对标准二分查找进行修改,使其能够:
- 找到第一个等于目标值的位置
- 找到最后一个等于目标值的位置
3. 解决方案设计
3.1 整体思路
我们可以通过两次二分查找来解决这个问题:
- 第一次二分查找:找到目标值的第一个出现位置
- 第二次二分查找:找到目标值的最后一个出现位置
如果第一次查找返回-1,说明目标值不存在,可以直接返回[-1,-1]。
3.2 查找第一个位置
为了找到第一个等于目标值的位置,我们需要修改二分查找的条件:
- 当nums[mid]等于target时,不能直接返回,需要继续向左搜索
- 只有当nums[mid]等于target且左边没有相同的值时,才是第一个位置
实现代码:
python复制def find_first(nums, target):
left, right = 0, len(nums) - 1
first = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
if nums[mid] == target:
first = mid
return first
3.3 查找最后一个位置
类似地,为了找到最后一个等于目标值的位置:
- 当nums[mid]等于target时,不能直接返回,需要继续向右搜索
- 只有当nums[mid]等于target且右边没有相同的值时,才是最后一个位置
实现代码:
python复制def find_last(nums, target):
left, right = 0, len(nums) - 1
last = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
if nums[mid] == target:
last = mid
return last
4. 完整解决方案实现
4.1 合并两个查找函数
将上述两个查找函数合并,得到完整的解决方案:
python复制class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def find_first(nums, target):
left, right = 0, len(nums) - 1
first = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
if nums[mid] == target:
first = mid
return first
def find_last(nums, target):
left, right = 0, len(nums) - 1
last = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
if nums[mid] == target:
last = mid
return last
first = find_first(nums, target)
if first == -1:
return [-1, -1]
last = find_last(nums, target)
return [first, last]
4.2 代码优化
我们可以进一步优化代码,减少重复计算:
python复制class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def binary_search(nums, target, find_first):
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
result = mid
if find_first:
right = mid - 1 # 继续向左找第一个
else:
left = mid + 1 # 继续向右找最后一个
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
first = binary_search(nums, target, True)
if first == -1:
return [-1, -1]
last = binary_search(nums, target, False)
return [first, last]
5. 边界条件与测试用例
5.1 常见边界情况
- 空数组:nums = [], target = 0 → [-1, -1]
- 单个元素匹配:nums = [1], target = 1 → [0, 0]
- 单个元素不匹配:nums = [1], target = 2 → [-1, -1]
- 所有元素相同:nums = [2,2,2], target = 2 → [0, 2]
- 目标值在开头:nums = [1,2,3,4], target = 1 → [0, 0]
- 目标值在结尾:nums = [1,2,3,4], target = 4 → [3, 3]
- 目标值不存在但位于范围内:nums = [1,3,5], target = 2 → [-1, -1]
- 目标值小于所有元素:nums = [1,3,5], target = 0 → [-1, -1]
- 目标值大于所有元素:nums = [1,3,5], target = 6 → [-1, -1]
5.2 测试代码示例
python复制def test_searchRange():
solution = Solution()
# 测试用例1
assert solution.searchRange([5,7,7,8,8,10], 8) == [3,4]
# 测试用例2
assert solution.searchRange([5,7,7,8,8,10], 6) == [-1,-1]
# 测试用例3
assert solution.searchRange([], 0) == [-1,-1]
# 测试用例4
assert solution.searchRange([1], 1) == [0,0]
# 测试用例5
assert solution.searchRange([2,2,2], 2) == [0,2]
print("所有测试用例通过!")
6. 算法复杂度分析
6.1 时间复杂度
我们进行了两次二分查找,每次的时间复杂度都是O(log n),因此总时间复杂度为:
O(log n) + O(log n) = O(log n)
这满足了题目要求的时间复杂度。
6.2 空间复杂度
我们只使用了常数级别的额外空间(几个变量),因此空间复杂度为O(1)。
7. 常见错误与调试技巧
7.1 常见实现错误
- 在查找第一个位置时,没有正确处理nums[mid] == target的情况,导致返回的位置不是第一个
- 在查找最后一个位置时,没有正确处理nums[mid] == target的情况,导致返回的位置不是最后一个
- 边界条件处理不当,如空数组或单个元素数组
- 循环条件错误,导致无限循环或提前退出
7.2 调试技巧
- 使用print语句输出中间变量值:
python复制print(f"left={left}, right={right}, mid={mid}, nums[mid]={nums[mid]}")
- 针对特定测试用例进行调试,如所有元素相同的情况
- 使用小数组手动模拟算法执行过程
- 检查循环不变式是否正确保持
8. 实际应用与扩展
8.1 实际应用场景
这种查找元素范围的算法在实际中有多种应用:
- 日志分析:查找特定时间范围内的事件
- 数据库查询:查找某个值在索引中的位置范围
- 统计计算:计算某个值在排序数据集中的出现频率
8.2 算法扩展
这个算法可以扩展解决类似问题:
- 查找小于目标值的最后一个元素
- 查找大于目标值的第一个元素
- 统计目标值在数组中出现的次数(通过找到范围后计算)
例如,统计目标值出现次数:
python复制def count_occurrences(nums, target):
first = find_first(nums, target)
if first == -1:
return 0
last = find_last(nums, target)
return last - first + 1
9. 性能优化与替代方案
9.1 性能优化
虽然我们的算法已经是O(log n)时间复杂度,但还可以进行一些微优化:
- 如果第一次查找返回-1,可以立即返回[-1,-1],避免第二次查找
- 在某些情况下,可以尝试在一次二分查找中同时记录第一个和最后一个位置
9.2 替代方案
除了两次二分查找的方案,还可以考虑以下方法:
- 先找到任意一个匹配位置,然后向两边扩展找到边界
- 最坏情况下时间复杂度为O(n),不符合题目要求
- 使用标准库函数:
- Python中可以使用bisect模块的bisect_left和bisect_right
- 但面试中通常要求自己实现
Python标准库实现示例:
python复制import bisect
def searchRange(nums, target):
left = bisect.bisect_left(nums, target)
if left == len(nums) or nums[left] != target:
return [-1, -1]
right = bisect.bisect_right(nums, target) - 1
return [left, right]
10. 面试技巧与注意事项
10.1 面试常见问题
面试官可能会问:
- 为什么选择二分查找来解决这个问题?
- 如何处理目标值不存在的情况?
- 如何证明算法的时间复杂度是O(log n)?
- 能否在一次二分查找中同时找到第一个和最后一个位置?
10.2 回答建议
- 强调排序数组和O(log n)时间复杂度要求自然指向二分查找
- 解释清楚边界条件的处理逻辑
- 准备手写代码,确保代码清晰可读
- 讨论算法的时间和空间复杂度
- 准备测试用例,展示全面的思考
10.3 编码风格建议
- 使用有意义的变量名(如first_pos而不是简单的res)
- 添加必要的注释,特别是对于二分查找的条件判断
- 保持代码简洁,但不要牺牲可读性
- 先写出清晰的代码,再考虑优化
在实际面试中,我通常会先写出基本的两次二分查找方案,确保正确性后再讨论可能的优化。这样既展示了解决问题的能力,又体现了对代码质量的关注。
