【面试算法题总结08】二分搜索算法

本文详细介绍了二分搜索算法在不同场景下的应用,包括寻找数字、寻找数字的边界、查找缺失数字、搜索旋转数组中的目标值、寻找两个有序数组的中位数等。通过具体的代码实现,解析了每个问题的解决思路,强调了二分搜索在保证时间复杂度上的优势。同时,提供了多种解题方法,如归并、二分查找和不合并数组的解决方案,展示了算法的灵活性和效率。
摘要由CSDN通过智能技术生成

二分搜索算法:

主要问题:while是否有=,mid的加一和减一

常见搜索场景:寻找一个数,寻找一个数的左侧边界,寻找一个数的右侧边界
1 寻找一个数字:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1。这样虽然也可以通过线性搜索这个数的左、右侧边界,但这样就难以保证二分搜索对数级的时间复杂度了
2 寻找一个数的左侧边界:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1,等于target时right=mid-1,返回left,要检查其是否越上界
3 寻找一个数的右侧边界:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1,等于target时left=mid+1,返回right,要检查其是否越下界

这里主要是借鉴《labuladong的算法小抄》p71~p84的思想
 

例题1:在排序数组中查找元素的第一个和最后一个位置

将"寻找一个数的左侧边界"和"寻找一个数的右侧边界"合体

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] result=new int[2];
        result[0]=mySearch(nums,target,"left");
        result[1]=mySearch(nums,target,"right");
        return result;
    }
    public int mySearch(int[] nums, int target,String type){
        int left=0,right=nums.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]==target){
                if(type=="left"){		//左右边界的区别1
                    right=mid-1;
                }else{
                    left=mid+1;
                }
            }else if(nums[mid]>target){
                right=mid-1;
            }else if(nums[mid]<target){
                left=mid+1;
            }
        }
        if(type=="left"){	//左右边界的区别2
            if(left>=nums.length||nums[left]!=target){
                return -1;
            }
            return left;
        }else{
            if(right<0||nums[right]!=target){
                return -1;
            }
            return right;
        }
    }
}

 

例题2:在排序数组中查找数字 I

例题1的衍生

class Solution {
    public int search(int[] nums, int target) {
        int smallIndex=mySearch(nums,target,"left");
        int bigIndex=mySearch(nums,target,"right");
        return (smallIndex==-1)?0:bigIndex-smallIndex+1;		//注意判断为-1的情况,以及其他情况时下标之差+1
    }
    public int mySearch(int[] nums, int target,String type){
        int left=0,right=nums.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]==target){
                if(type=="left"){
                    right=mid-1;
                }else{
                    left=mid+1;
                }
            }else if(nums[mid]>target){
                right=mid-1;
            }else if(nums[mid]<target){
                left=mid+1;
            }
        }
        if(type=="left"){
            if(left>=nums.length||nums[left]!=target){
                return -1;
            }
            return left;
        }else{
            if(right<0||nums[right]!=target){
                return -1;
            }
            return right;
        }
    }
}

 

例题3:0~n-1中缺失的数字

这题即可以理解为找下标和内容符合的右边界下标+1,或者下标和内容不符合的左边界下标。(这里要分清楚这题下标和内容的关系)
下标和内容符合的右边界下标+1:

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

下标和内容不符合的左边界下标:

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

 

例题4:旋转数组的最小数字

为什么本题二分法不用 nums[mid] 和 nums[left] 作比较?
二分的目的是判断结果和mid的位置关系,从而缩小区间。而在 nums[m] > nums[i]情况下,无法判断m在哪个排序数组中。
如1,2,3,4,5和2,3,4,5,1。结果可能在mid的左边,也可能在mid的右边。

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

 

例题5:搜索旋转排序数组

根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分

class Solution {
    public int search(int[] nums, int target) {
        int left=0,right=nums.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]==target){
                return mid;
            }
            if(nums[mid]<nums[right]){     //有序的在右侧。其实和nums[left]比也可以
                if(target>nums[mid]&&target<=nums[right]){      //target在右侧
                    left=mid+1;
                }else{
                    right=mid-1;
                }
            }else{              //有序的在左侧。其实和nums[left]比也可以
                if(target>=nums[left]&&target<nums[mid]){      //target在左侧
                    right=mid-1;
                }else{
                    left=mid+1;
                }
            }
        }
        return -1;
    }
}

 

例题6:寻找两个正序数组的中位数

解法1:归并为一个数组后,得到中位数

时间复杂度O(n+m)
空间复杂度O(n+m)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n=nums1.length;
        int m=nums2.length;
        if(n+m==0){
            return -1;
        }
        int[] temp=new int[n+m];
        int p1=0,p2=0,p3=0;
        while(p1<n&&p2<m){
            if(nums1[p1]<nums2[p2]){
                temp[p3++]=nums1[p1++];
            }else{
                temp[p3++]=nums2[p2++];
            }
        }
        if(p1!=n){
            while(p1<n){
                temp[p3++]=nums1[p1++];
            }
        }
        if(p2!=m){
            while(p2<m){
                temp[p3++]=nums2[p2++];
            }
        }
        if((m+n)%2==0){
            return (temp[(m+n)/2-1]+temp[(m+n)/2])*1.0/2;
        }else{
            return temp[(m+n)/2];
        }
    }
}

