正常实现

Input : [1,2,3,4,5]
key : 3
return the index : 2
public int binarySearch(int[] nums, int key) {
   
    int l = 0, h = nums.length - 1;
    while (l <= h) {
   
        int m = l + (h - l) / 2;
        if (nums[m] == key) {
   
            return m;
        } else if (nums[m] > key) {
   
            h = m - 1;
        } else {
   
            l = m + 1;
        }
    }
    return -1;
}

时间复杂度

二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。

m 计算

有两种计算中值 m 的方式:

  • m = (l + h) / 2
  • m = l + (h - l) / 2

l + h 可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是 l 和 h 都为正数,因此 h - l 不会出现加法溢出问题。所以,最好使用第二种计算法方法。

未成功查找的返回值

循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:

-1:以一个错误码表示没有查找到 key
l:将 key 插入到 nums 中的正确位置

变种

二分查找可以有很多变种,实现变种要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:

public int binarySearch(int[] nums, int key) {
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] >= key) {
            h = m;
        } else {
            l = m + 1;
        }
    }
    return l;
}

该实现和正常实现有以下不同:

  • h 的赋值表达式为 h = m
  • 循环条件为 l < h
  • 最后返回 l 而不是 -1

在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。

在 h 的赋值表达式为 h = m 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:

nums = {0, 1, 2}, key = 1
l   m   h
0   1   2  nums[m] >= key
0   0   1  nums[m] < key
1   1   1  nums[m] >= key
1   1   1  nums[m] >= key
...

当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。

1. 求开方

Leetcode-69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2

示例 2:

输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 
     由于返回类型是整数,小数部分将被舍去。

解法:

  • Java
class Solution {
   
    public int mySqrt(int x) {
   
        if (x<=1) return x;
        int l = 1, h = x;
        while (l<h) {
   
            int m = l + (h-l)/2;
            int sqrt = x/m;
            if (sqrt==m) return m;
            else if (sqrt>m) l = m+1;
            else h = m;
        }
        return l-1;
    }
}
public int mySqrt(int x) {
   
    if (x <= 1) {
   
        return x;
    }
    int l = 1, h = x;
    while (l <= h) {
   
        int mid = l + (h - l) / 2;
        int sqrt = x / mid;
        if (sqrt == mid) {
   
            return mid;
        } else if (mid > sqrt) {
   
            h = mid - 1;
        } else {
   
            l = mid + 1;
        }
    }
    return h;
}

2. 大于给定元素的最小元素

Leetcode-744. 寻找比目标字母大的最小字母

给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。

数组里字母的顺序是循环的。举个例子,如果目标字母target = ‘z’ 并且有序数组为 letters = [‘a’, ‘b’],则答案返回 ‘a’。

示例:

输入:
letters = ["c", "f", "j"]
target = "a"
输出: "c"

输入:
letters = ["c", "f", "j"]
target = "c"
输出: "f"

输入:
letters = ["c", "f", "j"]
target = "d"
输出: "f"

输入:
letters = ["c", "f", "j"]
target = "g"
输出: "j"

输入:
letters = ["c", "f", "j"]
target = "j"
输出: "c"

输入:
letters = ["c", "f", "j"]
target = "k"
输出: "c"

注:

  1. letters长度范围在[2, 10000]区间内。
  2. letters 仅由小写字母组成,最少包含两个不同的字母。
  3. 目标字母target 是一个小写字母。
  4. 如果找不到就返回第一个字符。

解法:

  • Java
class Solution {
   
    public char nextGreatestLetter(char[] letters, char target) {
   
        int l = 0, h = letters.length-1;
        while (l < h) {
   
            int m = l + (h-l)/2;
            if (letters[m] <= target) l = m+1;
            else h = m;
        }
        return target>=letters[letters.length-1]? letters[0]: letters[l];
    }
}

3. 有序数组的 Single Element

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

示例 1:

输入: [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: [3,3,7,7,10,11,11]
输出: 10
  • 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

解法

  • Java

使用位运算

class Solution {
   
    public int singleNonDuplicate(int[] nums) {
   
        for (int i=1;i<nums.length;i++) {
   
            nums[i] = nums[i-1]^nums[i];
        }
        return nums[nums.length-1];
    }
}

二分查找

要求以 O(logN) 时间复杂度进行求解,因此不能遍历数组并进行异或操作来求解,这么做的时间复杂度为 O(N)。

令 index 为 Single Element 在数组中的位置。在 index 之后,数组中原来存在的成对状态被改变。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。

从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。

因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。

class Solution {
   
    public int singleNonDuplicate(int[] nums) {
   
        int l = 0, h = nums.length-1;
        while (l < h) {
   
            int m = l + (h-l)/2;
            // 保持m一直在偶数位
            if (m%2==1) m--;
            if (nums[m]==nums[m+1]) l = m+2;
            else h = m;
        }
        return nums[l];
    }
}

4. 第一个错误的版本

Leetcode-278. 第一个错误的版本

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例:

给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。 

解法:

  • Java
public class Solution extends VersionControl {
   
    public int firstBadVersion(int n) {
   
        int l = 1, h = n;
        while (l<h) {
   
            int m = l + (h-l)/2;
            if (isBadVersion(m) == false) l = m+1;
            else h = m;
        }
        return l;
    }
}

5. 旋转数组的最小数字

Leetcode-153. 寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 1:

输入: [3,4,5,1,2]
输出: 1

示例 2:

输入: [4,5,6,7,0,1,2]
输出: 0

解法:

  • Java

中间值大于最右边的值,说明最小值点在中间值的右边

class Solution {
   
    public int findMin(int[] nums) {
   
        int l = 0, h = nums.length-1;
        while (l < h) {
   
            int m = l + (h-l)/2;
            if (nums[m]>nums[h]) l = m+1;
            else h = m;
        }
        return nums[l];
    }
}

6. 查找区间

Leetcode-34. 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

你的算法时间复杂度必须是 O(log n) 级别。

如果数组中不存在目标值,返回 [-1, -1]。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]

解法:

  • Java

可以用二分查找找出第一个位置和最后一个位置,但是寻找的方法有所不同,需要实现两个二分查找。我们将寻找 target 最后一个位置,转换成寻找 target+1 第一个位置,再往前移动一个位置。这样我们只需要实现一个二分查找代码即可。

class Solution {
   
    public int[] searchRange(int[] nums, int target) {
   
        int first = find(nums, target);
        int last = find(nums, target+1);
        if (first==nums.length || nums[first]!=target) return new int[]{
   -1, -1};
        return new int[]{
   first, nums[last]==target? last: last-1};

    }
    // 找到第一个大于等于target的位置
    private int find(int[] nums, int target) {
   
        int l = 0, h = nums.length-1;
        while (l<h) {
   
            int m = l+(h-l)/2;
            if (nums[m]<target) l = m+1;
            else h = m;
        }
        return l;
    }
}