1. 题目背景与核心需求
这道LeetCode经典面试题要求我们在一个已排序的整数数组中,找到目标值出现的第一个和最后一个位置(下标)。如果目标值不存在于数组中,则返回[-1, -1]。题目看似简单,但考察了多个关键算法能力:
- 对二分查找算法的深入理解和灵活应用
- 边界条件处理的严谨性
- 不同实现方案的性能分析能力
- 代码实现的简洁性和可读性
在实际面试中,面试官通常会要求候选人先给出暴力解法,然后逐步优化到最优解,并分析各种解法的时间复杂度。这道题之所以经典,是因为它能全面考察候选人对基础算法的掌握程度和问题解决思路。
2. 五种实现方案详解
2.1 线性扫描法(暴力解法)
这是最直观的解法,适合作为思考的起点:
python复制def searchRange(nums, target):
start = end = -1
for i in range(len(nums)):
if nums[i] == target:
if start == -1:
start = i
end = i
return [start, end]
时间复杂度:O(n)
空间复杂度:O(1)
注意:虽然这种解法简单,但在面试中应该明确指出这不是最优解,并主动提出可以优化为O(logn)的解法。
2.2 二分查找+线性扩展
先使用二分查找找到任意一个目标值,然后向左右线性扩展:
python复制def searchRange(nums, target):
def binary_search(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
pos = binary_search(nums, target)
if pos == -1:
return [-1, -1]
left = right = pos
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]
时间复杂度:最好O(logn),最坏O(n)(当所有元素都是目标值时)
空间复杂度:O(1)
2.3 两次二分查找法(标准解法)
这是面试官期望看到的最优解法,使用两次二分查找分别定位起始和结束位置:
python复制def searchRange(nums, target):
def find_first(nums, target):
left, right = 0, len(nums)-1
res = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
if nums[mid] == target:
res = mid
return res
def find_last(nums, target):
left, right = 0, len(nums)-1
res = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
if nums[mid] == target:
res = mid
return res
return [find_first(nums, target), find_last(nums, target)]
时间复杂度:O(logn)
空间复杂度:O(1)
2.4 使用bisect模块(Python专属)
Python的标准库bisect可以简化实现:
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]
时间复杂度:O(logn)
空间复杂度:O(1)
2.5 递归实现二分查找
对于理解递归的候选人,可以展示这种实现方式:
python复制def searchRange(nums, target):
def search(left, right, find_first):
if left > right:
return -1
mid = (left + right) // 2
if nums[mid] == target:
if find_first:
if mid == left or nums[mid-1] != target:
return mid
else:
return search(left, mid-1, find_first)
else:
if mid == right or nums[mid+1] != target:
return mid
else:
return search(mid+1, right, find_first)
elif nums[mid] < target:
return search(mid+1, right, find_first)
else:
return search(left, mid-1, find_first)
first = search(0, len(nums)-1, True)
if first == -1:
return [-1, -1]
last = search(first, len(nums)-1, False)
return [first, last]
时间复杂度:O(logn)
空间复杂度:O(logn)(由于递归调用栈)
3. 关键点分析与比较
3.1 二分查找的变体理解
这道题的核心在于理解二分查找的几种变体:
- 标准二分查找:找到一个目标值即可
- bisect_left:找到第一个≥目标值的位置
- bisect_right:找到第一个>目标值的位置
在实现时,关键在于如何处理nums[mid] == target的情况:
- 找第一个位置时,继续向左搜索
- 找最后一个位置时,继续向右搜索
3.2 边界条件处理
需要特别注意的边界情况包括:
- 空数组
- 数组中不存在目标值
- 数组中所有元素都是目标值
- 目标值出现在数组开头或结尾
3.3 五种方案的比较
| 方案 | 时间复杂度 | 空间复杂度 | 适用场景 | 面试建议 |
|---|---|---|---|---|
| 线性扫描 | O(n) | O(1) | 小规模数据 | 仅作为起点 |
| 二分+线性 | O(logn)-O(n) | O(1) | 目标值较少 | 过渡方案 |
| 两次二分 | O(logn) | O(1) | 通用 | 最优解 |
| bisect | O(logn) | O(1) | Python | 展示库知识 |
| 递归二分 | O(logn) | O(logn) | 教学 | 展示递归思维 |
4. 常见错误与调试技巧
4.1 典型错误案例
- 无限循环:
python复制# 错误的mid计算导致死循环
while left < right: # 应该用<=
mid = (left + right) // 2
if nums[mid] < target:
left = mid # 应该用mid+1
else:
right = mid # 应该用mid-1
- 边界错误:
python复制# 忘记检查数组越界
while nums[left-1] == target: # 当left=0时会越界
left -= 1
- 条件判断不全:
python复制# 只判断了nums[mid] == target的情况
if nums[mid] == target:
return mid
# 缺少其他情况的处理
4.2 调试技巧
- 使用小测试案例:
- 空数组 []
- 单元素数组 [1](目标值存在和不存在)
- 全相同数组 [2,2,2]
- 边界情况 [1,2,2,3]
- 打印关键变量:
python复制print(f"left={left}, right={right}, mid={mid}, nums[mid]={nums[mid]}")
- 使用断言验证:
python复制assert searchRange([5,7,7,8,8,10], 8) == [3,4]
assert searchRange([5,7,7,8,8,10], 6) == [-1,-1]
5. 面试实战建议
- 解题步骤建议:
- 先描述暴力解法及其复杂度
- 提出优化思路(二分查找)
- 讨论二分查找的不同实现方式
- 选择最优方案实现
- 分析时间/空间复杂度
- 测试边界条件
- 沟通技巧:
- 明确问题要求(是否允许重复元素?输入是否已排序?)
- 解释每种方案的优缺点
- 主动提出测试案例
- 讨论可能的变种问题(如统计出现次数)
- 进阶问题准备:
- 如果数组中有重复元素怎么办?(本题已考虑)
- 如何统计目标值出现的次数?(end-start+1)
- 如果数组是降序排列怎么办?(调整比较逻辑)
- 如何扩展到二维矩阵搜索?
6. 性能优化与变种
6.1 工程优化技巧
- 循环展开:对于性能关键场景,可以展开内部循环
- 使用位运算:用 (left + right) >> 1 代替 // 2
- 内存局部性:确保数组连续存储
6.2 算法变种
- 统计出现次数:
python复制def countOccurrences(nums, target):
left = bisect.bisect_left(nums, target)
if left == len(nums) or nums[left] != target:
return 0
right = bisect.bisect_right(nums, target)
return right - left
- 寻找最接近的数:
python复制def findClosest(nums, target):
left, right = 0, len(nums)-1
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
# 比较left和left-1哪个更接近
if left > 0 and abs(nums[left-1]-target) < abs(nums[left]-target):
return left-1
return left
- 旋转数组中的搜索:
python复制def searchInRotated(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 判断哪一部分是有序的
if nums[left] <= nums[mid]: # 左半部分有序
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else: # 右半部分有序
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
7. 实际应用场景
二分查找及其变体在工程中有广泛应用:
- 数据库索引:B+树索引的核心查找算法
- 版本控制:查找引入bug的提交版本
- 性能监控:查找性能下降的时间点
- 科学计算:寻找方程的近似解
- 游戏开发:物理引擎中的碰撞检测优化
理解这类基础算法题的实际价值在于,它们往往是更复杂系统的基础构件。在面试中展示这种系统级的理解,能让面试官看到你的工程思维深度。
