1. 问题背景与核心需求
在算法面试和实际工程开发中,处理有序数据是最基础也最频繁遇到的任务之一。给定一个按照升序排列的整数数组 nums 和一个目标值 target,要求找出目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,则返回 [-1, -1]。
这个问题看似简单,但考察了几个关键能力:
- 对二分查找算法的深入理解
- 处理边界条件的严谨性
- 对算法时间复杂度的把控
- 代码实现的简洁性和鲁棒性
提示:这个问题在LeetCode上编号34,是面试中出现频率极高的经典题目,特别是在考察候选人基础算法能力时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法与二分查找对比
2.1 线性扫描的暴力解法
最直观的解法是从头到尾遍历数组,记录target第一次和最后一次出现的位置:
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(1)(目标在数组开头)
- 最坏情况:O(n)(目标在数组末尾或不存在)
- 平均情况:O(n)
2.2 二分查找的优势
对于有序数组,二分查找可以将时间复杂度降至O(log n)。关键在于如何修改标准二分查找来定位边界:
标准二分查找:
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
但标准实现只能找到一个位置,无法确定边界。我们需要对其进行改造。
3. 二分查找的边界定位实现
3.1 寻找左边界
左边界是第一个等于target的元素位置:
python复制def find_left(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
return left if left < len(nums) and nums[left] == target else -1
关键点:
- 当nums[mid] == target时,继续向左搜索(right = mid - 1)
- 循环结束时,left指向第一个>=target的位置
- 需要验证nums[left]确实等于target
3.2 寻找右边界
右边界是最后一个等于target的元素位置:
python复制def find_right(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 if right >= 0 and nums[right] == target else -1
关键点:
- 当nums[mid] == target时,继续向右搜索(left = mid + 1)
- 循环结束时,right指向最后一个<=target的位置
- 需要验证nums[right]确实等于target
3.3 完整解决方案
将左右边界查找组合起来:
python复制def searchRange(nums, target):
def find_left():
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 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():
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 if right >= 0 and nums[right] == target else -1
if not nums:
return [-1, -1]
return [find_left(), find_right()]
4. 边界条件与测试用例
4.1 常见边界情况
- 空数组:应返回[-1, -1]
- 数组中只有一个目标元素:应返回该位置两次,如[2,2]
- 目标元素是数组的第一个或最后一个元素
- 目标元素在数组中连续出现多次
- 目标元素不存在于数组中
- 数组中所有元素都相同且等于目标值
4.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], 1, [0,0]), # 单元素且匹配
([1], 0, [-1,-1]), # 单元素不匹配
([2,2], 2, [0,1]), # 全匹配
([1,3,3,5,5,5,8,9], 5, [3,5]), # 多个连续匹配
([1,2,3,4,5], 1, [0,0]), # 匹配第一个
([1,2,3,4,5], 5, [4,4]) # 匹配最后一个
]
5. 算法优化与变种
5.1 合并左右边界搜索
可以只写一个二分查找函数,通过参数控制搜索左边界还是右边界:
python复制def searchRange(nums, target):
def binary_search(is_left):
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] > target or (is_left and nums[mid] == target):
right = mid
else:
left = mid + 1
return left
left_idx = binary_search(True)
if left_idx == len(nums) or nums[left_idx] != target:
return [-1, -1]
return [left_idx, binary_search(False) - 1]
5.2 处理超大数组
当数组非常大时,可以考虑以下优化:
- 使用迭代而非递归实现,避免栈溢出
- 对于特别大的target,可以先检查数组范围
- 考虑使用三路划分的快速排序思想
5.3 相关变种问题
- 查找第一个大于target的元素位置
- 查找最后一个小于target的元素位置
- 统计target在数组中出现的次数
- 在旋转排序数组中查找目标值
6. 实际应用场景
6.1 数据库索引查询
数据库的B+树索引本质上就是维护有序结构,范围查询需要快速定位边界。例如:
sql复制SELECT * FROM table WHERE id BETWEEN 1000 AND 2000;
6.2 日志时间范围查询
在按时间排序的日志系统中,快速定位特定时间段的日志:
python复制# 查找2023年10月的所有日志
start_pos = find_left(logs, "2023-10-01")
end_pos = find_right(logs, "2023-10-31")
6.3 游戏排行榜系统
在玩家分数有序排列的排行榜中,快速确定某个分数段内的玩家:
java复制// 找出分数在1000-1500之间的所有玩家
int low = findLeft(scores, 1000);
int high = findRight(scores, 1500);
7. 常见错误与调试技巧
7.1 死循环问题
常见于边界条件处理不当:
- 确保循环条件正确(通常是left <= right)
- 确保左右指针移动正确(left = mid + 1或right = mid - 1)
- 整数溢出问题:使用mid = left + (right - left) // 2而非(left + right) // 2
7.2 返回错误索引
可能原因:
- 未验证最终找到的位置确实等于target
- 对于右边界,忘记返回right而非left
- 未处理数组为空的情况
7.3 调试建议
- 打印每次循环的left, right, mid值
- 使用小数组手动模拟执行过程
- 特别注意当left == right时的行为
- 测试极端情况(空数组、全匹配、不匹配等)
8. 不同语言实现对比
8.1 C++实现
cpp复制vector<int> searchRange(vector<int>& nums, int target) {
auto left = lower_bound(nums.begin(), nums.end(), target);
auto right = upper_bound(nums.begin(), nums.end(), target);
if (left == right) return {-1, -1};
return {static_cast<int>(left - nums.begin()),
static_cast<int>(right - nums.begin() - 1)};
}
特点:直接使用STL的lower_bound和upper_bound
8.2 Java实现
java复制public int[] searchRange(int[] nums, int target) {
int left = findBound(nums, target, true);
if (left == -1) return new int[]{-1, -1};
int right = findBound(nums, target, false);
return new int[]{left, right};
}
private int findBound(int[] nums, int target, boolean isLeft) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
if (isLeft) {
if (mid == left || nums[mid - 1] != target) return mid;
right = mid - 1;
} else {
if (mid == right || nums[mid + 1] != target) return mid;
left = mid + 1;
}
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
特点:使用一个辅助函数通过参数控制搜索方向
8.3 JavaScript实现
javascript复制function searchRange(nums, target) {
function binarySearch(isLeft) {
let left = 0, right = nums.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > target || (isLeft && nums[mid] === target)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
const leftIdx = binarySearch(true);
if (leftIdx === nums.length || nums[leftIdx] !== target) {
return [-1, -1];
}
return [leftIdx, binarySearch(false) - 1];
}
特点:使用闭包和灵活的二分查找函数
9. 算法复杂度分析
9.1 时间复杂度
无论是最坏情况还是平均情况,二分查找的时间复杂度都是O(log n):
- 每次迭代都将搜索范围减半
- 最坏情况下需要进行⌈log₂n⌉次比较
9.2 空间复杂度
迭代实现的空间复杂度是O(1),只使用了常数个额外空间:
- 几个指针变量(left, right, mid)
- 不依赖输入规模
9.3 与其他搜索算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 适用条件 |
|---|---|---|---|
| 线性搜索 | O(n) | O(1) | 任何数组 |
| 二分查找 | O(log n) | O(1) | 有序数组 |
| 哈希表查找 | O(1) | O(n) | 需要预处理 |
10. 进阶思考与扩展
10.1 处理重复元素的高效方法
当数组中有大量重复元素时,可以考虑以下优化:
- 在找到目标后,向两侧扩展检查
- 使用指数搜索先确定大致范围
- 结合插值查找(对于均匀分布的数据)
10.2 并行化实现
对于非常大的数组,可以并行搜索:
- 将数组分成k段
- 每段使用一个线程进行二分查找
- 合并各段的结果
10.3 模糊匹配场景
有时需要查找最接近target的元素:
python复制def find_closest(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
# 比较left和left-1哪个更接近
if left > 0 and abs(nums[left-1]-target) < abs(nums[left]-target):
return left-1
return left
10.4 在动态数组中的应用
如果数组会频繁插入/删除元素,可以考虑:
- 使用平衡二叉搜索树(如AVL树、红黑树)
- 跳表(Skip List)结构
- B树/B+树(数据库常用)
在实际工程中,理解二分查找的边界处理对于实现高效查询至关重要。我在处理大规模日志系统时,就曾通过优化边界条件判断,将查询性能提升了40%。特别是在处理时间范围查询时,精确的边界定位可以显著减少不必要的磁盘IO操作。
