二分查找

第4章 二分查找

4.1 算法解释

二分查找也常被称为二分法或者折半查找,每次查找时通过将带查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度尾O(n)的数组,二分查找的事件复杂度尾O(log n)。

​ 举例来说,给定一个排好序的数组[3, 4, 5, 6, 7],我们希望查找4在不在这个数组内。第一次折半时考虑中位数5,因为5大于4,所以如果4存在于这个数组,那么其必定存在于 5左边这一半。于是我们的查找区间变成了[3, 4, 5]。第二次折半时考虑新的中位数4,正好是我们需要查找的数字。于是我们发现,对于一个长度为5的数组,我们只进行了2次查找。如果是遍历数组,最坏的情况则需要查找5次。

​ 我们也可以用更加数学的方式定义二分查找。给定一个在[a, b]区间内的单调函数f(x),若f(a)和f(b)正负性相反,那么必定存在一个解c,使得f(c)=0。在上个例子中,f(x)是离散函数f(x)=x+2,查找4是否存在等价于求f(x)-4=0是否有离散解。因为f(1)-4=3-4=-1<0,f(5)-4=7-4=3>0,且函数在区间内单调递增,因此我们可以利用二分法查找求解。如果最后二分到不能再分的情况,如果只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在离散解,即4不在这个数组内。

​ 具体到代码上,二分查找时区间的左右端取开区间或闭区间绝大多数时候都可以,建议左闭右开。