解法2:不进行合并,只要找到中位数的位置即可

由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
时间复杂度 O(n+m)
空间复杂度 O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n=nums1.length;
        int m=nums2.length;
        if((n+m)==1){
            return n!=0?nums1[0]:nums2[0];
        }
        int target=(n+m)/2;
        int p1=0,p2=0;
        int number1=0,number2=0;
        while(p1<n&&p2<m){
            if((p1+p2)==target-1){
                if(nums1[p1]<=nums2[p2]){
                    number1=nums1[p1];
                }else{
                    number1=nums2[p2];
                }
            }
            if((p1+p2)==target){
                if(nums1[p1]<nums2[p2]){
                    number2=nums1[p1];
                }else{
                    number2=nums2[p2];
                }
            }
            if(nums1[p1]<nums2[p2]){
                ++p1;
            }else{
                ++p2;
            }
        }
        if(p1!=n){
            while((p1+p2)<=target){
                if((p1+p2)==target-1){
                    number1=nums1[p1];
                }
                if((p1+p2)==target){
                    number2=nums1[p1];
                }
                ++p1;
            }
        }
        if(p2!=m){
            while((p1+p2)<=target){
                if((p1+p2)==target-1){
                    number1=nums2[p2];
                }
                if((p1+p2)==target){
                    number2=nums2[p2];
                }
                ++p2;
            }
        }
        if((m+n)%2==0){
            return (number1+number2)*1.0/2;
        }else{
            return number2;
        }
    }
}

解法3:二分查找

这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2或 (m+n)/2+1。
假设两个有序数组分别是A 和B。要找到第 k 个元素,我们可以比较A[k/2-1]和B[k/2-1],其中 /表示整数除法。由于A[k/2−1] 和B[k/2−1] 的前面分别有A[0…k/2−2] 和B[0…k/2−2],即k/2−1 个元素,对于A[k/2−1] 和B[k/2−1] 中的较小值,最多只会有(k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 k小的数了。
时间复杂度 O(log(n+m))
空间复杂度 O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n1=nums1.length,n2=nums2.length;
        //奇偶数不同情况
        if((n1+n2)%2==0){
            int k1=(n1+n2)/2,k2=(n1+n2)/2+1;
            return (kSmall(nums1,nums2,k1)+kSmall(nums1,nums2,k2))/2.0;
        }else{
            int k=(n1+n2)/2+1;
            return (double)kSmall(nums1,nums2,k);
        }
    }
    //两个数组中第k小的数
    public int kSmall(int[] nums1, int[] nums2,int k){
        int n1=nums1.length,n2=nums2.length;
        int index1=0,index2=0;
        while(true){
            //终止边界条件
            if(index1==n1){
                return nums2[index2+k-1];
            }
            if(index2==n2){
                return nums1[index1+k-1];
            }
            if(k==1){
                return Math.min(nums1[index1],nums2[index2]);
            }
            //二分
            int half=k/2;
            int newIndex1=Math.min(index1+half,n1)-1;
            int newIndex2=Math.min(index2+half,n2)-1;
            if(nums1[newIndex1]<=nums2[newIndex2]){
                k-=(newIndex1-index1+1);
                index1=newIndex1+1;
            }else{
                k-=(newIndex2-index2+1);
                index2=newIndex2+1;
            }
        }
    }
}

解法4:划分数组

在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
时间复杂度 O(log(min(n,m)))
空间复杂度 O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if(nums1.length>nums2.length){
            return findMedianSortedArrays(nums2,nums1);
        }
        int m=nums1.length;
        int n=nums2.length;
        int left=0,right=m;
        // median1:前一部分的最大值
        // median2:后一部分的最小值  
        int median1=0,median2=0;
        while(left<=right){
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]  
            int i=(left+right)/2;
            int j=(m+n+1)/2-i;
            // nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            int nums_im1=(i==0?Integer.MIN_VALUE:nums1[i-1]);
            int nums_i=(i==m?Integer.MAX_VALUE:nums1[i]);
            int nums_jm1=(j==0?Integer.MIN_VALUE:nums2[j-1]);
            int nums_j=(j==n?Integer.MAX_VALUE:nums2[j]);

            if(nums_im1<=nums_j){
                median1=Math.max(nums_im1,nums_jm1);
                median2=Math.min(nums_i,nums_j);
                left=i+1;
            }else{
                right=i-1;
            }
        }  
        return (m+n)%2==0?(median1+median2)/2.0:median1;    
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值