1. 问题背景与核心挑战
这道力扣经典题目看似简单,实则暗藏多个考察点。给定一个按照非递减顺序排列的整数数组和一个目标值,要求找出目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,则返回[-1, -1]。题目明确要求算法时间复杂度必须为O(log n),这意味着暴力解法在这里完全不合格。
在实际面试中,这道题经常作为二分查找算法的进阶考核点。我遇到过不少候选人能够快速写出标准二分查找,但在处理边界条件时频频出错。问题的难点在于:当数组中存在重复元素时,如何精准定位到目标值的左右边界?这需要我们对二分查找有更深刻的理解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 标准二分查找的局限性
2.1 传统二分查找的问题
常规的二分查找在找到目标值后会立即返回,这在查找唯一元素时没有问题。但当数组包含重复元素时,这种实现无法确定找到的是第一个、最后一个还是中间某个位置的匹配项。例如在数组[5,7,7,8,8,10]中查找8,标准二分可能返回索引3或4,但我们需要的是[3,4]。
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid # 问题所在:直接返回任意匹配位置
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
2.2 边界条件的复杂性
当处理边界时,我们需要特别考虑以下几种情况:
- 目标值小于数组所有元素(应返回[-1,-1])
- 目标值大于数组所有元素(应返回[-1,-1])
- 目标值存在于数组但只有单个匹配(如[1,3,5,5,6]中找3应返回[1,1])
- 目标值存在于数组且有多个连续匹配(如[1,3,3,3,6]中找3应返回[1,3])
- 目标值不存在但位于数组元素范围内(如[1,3,5,7]中找4应返回[-1,-1])
3. 改进的二分查找实现
3.1 寻找左边界
要找到目标值的第一个出现位置,我们需要修改二分查找的逻辑,使得在找到匹配项时继续向左搜索:
python复制def find_left_bound(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] >= target:
right = mid - 1 # 即使匹配也继续向左
else:
left = mid + 1
# 检查left是否越界或未找到
if left >= len(nums) or nums[left] != target:
return -1
return left
这个实现的关键在于当nums[mid] == target时,我们不是立即返回,而是继续将right移动到mid-1,这样循环会持续向左压缩搜索空间,直到找到最左边的匹配项。
3.2 寻找右边界
类似地,我们可以修改算法来寻找最后一个匹配位置:
python复制def find_right_bound(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 - 1
# 检查right是否越界或未找到
if right < 0 or nums[right] != target:
return -1
return right
这里当nums[mid] == target时,我们将left移动到mid+1,迫使搜索向右进行,直到找到最右边的匹配项。
4. 完整解决方案
将左右边界的查找结合起来,我们得到最终解法:
python复制def searchRange(nums, target):
def find_left(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
return left if left < len(nums) and nums[left] == target else -1
def find_right(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 - 1
return right if right >= 0 and nums[right] == target else -1
left_pos = find_left(nums, target)
if left_pos == -1:
return [-1, -1]
return [left_pos, find_right(nums, target)]
这个实现的时间复杂度为O(log n),因为我们进行了两次二分查找(一次找左边界,一次找右边界),而2*O(log n)仍然是O(log n)。
5. 边界条件测试与验证
为了确保我们的解决方案在各种情况下都能正常工作,我们需要设计全面的测试用例:
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,5,6], 3, [1,1]),
([1,3,3,3,6], 3, [1,3]),
([1,3,5,7], 4, [-1,-1]),
([2,2], 2, [0,1]),
([1,2,3,4,5], 1, [0,0]),
([1,2,3,4,5], 5, [4,4])
]
for nums, target, expected in test_cases:
result = searchRange(nums, target)
assert result == expected, f"Failed: {nums}, {target}, got {result}, expected {expected}"
print("All test cases passed!")
6. 算法优化与变种
6.1 单次二分查找实现
虽然我们之前的解决方案已经满足题目要求,但有些面试官可能会问是否可以只进行一次二分查找。实际上可以通过记录候选边界来实现:
python复制def searchRange_optimized(nums, target):
left, right = 0, len(nums) - 1
first, last = -1, -1
# 查找第一个位置
while left <= right:
mid = (left + right) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
if nums[mid] == target:
first = mid
# 重置指针查找最后一个位置
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
if nums[mid] == target:
last = mid
if first != -1 and last != -1:
return [right if nums[right] == target else first,
left if left < len(nums) and nums[left] == target else last]
return [-1, -1]
6.2 处理超大数组
当数组非常大时,我们可以考虑以下优化:
- 先检查数组首尾元素,如果target不在这个范围内,直接返回[-1,-1]
- 使用迭代而非递归实现,避免栈溢出
- 对于特别大的数组,可以考虑分块处理或使用更高级的数据结构
7. 常见错误与调试技巧
7.1 死循环问题
在实现二分查找时,最常见的错误是造成死循环。这通常发生在:
- 循环条件错误(应该是left <= right而非left < right)
- 边界更新错误(应该是left = mid + 1而非left = mid)
- 整数溢出问题(在超大数组时,(left + right) // 2可能溢出)
解决方案:
python复制mid = left + (right - left) // 2 # 防止溢出
7.2 边界检查遗漏
另一个常见错误是忘记检查最终找到的位置是否确实等于target。例如:
python复制# 错误示例
return left # 可能nums[left] != target
正确的做法应该像我们之前实现的那样,在返回前验证nums[left] == target。
7.3 处理空数组
很多初学者会忘记处理空数组的情况。在我们的实现中,空数组会自然地返回[-1,-1],因为初始left=0, right=-1,循环条件不满足。
8. 实际应用场景
这种查找边界的算法在实际开发中有广泛应用:
- 日志系统中查找特定时间范围内的事件
- 数据库查询中查找某个值的范围
- 统计分析中确定某个百分位数的边界
- 游戏开发中快速定位某个分数段的玩家
例如,假设我们有一个按时间戳排序的用户登录记录,我们需要找出某个用户在某天的所有登录记录,就可以使用这种算法快速定位时间范围。
9. 扩展思考
9.1 非整数数组的处理
我们的算法同样适用于其他可比较的数据类型,如浮点数或字符串,只要数组是有序的。例如:
python复制words = ["apple", "banana", "banana", "banana", "cherry"]
target = "banana"
print(searchRange(words, target)) # 输出[1,3]
9.2 多维数据查找
对于多维数据,我们可以先按某一维排序,然后使用类似的二分查找策略。例如查找地理坐标范围内的点:
- 先按经度排序
- 使用二分查找确定经度范围
- 在经度范围内再按纬度筛选
9.3 流数据中的近似查找
对于不断流入的数据,我们可以结合二分查找和滑动窗口技术,实现近似的范围查询。这种情况下,可能需要牺牲一些精确性来换取处理速度。
