二分查找--JZ6、面试题53

JZ6、旋转数组的最小数

题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

示例1
输入
[3,4,5,1,2]
返回值
1

注:
1、非递减排序:这里有些歧义,暂且当做递增排序来处理。
2、这里定义的旋转:比如说{3,4,5,1,2}是{1,2,3,4,5}的一个旋转,旋转后第一个元素大于等于最后一个元素。
3、最开始的若干个元素:若干个可以是0个,若是0个,则旋转后的数组就是原递增数组,就不存在:第一个元素大于等于最后一个元素。

题解:
本题是找出最小数,即首先是一个查找题
又因为旋转后的数组实际上可划分为两个(递增)排序子数组(比如:示例的[3,4,5]和[1,2]),即,本题的数组在一定程度上是排序的,而排序的数组可以用二分法实现查找。

或者说:
如果能够明确二分之后,答案存在于二分的某一侧,就可以使用二分。

关键点:
1、二分查找的关键是arr[mid]跟谁比。原则是arr[mid]跟某个值比后能产生二分效果(即,一次比较后能够确定答案在mid的某一侧)
2、一般的比较原则有:
—如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
—如果没有目标值,就看arr[mid]跟某个值比后能产生二分效果,一般可以考虑 端点

下面以右端点看作target 来进行分析(也可将左端点 或 左右两个端点作为target )
1、情况1:
arr[mid] > target
比如旋转数组4 5 6 1 2 3 :arr[mid] 为 6, target为右端点 3, arr[mid] > target,说明arr[mid]处于前边的递增子数组, 说明[first … mid] 都是 >= target 的,可以确定最小数在arr[mid]右侧,在[mid+1…last]区间内,所以 first = mid + 1
2、情况2:
arr[mid] < target,
比如旋转数组5 6 1 2 3 4:说明arr[mid]处于后边的递增子数组,可以确定最小数在arr[mid]及其左侧,说明答案肯定不在[mid+1…last],但是arr[mid] 有可能是答案,所以答案在[first, mid]区间,所以last = mid;
3、情况3:
arr[mid] == target:
如果是 1 0 1 1 1, arr[mid] = target = 1, 显然答案在左边
如果是 1 1 1 0 1, arr[mid] = target = 1, 显然答案在右边
所以这种情况,不能确定答案在左边还是右边,那么就让last = last - 1;慢慢缩少区间,同时也不会错过答案。

class Solution {
    
    
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
    
    if (rotateArray.size() == 0) return 0;
        int first = 0, last = rotateArray.size() - 1;
        while (first < last) {
    
     // 最后剩下一个元素,即为答案
            if (rotateArray[first] < rotateArray[last]) {
    
     // 提前退出:将前0个元素搬到数组末尾的情况;
                return rotateArray[first];
            }
            int mid = (first + last)/2;
            if (rotateArray[mid] > rotateArray[last]) {
    
     // 情况1
                first = mid + 1;
 
            }
            else if (rotateArray[mid] < rotateArray[last]) {
    
     //情况2
                last = mid;
            }
            else {
    
     // 情况3
                --last;
            }
        }
        return rotateArray[first];
    }
};

时间复杂度:二分,所以为O(longN), 但是如果是[1, 1, 1, 1],会退化到O(n)(last = last - 1:一个一个的缩小,缩小n次)
空间复杂度:没有开辟额外空间,为O(1)

面试题53-I 数字在排序数组中出现的次数

题目:
统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:

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

题解:
模式识别: 排序数组+一次数出现次数(即,查找一个数出现次数)----------->二分查找
思路一:
使用二分查找查找出一个给定数(O(logn)),然后从这个数开始用向两边遍历,统计重复的次数,因给定数在长度为n的数组中可能出现n次,所以相连变扫描的时间复杂度为O(n),这和直接从头扫描整个数组一样为O(n)。

