数据结构和算法(六)——二分法

588 阅读6分钟

二分查找

普通二分查找

二分查找

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


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

算法思路:

初始化左指针 left = 0, 右指针 right = nums.length - 1。 则其中点 pivot = left + ((right - left)>>1).

如果 target = nums[pivot],则找到了目标元素,返回 pivot。

如果 target < nums[pivot],由于在顺序数组中,则target不可能在pivot的右侧,则在左侧继续搜索 right = pivot - 1。

如果 target > nums[pivot],由于在顺序数组中,则target不可能在pivot的左侧,则在右侧继续搜索 left = pivot + 1。

代码如下:

class Solution {
  public int search(int[] nums, int target) {
    int pivot, left = 0, right = nums.length - 1;
    while (left <= right) {
      pivot = left + (right - left) / 2;
      if (nums[pivot] == target) return pivot;
      if (target < nums[pivot]) right = pivot - 1;
      else left = pivot + 1;
    }
    return -1;
  }
}

由于每次上述都将数组的长度变为原来的一半,则其时间复杂度为O(logn)

模糊二分查找

查找第一个等于给定值的元素的位置

代码如下:

private static int search(int[] data,int value){
        if (data == null)return -1;
        if(data.length <= 0)return -1;
        if (data.length == 1){
            if(data[0] == value)return 0;
            else return -1;
        }
        int start = 0;
        int end = data.length - 1;
        while (end >= start){
            int middle = (end - start)/2 + start;
            if(data[middle] == value){
                if(middle == 0)return middle;
                if(data[middle - 1] != value)return middle;
                end = middle - 1;
            }else if(data[middle] > value){
                end = middle - 1;
            }else if(data[middle] < value){
                start = middle + 1;
            }
        }
        return -1;
    }

查找最后一个等于指定元素的值的位置

代码如下:

private static int search(int[] data,int value){
        if(data == null)return -1;
        if(data.length <= 0)return -1;
        if (data.length == 1){
            if(data[0] == value)return 0;
            else return -1;
        }
        int start = 0;
        int end = data.length - 1;
        while (end >= start){
            int middle = (end - start)/2+start;
            if(data[middle] == value){
                if(middle == data.length - 1)return middle;
                if(data[middle+1]!=value)return middle;
                start = middle + 1;
            }else if(data[middle] > value){
                end = middle - 1;
            }else{
                start = middle + 1;
            }
        }
        return -1;
    }

查找第一个大于等于指定值的元素的位置

代码如下:

private static int search(int[] data,int value){
      if(data == null)return -1;
      if(data.length <=0)return -1;
      if(data.length == 1){
          if(data[0] >= value)return 0;
          else return -1;
      }
      int start = 0;
      int end = data.length - 1;
      while (end >= start){
          int middle = (end - start)/2 + start;
          if(data[middle] >= value){
              if(middle == 0)return middle;
              if(data[middle - 1] < value)return middle;
              end = middle - 1;
          }else{
              start = middle + 1;
          }
      }
      return -1;
    }

查找最后一个小于等于指定值的元素的位置

代码如下:

private static int search(int[] data,int value){
        if(data == null)return -1;
        if(data.length <= 0)return -1;
        if(data.length == 1){
            if(data[0] <= value)return 0;
            else return -1;
        }
        int start = 0;
        int end = data.length - 1;
        while (end >= start){
            int middle = (end - start)/2 + start;
            if(data[middle] <= value){
                if (middle == data.length-1)return middle;
                if(data[middle+1] > value)return middle;
                start = middle + 1;
            }else{
                end = middle - 1;
            }
        }
        return -1;
    }

二分查找的应用

x 的平方根

实现 int sqrt(int x) 函数。

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

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

示例 1:
输入: 4
输出: 2

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

思路:

先设置左边界 l ,右边界 r;其中点为 mid = (r - l)/2 + l;

当mid * mid > x 时,说明所找的的数在 mid 的左边,即 r = mid - 1; 当mid * mid < x 时,说明所找的数在 mid 右边,即 l = mid + 1,同时记录mid值,即 ans = mid,保存可能的结果;当 mid * mid == x时,直接返回。

代码如下:

class Solution {
    public int mySqrt(int x) {
        int l = 0;
        int r = x;
        int ans = -1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            long res = (long)mid * mid;
            if (res == x) {
               return mid;
            }else if(res > x){
                r = mid - 1;
            }else{
                l = mid + 1;
                ans = mid;
            }
        }
        return ans;
    }
}

0~n-1 中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:
输入: [0,1,3]
输出: 2

示例2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8

从上面的例子可以看出,二分法就是要找出划分的条件将数组一分为二。通过题目,我们可以发现,当数组的下标与数组值满足 index = nums[index] 时,其要找的数字在index右边。

代码如下:

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

旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

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

示例 2:
输入:[2,2,2,0,1]
输出:0

解析来源这里:

循环二分: 设置 i,j指针分别指向 numbers 数组左右两端,m = (i + j) // 2 为每次二分的中点("//"代表向下取整除法,因此恒有 i <= m < j),可分为以下三种情况:

numbers[m] > numbers[j]时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j]闭区间内,因此执行 i = m + 1

numbers[m] < numbers[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m] 闭区间内,因此执行 j = m

numbers[m] == numbers[j] 时: 无法判断 m 在哪个排序数组中,即无法判断旋转点 x 在 [i, m]还是 [m + 1, j]区间中。解决方案: 执行 j = j - 1缩小判断范围

代码如下:

class Solution {
    public int minArray(int[] numbers) {
        int i = 0, j = numbers.length - 1;
        while (i < j) {
            int m = (i + j) / 2;
            if (numbers[m] > numbers[j]) i = m + 1;
            else if (numbers[m] < numbers[j]) j = m;
            else j--;
        }
        return numbers[i];
    }
}

快速幂

数值的整数次方

实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

示例 1:
输入: 2.00000, 10
输出: 1024.00000

示例2:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25

可知:x^n = x^(n/2) * x^(n/2)

当n为偶数时,x^n = x^(n/2) * x^(n/2)

当n为奇数时,x^n = x*x^(n/2) * x^(n/2).

通过循环 x = x^2 操作,每次把幂从 n 降至 n/2 ,直至将幂降为 0.可以看出它的时间复杂度为O(logn)。

代码如下:

class Solution {
    public double myPow(double x, int n) {
        if(x == 0) return 0;
        long b = n;
        double res = 1.0;
        if(b < 0) {
            x = 1 / x;
            b = -b;
        }
        while(b > 0) {//b为奇数时b&1 == 1;b为偶数时b&1 == 0
            if((b & 1) == 1) res *= x;
            x *= x;
            b >>= 1;//等同于 b /= 2
        }
        return res;
    }
}

如果看不懂二分法解释可以看一下二进制的解释

二分法练习题