Skip to content

704.二分查找

难度:简单

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

解题思路

这道题目的重点是题干中说:有序数组,同时题目还强调 数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当看到题目描述满足如上条件的时候,就要想一想是不是可以用二分法了。

二分查找虽然逻辑比较简单,但难点是它涉及到很多 边界条件:例如到底是 while(left < right) 还是 while(left <= right),到底是 right = middle 呢,还是要 right = middle - 1 呢?

写二分法容易写乱,就是因为 对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在 while 寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是 循环不变量 规则。

区间的定义我选择左闭右闭即 [left, right],第一反应是用递归来写二分法,使用递归算法并不一定是在性能上是最优的,但递归能简化代码形式。

我的思路:

  1. 设定左右指针 left、right

  2. 找出中间位置 middle,并判断该位置的值是否等于 target

  3. 若 nums[middle] == target,则返回 middle 下标

    若 nums[middle] > target,则 right 指针移到 middle - 1 的位置

    若 nums[middle] < target,则 left 指针移到 middle + 1 的位置

  4. 终止条件:找到了满足需求的 middle 或者左右指针不满足 left <= right 的边界条件

我的递归代码

java
public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    return binarySearch(nums, left, right, target);
}

public int binarySearch(int[] nums, int left, int right, int target) {
    if (left > right) return -1;
    int middle = left + (right - left) / 2;
    if (nums[middle] == target) {
        return middle;
    } else if (nums[middle] < target) {
        return binarySearch(nums, middle + 1, right, target);
    } else if (nums[middle] > target) {
        return binarySearch(nums, left, middle - 1, target);
    }
}

时间复杂度:O(log n)

空间复杂度:O(log n)

递归算法的时间复杂度 = 递归的次数 * 每次递归中的操作次数

递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度

每次递归的空间复杂度可以看出主要就是参数里传入的这个 nums 数组,但需要注意的是在 C/C++/Java 中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。

也就是说每一层递归都是公用一块数组地址空间的,所以 每次递归的空间复杂度是常数即:O(1)。

再来看递归的深度,二分查找的递归深度是 log n ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * log n = O(log n)。

大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(n log n)

计算 middle 时需要防止数值溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了因为 leftright 太大,导致直接相加数值溢出的情况。

递归改成循环

java
public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    return binarySearch(nums, left, right, target);
}

public int binarySearch(int[] nums, int left, int right, int target) {
    int result = -1;
    while (left <= right) {
        int middle = left + (right - left) / 2;
        if (nums[middle] == target) {
            result = middle;
            break;
        } else if (nums[middle] < target) {
            left = middle + 1;
        } else if (nums[middle] > target)  {
            right = middle - 1;
        }
    }
    return result;
}

时间复杂度:O(log n)

空间复杂度:O(1)

总结

二分法需要注意搜索的区间和 while 的终止条件。

分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节

计算 middle 时需要防止数值溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了因为 leftright 太大,导致直接相加数值溢出的情况。

使用递归算法并不一定是在性能上是最优的,但递归能简化代码层面的复杂程度。

递归算法的时间复杂度 = 递归的次数 * 每次递归中的操作次数

递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度