思路二:(分别查找重复数字的第一个和最后一个)
在这里插入图片描述
记重复数字的第一个为左边界,最后一个为右边界;
在这里插入图片描述
具体步骤(以查右边界为例):
1、nums[mid]<target:右边界在后半段,即left=mid+1;
2、nums[mid]>target:右边界在前半段,即right=mid-1;
3、nums[mid]==target:此时需判断这个值是不是右边界,
3.1 若nums[mid]位于数组的右边界(即,mid ==(nums.size()-1),或者nums[mid]和它后一位不相等(nums[mid]!=nums[mid+1]),此时nums[mid]是右边界
3.2 否则,nums[mid]不是右边界,右边界在后半段left=mid+1;

代码:

class Solution {
    
    
public:
    int search(vector<int>& nums, int target) {
    
    
        if(nums.size()==0) return 0;//判特
        int left=0, right=nums.size()-1, mid;
        //找右边界,跳出循环后mid代表右边界
        while(left<=right){
    
    
            mid=left+(right-left)/2;
            if(nums[mid]<target) left=mid+1;
            else if(nums[mid]>target) right=mid-1;
            else{
    
    
                if(mid==(nums.size()-1) ||nums[mid]!=nums[mid+1]) break;
                else left=mid+1;
            }
        }
        // 若数组中无 target ,则提前返回
        //mid指向最后查找的位置,若nums[mid]!=target则表明数组没有target值;
        if(nums[mid]!=target) return 0;
        //记录右边界值
        int r=mid;//下面还会进行一次查找,mid会变,所以这里记录右边界值
        left=0,right=nums.size()-1;
        //找左边界,跳出循环后mid代表左边界
        while(left<=right){
    
    
            mid=left+(right-left)/2;
            if(nums[mid]<target) left=mid+1;
            else if(nums[mid]>target) right=mid-1;
            else{
    
    
                if(mid==0 ||nums[mid]!=nums[mid-1]) break;
                else right=mid-1;
            }
        }
        //记录左边界值
        int l=mid;
        return r-l+1;
    }
};

时间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1) : 几个变量使用常数大小的额外空间。

面试题53-II. 0~n-1中缺失的数字

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

限制:

1 <= 数组长度 <= 10000

示例 1:

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

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

题解:
模式识别:排序数组+找出(查找)------>二分查找

思路:
在这里插入图片描述

即,查找第一个下标与其值不同的元素,其下标就是缺失的值;
有一种特殊情况:当 nums 缺失的是第 n-1 个数时,比如 nums = [0, 1],此时 nums 缺失的是 2,但所有元素都是nums[index]==index,所以没有第一个下标和值不同的元素;
代码:C++,使用带等号的二分查找while(i<=j)

class Solution {
    
    
public:
    int missingNumber(vector<int>& nums) {
    
    
        int n=nums.size();
        int left=0,right=n-1,mid;
        while(left<=right){
    
    
            mid=left+(right-left)/2;
            if(nums[mid]==mid) left=mid+1;
            else{
    
    
                if(mid==0 || nums[mid-1]==mid-1) return mid;
                else right=mid-1;
            }
        }
        //循环结束也没查找成功,原因是当 nums 缺失的是第 n-1 个数时,比如 nums = [0, 1],此时 nums 缺失的是 2,但所有元素都是nums[mid]==mid,所以查找不到第一个下标和值不同的元素;循环到最后mid指向最后一个元素,而left=mid+1则是缺失的元素;
        return left;


    }
};

间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。

Java,不使用带等号的二分查找while(i < j)

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

    }
}


二分查找循环条件带不带等号的区别,即while(i<=j)和while(i < j)

  • while(i<=j),区间缩小到 i =j,可能会陷入到死循环,所以,需要return 语句退出while循环;
  • while(i < j),区间缩小到 i =j,不满足 while(i < j),即自动退出循环,while循环内一般不需要return语句。

猜你喜欢

转载自blog.csdn.net/qq_42647047/article/details/110294011
今日推荐