1. 问题背景与需求分析
在算法面试和日常编程中,二分查找是最基础也最容易被低估的算法之一。LeetCode第34题"在排序数组中查找元素的第一个和最后一个位置"看似简单,实则考察了对二分查找的深刻理解和灵活应用能力。
这道题的典型应用场景包括:
- 日志系统中按时间戳范围检索日志条目
- 电商平台筛选某价格区间的商品
- 数据库查询优化中的范围索引查找
给定一个按照升序排列的整数数组nums和一个目标值target,要求:
- 如果target存在于数组中,返回其第一次和最后一次出现的位置
- 如果target不存在于数组中,返回[-1, -1]
- 算法时间复杂度必须为O(log n)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二分查找的核心思想与变种
2.1 标准二分查找的局限性
传统二分查找在找到目标值后立即返回,无法处理重复元素的情况。例如在数组[5,7,7,8,8,10]中查找8,标准二分可能返回索引3或4,但我们需要的是[3,4]。
2.2 查找边界的两种策略
解决这个问题需要实现两种变种的二分查找:
- 查找左边界:找到第一个等于target的元素
- 查找右边界:找到最后一个等于target的元素
关键区别在于当nums[mid] == target时的处理:
- 找左边界时继续向左搜索(right = mid - 1)
- 找右边界时继续向右搜索(left = mid + 1)
3. C语言实现详解
3.1 函数接口设计
c复制/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
*returnSize = 2;
int* result = (int*)malloc(2 * sizeof(int));
result[0] = findFirst(nums, numsSize, target);
result[1] = findLast(nums, numsSize, target);
return result;
}
3.2 查找左边界实现
c复制int findFirst(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
if (nums[mid] == target) res = mid;
} else {
left = mid + 1;
}
}
return res;
}
关键点解析:
- 当nums[mid] == target时,记录位置并继续向左搜索
- 使用left + (right - left)/2避免整数溢出
- 循环条件为left <= right确保处理单元素情况
3.3 查找右边界实现
c复制int findLast(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
if (nums[mid] == target) res = mid;
} else {
right = mid - 1;
}
}
return res;
}
与左边界查找的对称性:
- 当nums[mid] == target时,记录位置并继续向右搜索
- 其他逻辑与左边界查找形成镜像关系
4. 边界条件与测试用例
4.1 常见边界情况
- 空数组输入:nums = [], target = 0 → [-1, -1]
- 单元素匹配:nums = [1], target = 1 → [0, 0]
- 全数组相同:nums = [2,2,2], target = 2 → [0, 2]
- 目标值不存在:nums = [1,3,5], target = 2 → [-1, -1]
- 目标值在两端:nums = [1,2,3,3,4], target = 1 → [0, 0]
4.2 测试驱动开发建议
建议在实现时先写出测试用例:
c复制void test_searchRange() {
int nums1[] = {5,7,7,8,8,10};
int size1 = 6;
int returnSize;
int* result = searchRange(nums1, size1, 8, &returnSize);
assert(result[0] == 3 && result[1] == 4);
free(result);
// 添加更多测试用例...
}
5. 算法优化与变种
5.1 合并查找过程
可以编写一个统一的二分查找函数,通过参数控制查找左边界还是右边界:
c复制int binarySearch(int* nums, int numsSize, int target, bool findLeft) {
int left = 0, right = numsSize - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
res = mid;
if (findLeft) {
right = mid - 1;
} else {
left = mid + 1;
}
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
5.2 时间复杂度分析
两种实现方式的时间复杂度均为O(log n):
- 标准二分查找:每次迭代将搜索范围减半
- 边界查找:最坏情况下需要两次完整的二分查找
空间复杂度均为O(1),除了返回结果需要的固定空间。
6. 实际工程中的注意事项
- 内存管理:LeetCode接口要求返回malloc分配的内存,调用者负责释放
- 大数处理:使用left + (right - left)/2而非(left + right)/2避免溢出
- API设计:良好的函数拆分提高代码可读性和复用性
- 错误处理:实际工程中需要处理NULL指针等异常情况
7. 二分查找的常见误区
- 循环条件错误:使用while(left < right)可能漏掉单元素情况
- 边界更新错误:left = mid导致死循环(应用left = mid + 1)
- 中间值计算溢出:直接相加可能导致整数溢出
- 返回值处理:未找到时应返回特定值而非随机内存内容
8. 扩展应用场景
掌握这种边界查找技巧后,可以解决一系列变种问题:
- 查找插入位置(LeetCode 35)
- 在旋转排序数组中搜索(LeetCode 33)
- 寻找峰值元素(LeetCode 162)
- 寻找重复数(LeetCode 287)
在数据库索引、文件系统检索、网络路由表等实际系统中,二分查找的变种应用广泛。理解其本质后,可以灵活应对各种查找需求。
