题目描述
704. 二分查找
难度:⭐ 简单
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
要求:算法时间复杂度为 O(log n) 。
示例
输出: 4
解释: 9 出现在 nums 中并且下标为 4
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示
- 数组中的元素不重复
- 数组长度
n在[1, 10000]之间 - 每个元素在
[-9999, 9999]之间
解题思路
题目明确要求 O(log n) 的时间复杂度,看到这个复杂度,几乎立刻就能想到二分查找。
二分查找的核心思想是:
- 在有序数组中,每次取中间位置的元素与目标值比较。
- 如果中间元素等于目标值,则直接返回下标。
- 如果中间元素大于目标值,说明目标值在左半部分,缩小右边界。
- 如果中间元素小于目标值,说明目标值在右半部分,缩小左边界。
- 重复这个过程,直到找到目标值或区间无效。
关键在于区间的定义,是左闭右闭 [left, right] 还是左闭右开 [left, right)。
这里我们采用最常用的左闭右闭区间,保证每次搜索都在有效的索引范围内。
边界处理细节
-
初始化
left = 0,right = nums.length - 1(因为右边界是包含的)。 -
循环条件
while (left <= right):当left > right时,说明区间内没有元素,查找失败。 -
计算中间值:
mid = left + Math.floor((right - left) / 2),避免大数溢出(虽然 JS 中不常见,但习惯这样写更安全)。 -
根据
nums[mid]与target的大小关系调整left或right:nums[mid] > target→right = mid - 1nums[mid] < target→left = mid + 1- 相等 → 返回
mid
-
循环结束未找到,返回
-1。
代码实现
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
// 防止 left + right 溢出(虽然 JS 数字范围很大,但这是良好习惯)
let mid = Math.floor(left + (right - left) / 2);
if (nums[mid] > target) {
right = mid - 1; // 目标在左半边
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半边
} else {
return mid; // 找到目标
}
}
return -1; // 未找到
};
复杂度分析
- 时间复杂度:O(log n)
每一次循环都将搜索范围缩小一半,因此最多需要 log₂(n) 次比较。 - 空间复杂度:O(1)
只使用了常数个变量,没有额外数组或递归栈。
常见错误与避坑指南
- 循环条件写错
如果使用while (left < right)配合闭区间,会漏掉left == right时的情况,导致某些边界元素被跳过。 - mid 更新写法
let mid = Math.floor((left + right) / 2)在 left 和 right 很大时可能有整数溢出风险(其他语言),推荐left + (right - left) / 2。 - 边界移动错误
当nums[mid] > target时,应该让right = mid - 1而不是right = mid,否则可能死循环(因为 mid 已被排除)。 - 没有处理数组为空的情况
题目保证了n >= 1,但实际工程中建议增加if (!nums.length) return -1。
总结
二分查找虽然基础,但它是许多高级算法(如二叉搜索树、快速排序、查找旋转排序数组等)的基石。
只要记住 区间不变量(这里我们保证 [left, right] 始终是待搜索区间),并在更新边界时保持这个不变性,就能写出正确的二分查找。
这道题也提醒我们:简单并不意味着可以掉以轻心,边界条件永远是二分查找的灵魂。
你学会了吗? 不妨自己手动模拟一遍示例的查找过程,或者在本地写几个测试用例加深理解。如果觉得文章有帮助,欢迎点赞、收藏、评论交流~ 😊