1. 问题背景与核心挑战
在算法面试和实际工程开发中,处理有序数据是最基础也最频繁遇到的任务之一。给定一个按照升序排列的整数数组和一个目标值,要求找出该目标值在数组中的开始位置和结束位置,这就是经典的"在排序数组中查找元素的第一个和最后一个位置"问题(LeetCode第34题)。
这个问题看似简单,但隐藏着几个关键挑战:
- 当目标值不存在时,需要返回[-1, -1]
- 算法时间复杂度必须优于O(n),即不能简单地遍历整个数组
- 需要处理数组中存在大量重复目标值的情况
- 边界条件处理(如目标值小于最小值或大于最大值)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
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),当数组很大时(比如百万级数据),效率会非常低下。
2.2 二分查找的优势
对于有序数组,二分查找(Binary Search)是更优的选择,它可以将时间复杂度降低到O(log n)。标准的二分查找可以快速定位目标值,但对于存在重复值的情况需要特殊处理。
3. 改进的二分查找实现
3.1 查找第一个位置
要找到目标值的第一个出现位置,我们需要对标准二分查找进行修改:
- 当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:
first = mid
right = mid - 1 # 继续向左搜索
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return first
3.2 查找最后一个位置
类似地,查找最后一个位置需要向右搜索:
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:
last = mid
left = mid + 1 # 继续向右搜索
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return last
3.3 完整解决方案
将两个辅助函数组合起来:
python复制def searchRange(nums, target):
return [findFirst(nums, target), findLast(nums, target)]
4. 边界条件与异常处理
4.1 空数组处理
当输入数组为空时,直接返回[-1, -1]
4.2 目标值不存在
当findFirst返回-1时,可以直接返回[-1, -1],无需调用findLast
4.3 单元素数组
当数组只有一个元素时,需要特殊判断该元素是否等于目标值
4.4 全相同元素数组
如nums = [2,2,2,2], target = 2,需要正确返回[0, 3]
5. 算法复杂度分析
- 时间复杂度:O(log n),因为两次二分查找各自需要O(log n)时间
- 空间复杂度:O(1),只使用了常数级别的额外空间
6. 实际应用场景
这种算法在以下场景中非常有用:
- 日志系统中查找特定时间范围内的事件
- 数据库索引中定位某个值的所有记录
- 基因组数据中查找特定序列的位置
- 电商系统中查询某价格区间的商品
7. 优化技巧与注意事项
7.1 避免整数溢出
计算mid时使用left + (right - left) // 2而非(left + right) // 2,可以防止大数相加导致的溢出。
7.2 提前终止条件
当发现nums[left] > target或nums[right] < target时,可以提前返回[-1, -1]
7.3 测试用例设计
完善的测试应该包括:
- 常规情况(目标值存在且不重复)
- 目标值不存在
- 目标值是数组最小值/最大值
- 全相同元素数组
- 空数组
- 单元素数组
8. 其他语言实现示例
8.1 Java实现
java复制class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = new int[2];
result[0] = findFirst(nums, target);
result[1] = findLast(nums, target);
return result;
}
private int findFirst(int[] nums, int target) {
int idx = -1;
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
if (nums[mid] == target) idx = mid;
}
return idx;
}
private int findLast(int[] nums, int target) {
int idx = -1;
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
if (nums[mid] == target) idx = mid;
}
return idx;
}
}
8.2 C++实现
cpp复制class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
return {findFirst(nums, target), findLast(nums, target)};
}
int findFirst(vector<int>& nums, int target) {
int idx = -1;
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
if (nums[mid] == target) idx = mid;
}
return idx;
}
int findLast(vector<int>& nums, int target) {
int idx = -1;
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
if (nums[mid] == target) idx = mid;
}
return idx;
}
};
9. 常见错误与调试技巧
9.1 死循环问题
当left和right相邻时,如果更新条件不当可能导致死循环。确保每次迭代都能缩小搜索范围。
9.2 边界检查遗漏
容易忘记检查数组为空或目标值不在数组范围内的情况。
9.3 返回值错误
在辅助函数中,需要确保返回的是最后一次匹配的位置,而不是循环结束时的left或right。
9.4 调试建议
可以在循环中添加打印语句,输出每次迭代的left、right和mid值,帮助理解搜索过程。
10. 算法变种与扩展
10.1 查找小于目标值的最大数
修改二分查找条件,可以找到数组中比目标值小的最大数。
10.2 查找大于目标值的最小数
类似地,可以找到比目标值大的最小数。
10.3 统计目标值出现次数
找到第一个和最后一个位置后,出现次数即为last - first + 1(当存在时)。
10.4 模糊匹配
可以扩展算法来处理近似匹配的情况,如查找与目标值相差不超过k的所有元素。
