二分法例题 in Java

73 阅读5分钟

常见二分查找

思路就是在利用原本序列有序的性质,将中间元素和寻找的元素进行比较,从而筛掉一半不用比较的数。 由于有序性质一直存在,即使筛到剩下两个,也可以通过比较其中一个元素得知与另一个元素的关系。

//递归写法
public int sortBin(int[] ints,int target,int start,int end){
    //常规二分法思路简单,两个指针l,r,通过target和mid比较更小mid到l或者r             //二分法可以递归,也可以while true下靠近两侧指
    if(end == start){
        return start;
    }
    int mid = start +((end-start)>>1);
    int midValue = ints[mid];
    if(target < midValue){
        return sortBin(ints, target, start, mid);
    }else if(target > midValue){
        return sortBin(ints, target, mid + 1, end);
    }else {
        return mid;
    }
}
//双指针写法
public int sortBinLR(int[] ints,int target){
    int l = 0;
    int r = ints.length-1;

    while (l<r){
        int mid = l + ((r - l) >> 1);
        if(target < mid){
            r = mid-1;
        }else if(target > mid){
            l = mid+1;
        }else {
            return mid;
        }
    }
    return l;
}

例1: 给定一个有序数组和其中一个数target,获取target出现的最左边的index

例如:{1,1,1,2,3,4,4,4,4,5,6,7,7,7,7},找出最左边的4

一般想法自然是O(N)一波遍历,但用二分法任能做到O(logN)

这个题为什么可以用二分法?二分法关键的一步是:通过对中间元素mid和目标元素target比较后能否确保一半元素没用。按照上面的例子,第一波mid = 4,可能是最左边4的也可能最左边的4在mid左边,但可以确保右边元素淘汰。 代码:

public void sortBin2(int[] ints,int target,int start,int end){
    if(end - start == 1){
        System.out.println(start);
    }
    int mid = (start + end) / 2;
    int midValue = ints[mid];
    if(target <= midValue){
        sortBin2(ints,target,start,mid);
    } else {
        sortBin2(ints,target,mid+1,end);
    }
}

例2:一个无序数组,寻找一个局部最小值,要求是时间复杂度优于O(n)

局部最小值:如果某个数左右有数的话,需要左右两边都比此数大,对于两边的数只需要小于其一头 例如: {32,31,3,5,3,2,31,5,4,7,6,4,2,24,47,3}中索引为2的3

无序怎么用二分法?这个题目的思路并非是用二分法筛掉不符合特征的数,而是筛掉不符合特征的趋势。

题目中想要局部最小值,其实就是一个凹的形状;

首先可以把凹形在两头的可能性验证一下。

除此之外,两头相邻的数一定比两头的数更小,具体是有哪些可能?

image.png

image.png

题目要求我们找到一个局部最小值,用二分法该怎么考虑? 如果我们获取到mid,能不能抛弃掉一半的数?

当然可以———假如一个函数一开始一阶导小于0,最后大于0,那么一定至少存在一个一阶导等于0的点;并且如果某个点一阶导小于0(下降),一定可以判断右侧存在一个拐点;如果某个点一阶导大于0(上升),一定可以判断左侧存在一个拐点——于是可以筛掉一半的数

public void sortBin3(int[] ints,int start,int end){
    if(end - start == 2){
        System.out.println((end+start)/2);
        return;
    }
    int mid = (start + end)/2;
    if(ints[mid-1]<ints[mid]){
        sortBin3(ints,start,mid);
    }else{
        sortBin3(ints, mid,end);
    }
}

例3:给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

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

同样的思路:由于暴力遍历的过程是有序的,可以用二分法将O(N)优化为O(logN)

此题有几点值得注意:

  1. 为了防止在运算中超过数据类型的规定范围,一般会将取中间值写为:mid = start + ((end - start)>> 1),会将乘法运算比大小变成除法运算比大小:x*x<y --> x<y/x
  2. LeetCode官方提供了一种题解:通过算数变化简化了运算逻辑:

image.png

优雅,太优雅了

public int sqrtD(int x, int start, int end) {  //end = x 包括最后一位
    if (end - start <= 1) {
        return end * end == x ? end : start;
    }
    int mid = start + ((end - start) >> 1);
    int ans;
    if (x / mid < mid) {
        ans = sqrtD(x, start, mid);
    } else if (x / mid > mid) {
        ans = sqrtD(x, mid, end);
    } else {
        ans = mid;
    }
    return ans;
}

例4:LeetCode 81

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

你必须尽可能减少整个操作步骤。

此题和LeetCode 153,154想法类似

思路:首先研究这个数组是什么样子:先增,骤减,再增;在此图像中怎么使用二分法查找?

画图很好疏通思路:

情况1:

image.png

情况2:

image.png

思路很明显了,在两种情况下,要么递归,要么用朴素二分法

但这个题目有一个问题:假如说是这两个数组 {1,1,1,1,1,mid(1),1,1,13,1,1}

{1,1,13,1,1,mid(1),1,1,1,1,1}

实际上是没法轻松判别mid左右哪个是有序的序列,此时这个方法就只能退化成群体遍历,O(N)

代码:

public boolean search(int[] nums, int target) {
    return searchD(nums, target, 0, nums.length - 1);
}

public boolean searchD(int[] nums, int target, int start, int end) {
    if (end - start <= 1) {
        return nums[start] == target || nums[end] == target;
    }

    int mid = start + ((end - start) >> 1);

    if (nums[mid] > nums[nums.length - 1]) {  //mid在前半截
        if (target < nums[mid]) {
            return searchBin(nums, target, start, mid);
        } else if (target == nums[mid]) {
            return true;
        } else {
            return searchD(nums, target, mid, end);
        }
    } else {
        if (target > nums[mid]) {
            return searchBin(nums, target, mid, end);
        } else if (target == nums[mid]) {
            return true;
        } else {
            return searchD(nums, target, start, mid);
        }
    }
}

public boolean searchBin(int[] ints, int target, int start, int end) {  //包括end

    if (start == end) {
        return target == ints[start];
    }

    int mid = start + ((end - start) >> 1);
    if (target < ints[mid]) {
        return searchBin(ints, target, start, mid);
    } else if (target > ints[mid]) {
        return searchBin(ints, target, mid + 1, end);
    } else {
        return true;
    }
}

待续...