4.2 求开方

  1. Sqrt(x) (Easy)

    题目描述

    给定要给非负整数,求它的开方,向下取整。

    输入输出样例

    input: 8
    output: 2
    

    8开方的结果是2.82842...,向下取整既是2。

    题解

    ​ 这道题可以理解为给定一个非负整数a,求f(x)=X^2 - a=0的解。因为只考虑x>=0,所以在定义域上是单调递增的。由于f(0)=-a<=0,f(a)=a^2 -a >=0,对区间[0, a]内使用二分法找到f(x)=0的解。

    ​ 为了防止除以0,a=0单独考虑,然后对区间[1, a]进行二分查找。

    private static int mySqrt1(int a){
            if(a==0) return 0;
            int left=1, right=a, mid, sqrt;
            while(left<=right){
                mid = left + (right-left)/2;
                sqrt = a/mid;
                if(sqrt==mid){
                    return mid;
                }else if(mid>sqrt){
                    right = mid-1;
                }else{
                    left = mid+1;
                }
            }
            return right;
        }
    

    还有一种更快的算法——牛顿迭代法,其公式为Xn+1 = Xn - f(Xn)/f`(Xn)。给定f(x) = x^2 - a = 0,这里的迭代公式为Xn+1 = (Xn + a/Xn)/2:

    private static int mySqrt2(int a){
            long x = a;
            while(x*x > a){
                x = (x + a/x)/2;
            }
            return (int)x;
        }
    

4.3 查找区间

  1. Find First and Last Position of Element in Sorted Array(Medium)

    题目描述

    ​ 给定一个增序的整数数组 和一个值,查找该值第一次和最后一次出现的位置。

    输入输出样例

    输入是一个数组和一个值,输出为该值第一次出现的位置和最后一次出现的位置(从0开始),如果不存在则返回-1。

    input: nums = [5, 7, 7, 8, 8, 10], target = 8
    output: [3, 4]
    

    数字8在索引 3第一次出现,索引4最后一次出现。

    题解

    感觉这道题像是java中的indexOf()和lastIndexOf(),但是使用的算法不同。

    private static int[] searchRange(int[] nums, int target){
            if(nums.length==0) return new int[]{-1, -1};
            int first = first_index(nums, target);
            int last = last_index(nums, target) - 1;
            if(first==nums.length||nums[first]!=target){
                return new int[]{-1, -1};
            }
            return new int[]{first, last};
        }
    
        private static int first_index(int[] nums, int target){
            int left=0, right=nums.length, mid;
            while (left<right){
                mid = (left+right)/2;
                if(nums[mid]>=target){
                    right = mid;
                }else{
                    left = mid+1;
                }
            }
            return left;
        }
    
        private static int last_index(int[] nums, int target){
            int left=0, right=nums.length, mid;
            while (left<right){
                mid = (left+right)/2;
                if(nums[mid]>target){
                    right = mid;
                }else{
                    left = mid+1;
                }
            }
            return left;
        }
    

4.4 旋转数组查找数字

  1. Search in Rotated Sorted Array II(Medium)

    题目描述

    ​ 一个原本增序的数组被首尾相连后按某个位置断开(如[1,2,2,3,4,5] -> [2,3,4,5,,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值和一个旋转数组,判断这个值是否存在于这个旋转数组中。

    输出输出样例

    ​ 输入是一个数组和一个值,输出是一个布尔值,表示数组中是否存在该值。

    input: nums = [2, 5, 6, 0, 0, 1, 2], target = 0
    output: true
    

    题解

    ​ 即使数组被旋转过,我们仍然可以利用这个数组的递增性(其仍为单调递增),使用二分查找。对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对另一半区间继续二分查找。

    ​ 注意,因为数组存在重复数字,如果中点和左端的数字相同,我们不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。

    private static boolean search(int[] nums, int target){
            int start=0, end=nums.length-1, mid;
            while (start<=end){
                mid = (start+end)/2;
                if(nums[mid]==target){
                    return true;
                }
                if(nums[mid]==nums[start]){
                    // 无法判断哪个区间是增序的
                    start++;
                }else if(nums[mid]<=nums[end]){
                    // 右区间是增序的
                    if(target<nums[mid] && nums[end]<=nums[mid]){
                        start = mid + 1;
                    }else{
                        end = mid - 1;
                    }
                }else{
                    // 左区间是增序的
                    if(nums[start]<=target &&target<nums[mid]){
                        end = mid - 1;
                    }else{
                        start = mid + 1;
                    }
                }
            }
            return false;
        }
    

4.5 练习

基础难度

  1. Find Minimum in Rotated Sorted Array II(Medium)

    题目描述

    旋转数组的变形题之一。

    假设以升序排序的数组在事先未知的某个轴上旋转。

    (即[0,1,2,4,5,6,7]可能会变成[4,5,6,7,0,1,2])。

    找到最小的元素。

    该数组可能包含重复项。

    输入输出样例:

    input: nums=[4,5,6,7,0,1,2]
    output: 0
    

    题解

    private static int minimumInRotatedArray(int[] nums){
            int left=0, right=nums.length-1, mid=-1;
            while (left<=right){
                mid = (left+right)/2;
                if(nums[mid]==nums[left]){
                    // 无法判断哪个区间是递增的
                    left++;
                }else if(nums[mid]<=nums[right]){
                    // 右侧区间是递增的
                    left = mid;
                }else{
                    // 左侧区间是递增的
                    right = mid;
                }
            }
            return Math.min(nums[left], nums[right]);
        }
    
  2. Single Element in a Sorted Array(Medium)

    题目描述

    在出现独立数之前和之后,奇偶位数的值发生了什么变化?

    给定一个仅由整数组成的排好序的数组,其中每个元素仅出现两次,只有一个元素除外。

    找到只出现一次的这个元素。

    提示:您的解决方案应在O(log n)时间和O(1)空间中运行。

    样例输入输出

    Input: nums = [1,1,2,3,3,4,4,8,8]
    Output: 2
    

    题解

    private static int singleNonDuplicate(int[] nums){
            int left=0, right=nums.length-1, mid;
            while(left < right){
                mid = (left+right)/2;
                // 在右侧
                if ((nums.length-1-mid)%2==1){
                    if(nums[mid]==nums[mid + 1]){
                        right = mid;
                    }else{
                        left = mid+1;
                    }
                // 在左侧
                } else{
                    if(nums[mid]!=nums[mid + 1]){
                        right = mid;
                    }else{
                        left = mid+1;
                    }
                }
            }
            return nums[left];
        }
    

进阶难度

  1. Median of Two Sorted Arrays(Hard)

    题目描述

    需要对两个数组同时进行二分搜索。

原文地址:https://www.cnblogs.com/pangqianjin/p/14254124.html