1. 问题分析与算法选型
遇到在有序数组中查找目标值范围的问题,第一反应就是二分查找。这道题的特殊之处在于需要找到目标值的起始和结束位置,而不仅仅是判断是否存在。我们先拆解题目要求:
- 输入:非递减排列的整数数组(可能存在重复元素)
- 输出:目标值的第一个和最后一个位置索引(从0开始计数)
- 边界情况:数组为空或目标值不存在时返回[-1, -1]
- 时间复杂度要求:O(log n)
常规的二分查找在找到目标值后会立即返回,但这里需要继续搜索边界。我想到可以通过改造二分查找来实现:
- 先找到第一个等于target的位置
- 再找到最后一个等于target的位置
- 两次查找都可以用二分法实现
这种分两次查找的思路,时间复杂度是2*O(log n),依然满足题目要求。相比遍历数组的O(n)解法,效率提升明显。
2. 算法实现细节
2.1 查找第一个位置
查找第一个等于target的位置时,当nums[mid] == target,我们不能直接返回,而应该继续在左半部分查找:
python复制def findFirst(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
关键点:
- 当nums[mid] == target时,记录位置但不终止循环
- 继续向左查找(right = mid - 1)看是否有更早出现的target
2.2 查找最后一个位置
查找最后一个位置时,当nums[mid] == target,我们需要继续在右半部分查找:
python复制def findLast(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
关键点:
- 当nums[mid] == target时,记录位置但不终止循环
- 继续向右查找(left = mid + 1)看是否有更晚出现的target
2.3 完整解决方案
将两个查找函数组合起来:
python复制def searchRange(nums, target):
first = findFirst(nums, target)
if first == -1:
return [-1, -1]
last = findLast(nums, target)
return [first, last]
3. 边界情况处理
实际编码时需要特别注意以下几种边界情况:
- 空数组输入:直接返回[-1, -1]
- 目标值不存在:第一次查找就会返回-1
- 目标值只有一个:first和last会相同
- 目标值全部相同:如nums=[2,2,2], target=2
- 目标值在数组两端:如nums=[1,2,2,3], target=1或3
提示:在面试中,写完代码后要主动列举这些边界案例进行测试
4. 算法复杂度分析
- 时间复杂度:两次二分查找,每次O(log n),总体O(log n)
- 空间复杂度:只使用了常数空间,O(1)
相比线性扫描的O(n)解法,二分法在大数据量时优势明显。例如当n=1,000,000时,log2(n)≈20,只需约40次比较就能确定范围。
5. 实际编码中的优化技巧
5.1 合并两个查找函数
可以将first和last查找合并到一个函数中,通过参数控制查找方向:
python复制def findBound(nums, target, isFirst):
left, right = 0, len(nums) - 1
bound = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
bound = mid
if isFirst:
right = mid - 1
else:
left = mid + 1
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return bound
5.2 提前终止条件
当查找第一个位置返回-1时,可以直接返回[-1, -1],无需进行第二次查找:
python复制def searchRange(nums, target):
first = findBound(nums, target, True)
if first == -1:
return [-1, -1]
last = findBound(nums, target, False)
return [first, last]
5.3 避免整数溢出
计算mid时使用left + (right - left) // 2而非(left + right) // 2,可以防止大数相加导致的溢出。
6. 常见错误与调试技巧
6.1 死循环问题
二分查找容易出现死循环,特别是在处理边界时。确保每次迭代区间都在缩小:
- 检查left和right的更新是否至少移动1位
- 终值条件是否包含left == right的情况
6.2 漏判相等情况
在nums[mid] == target时,除了记录位置外,还要继续搜索可能存在的更早/更晚出现的位置。
6.3 测试用例设计
建议测试以下典型场景:
- 常规情况:nums = [1,2,2,2,3], target = 2
- 目标值在两端:nums = [2,2,3,4], target = 2
- 目标值不存在:nums = [1,3,5], target = 2
- 空数组:nums = [], target = 1
- 全数组相同:nums = [2,2,2], target = 2
7. 算法扩展思考
这个问题可以延伸出几个变种:
- 统计目标值出现次数:last - first + 1
- 查找第一个大于/小于目标值的位置
- 在旋转有序数组中查找目标值范围
理解这个问题的解法后,这些变种都能迎刃而解。例如统计出现次数:
python复制def countTarget(nums, target):
first = findFirst(nums, target)
if first == -1:
return 0
last = findLast(nums, target)
return last - first + 1
在实际工程中,这种查找范围的算法常用于日志时间范围查询、数据库索引检索等场景。掌握二分查找的各种变体,是算法工程师的基本功。