二分查找练习

二分查找通常用于一个有序数组中。

x的平方根

实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去

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

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sqrtx

在没有使用sqrt()方法的时候,虽然可以遍历[1,n - 1]进行获取最后的结果,但是耗时,因此我们需要利用二分查找,恰好[1,n - 1]的数字是有序的,可以通过二分查找来实现。
基本思路:
1、在[1,x ]中进行二分查找

  • 初始值为left = 1,right = x,定义中间变量为mid,sqrt(基于下面的mid方法求解,需要将right初始值为x).
  • 在left <= right的时候,不断进行循环,并且mid = left + (right - left) / 2
  • 获取sqrt,即sqrt = x / mid,然后依据sqrt和mid的大小关心,更新left、right:
    ①sqrt == mid,说明sqrt的平方刚好等于x,直接返回sqrt
    ②sqrt > mid,说明mid太小,所以为了使得sqrt、mid两者趋于相等,我们需要将mid变大,所以需要移动left,使得left = mid + 1
    ③sqrt < mid,说明mid太大,所以为了让sqrt、mid两者趋于相等,需要将mid变小,所以需要将right变小,使得right = mid - 1
    2、当left > right的时候,直接返回right,才会保证向下取整。即比如8,当退出循环的时候,left为3,right为2,因为8的平方根是2.82842…,向下取整就是2,即right.同时如果x等于0的时候,刚好可以返回0.

为什么mid = left + (right - left) / 2

我们需要取[left,right](这个区间公差为1,所以每一个数字相差1)中的中间值,那么这时候(right - left)表示这一个区间中一共有right - left数字,除以一半,在加上left就是[left,right]区间的中间数字

对应的代码:

class Solution {
    public int mySqrt(int x) {
       int l = 1,r = x,mid,sqrt;
       while(l <= r){
           mid = l + (r - l) / 2;
           sqrt = x / mid;
           if(sqrt == mid)
               return sqrt;
           else if(mid > sqrt)
               r = mid - 1;
           else
               l = mid + 1;
       }
       return r;
    }
}

运行结果:
在这里插入图片描述

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

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

示例 3:
输入:nums = [], target = 0
输出:[-1,-1]

提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array

利用两次二分查找寻找target的左右两个边界。同时在进行第二个二分查找之前,可以利用第一个二分查找判断target是否存在于数组中,如果不存在,那么就返回[-1,-1],否则进行第二个二分查找
对应的代码:

class Solution {
    
    public int[] searchRange(int[] nums, int target) {
       int right = upperBound(nums,0,nums.length - 1,target);
       /*
       如果right等于-1,说明target不存在于数组中或者数组长度为0,直接返回
       [-1,-1],无需进行第二次二分查找
       */
       if(right == -1)
          return new int[]{-1,-1};
       int left = lowerBound(nums,0,right,target);
       return new int[]{left,right};

    }
    //寻找右边界
    public int upperBound(int[] nums,int low,int high,int target){
        int mid;
        while(low <= high){
           mid = (low + high) / 2;
           if(nums[mid] <= target)//求解右边界,所以需要不断往右移动
              low++;
           else
              high = mid - 1;
       }
       /*
       如果high在合理的范围(即大于等于0,说明数组的长度不是0)
       如果nums[high] == target,说明target存在于数组中,所以返回
       high,否则返回-1,表示数组的长度为0或者target没有在数组中
       */
       return high >= 0 && nums[high] == target ? high : -1;
    }
    //寻找左边界
    public int lowerBound(int[] nums,int low,int high,int target){
        int mid;
        while(low <= high){
           mid = (low + high) / 2;
           if(nums[mid] >= target)//不断向左边移动,从而得到左边界
              high = mid - 1;
           else
              low = mid + 1;
       }
       return low;
    }
}

运行结果:
在这里插入图片描述

搜索旋转排序数组II

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

传递给函数之前,nums 在预先未知的某个下标 k(0 <= 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 。

示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

提示:

1 <= nums.length <= 5000
-104 <= nums[i] <= 104
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104

进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii

由于进行旋转之前,数组是有序的,并且是非降序,所以逐渐递增的,进行旋转之后,就会分成了两个子数组,此时每一个子数组都是递增的。而我们利用二分查找的前提是在一个有序数组中进行的。所以划分局部有序子数组是本题的关键。

基本步骤:
①定义变量,left、right,mid,并且left = 0,right = nums.length - 1.
②在left小于等于right的时候,不断进行循环。

  • mid = (left + high) / 2,获取数组中的中间值,如果这个中间值等于target,那么直接返回true,否则进行下面的步骤。
  • 如果nums[mid]的值大于nums[high]数字,说明[low,high]这个区间中,[low,mid]是局部有序,并且都是大于nums[high]的值,因此这时候我们需要判断target是否在[nums[low],nums[mid]]这一个区间中,如果在,就在[low,mid]这一个区间中查找target,否则,说明target可能在[mid + 1,high]区间,这时候更新low,使得low = mid + 1.
  • nums[mid] 小于nums[high]的值,表明[mid + 1,high]是局部有序的,所以需要判断target是否在[mid + 1,high]区间中,如果在,就二分查找进行查找,否则更新high,使得high = mid
  • nums[mid]等于nums[high]的时候,并且当前的nums[mid]不等于target,此时我们并不能得出[mid,high]的所有数字相等且都不等于target的结论,因为存在[1,1,1,1,1,1,1,1,2,1,1,1,1,1],target = 2,这时候nums[mid] = nums[7] = 1 = nums[high] = nums[13] = 1,但是此时[mid,high]的所有数字并没有相等,所以**需要将high–,**而不是将high = mid。

对应的代码:

class Solution {
    public boolean search(int[] nums, int target) {
        int low = 0,high = nums.length - 1,mid,pivot,tmp;
        //找到中间枢纽
        while(low <= high){
            mid = (low + high) / 2;
            if(nums[mid] == target)
               return true;
            if(nums[mid] > nums[high]){
                /*
                如果中间值大于最后一个数字,说明[low,mid]是单调的,并且
                target的值在这个区间中,利用二分查找进行判断
                */
                if(target >= nums[low] && target < nums[mid])
                   return bounarySearch(nums,low,mid,target);
               //target不在这个区间中,那么说明在右边的区间中,所以low = mid + 1
                low = mid + 1;
            }
            else if(nums[mid] < nums[high]){
               /*
               如果中间值小于nums[high],说明[mid + 1,high]都是大于
               nums[mid]的值,并且是逐渐递增的,所以可以进行二分查找
               */
               if(target >= nums[mid + 1] && target <= nums[high])
                   return bounarySearch(nums,mid + 1,high,target);
                //如果没有找到,说明需要将缩小范围为[low,mid]
                high = mid;
            }else{
                /*
                中间值等于nums[high],表示[low,high]不是单调的,在某一处是
                变化的,所以需要缩小范围,执行high--
                而不是认为[mid,high]的所有数字都是相等的(事实并不一
                定),然后执行high = mid - 1或者high = mid
                */
               high--;
            }
        }
        return false;
    }
    //二分查找target
    public boolean bounarySearch(int[] nums,int low,int high,int target){
        int mid;
        while(low <= high){
            mid = (low + high) / 2;
            if(nums[mid] == target)
               return true;
            else if(nums[mid] > target)
               high = mid - 1;
            else 
               low = mid + 1;
        }
        return false;
    }
}

运行结果:
在这里插入图片描述

有序数组中的单一元素

给定一个只包含整数的有序数组每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10

提示:

1 <= nums.length <= 105
0 <= nums[i] <= 105

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/single-element-in-a-sorted-array

由于数组有序,所以考虑二分查找,并且每一个数字出现两次,只有一个单一数字,表示数组的总个数必然是奇数,所以单一数字所在的那一侧数字总数必然是奇数。基于这个原理进行更新left、right。
①获取中间值下标mid = (left + right) / 2
②这时候中间值有可能是单一数字,也有可能不是,并且如果中间值不是单一数字的时候,要么和左边相邻数字相等,要么和右边相邻数字相等,因此
可以分为几种情况进行考虑:

  • 中间值等于左边相邻的数字,即nums[mid] == nums[mid - 1],那么左边不等于中间值的数字个数为(mid - 1 - left),那么我们需要考虑左边不等于中间值的数字个数的奇偶性,当(mid - 1 - left) % 2 == 0的时候,左边不等于中间值的数字个数为偶数,表明单一数字在中间值的右侧,所以更新left,使其等于mid + 1,否则,(mid - 1 - left) % 2 != 0的时候,表示左边不等于中间值数字个数为奇数,表示单一数字在中间值的左侧,更新right为mid - 2(因为mid - 1下标对应的值和当前mid的值相等,所以不是mid - 1)
  • 中间值等于右边相邻的数字,即nums[mid] == nums[mid + 1],那么右边不等于中间值的数字个数为(right - mid - 1),这时候需要考虑右边不等于中间值的数字个数的奇偶性,从而判断单一数字在中间值的哪一侧。如果(right - mid - 1) % 2 == 0,表示中间值右边不等于中间值的数字个数为偶数,这时候单一数字在中间值左侧,所以更新right为mid - 1,否则,(right - mid - 1)% 2 != 0,表示中间值右边不等于中间值的数字个数为奇数,这时候表示单一数字在中间值的右侧,所以更新left为mid + 2
  • 中间值不等于左右两个相邻数字,表示它就是单一数字,直接返回nums[mid]

对应的代码:

class Solution {
   public int singleNonDuplicate(int[] nums) {
        int left = 0,right = nums.length - 1,mid;
        while(left <= right){
            mid = (left + right) / 2;
            if(mid - 1 >= 0 && nums[mid] == nums[mid - 1]){
                /*
                如果当前中间下标的值等于左边相邻的数字,并不能断定单一数字
                一定在右侧,例如[1,1,2,3,3,4,4,8,8],此时中间下标为4,和下
                标为3的数字相等,但是单一数字却在左侧。所以当中间数字等于
                左边相邻数字的时候,则左边数字不等于中间值的个数为mid - 1 
                - left,如果是奇数,表示单一数字在左侧,所以需要更新right
                使其等于mid - 2,否则,如果是偶数,表示在右侧,更新left,
                使其等于mid + 1
                */
                if((mid - 1 - left) % 2 == 0) //如果左侧不等于中间值的数字个数是偶数,那么说明单一数字在中间数字的右侧
                   left = mid + 1;
                else//左侧为不等于中间值的数字个数为奇数,表示在中间值的左侧
                   right = mid -2;
            }else if(mid + 1 < nums.length && nums[mid + 1] == nums[mid]){
                /*
                如果中间下标的值等于右边相邻的数字的时候,需要知道右边不等
                于中间值数字的个数,即right - mid - 1,这时候如果是偶数,
                说明单一数字出现中间值的左侧,所以更新right为mid - 1
                否则,如果是奇数,表示单一数字出现右侧,更新left为mid + 2
                */
                if((right - mid - 1) % 2 == 0)
                   right = mid - 1;
                else
                   left = mid + 2;
            }else //左右两个数字都不想等,表示中间值就是单一数字
                return nums[mid];
        }
        return -1;
    }
}

运行结果:
在这里插入图片描述

长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0

示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105

进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

方法一:暴力解决

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int res = 0,i,j = 0,sum = 0;
        for(i = 0; i < nums.length; ++i){
           sum += nums[i];
           while(sum >= target){
               if(res == 0 || i - j + 1 < res)
                 res = i - j + 1;
               sum -= nums[j++];
           }
        }
        return res;
    }
}

运行结果:
在这里插入图片描述
方法二:前缀和+二分查找
定义一个sum数组,表示[0,i]之间的元素的和。由于nums数组中的元素是正整数,所以sum数组必定是递增的数组,因此是有序的,符合二分查找的条件

假设[ j, i]是一个连续子数组,并且这个连续子数组的和大于等于target,其中[ j, i]的连续子数组等于nums[ j ] + nums[ j + 1] + nums[ j + 2] + ...... + nums[ i ],也就等于了sum[ i ] - sum[ j - 1],所以满足sum[ i ] - sum[ j - 1] >= target,这时候在知道 i 的情况下,需要求解出满足条件的sum[ j - 1],并且使其距离 i 最近即可

所以遍历sum,每次循环进行二分查找:
1、定义res,初始值为0,表示符合条件的最小连续子数组的长度
2、如果当前的sum[ low ] 小于等于target,这时候需要讨论sum是小于target,还是等于target,如果等于target,则表示[0 , low]的连续子数组的和刚好等于target,这时候需要将low - 0 + 1赋值给res
3、否则,sum[ i ]大于target,那么需要进行二分查找,寻找距离 low 最近的sum[ j - 1] ,使得sum[ i ] - sum[ j - 1] >= targert并且长度最短,也即sum[j - 1] <= sum[ i ] - target,因此只要sum[j - 1]在满足条件下,sum[ j - 1]尽可能地大,就会使得连续子数组个数越小,此时表示[ j , i]这个连续子数组的和大于等于target,需要将j - i + 1赋值给res.

  • 在findIndex方法中执行二分查找的方法,查找最大地sum[ j ],使得sum[ j ] >= sum[low] - target,因此该二分查找和寻找sum[low] - target的右边界的道理类似
  • 当sum[mid] 小于等于 target的时候,需要更新low,使sum[mid]变大,所以low = mid + 1
  • 否则,sum[mid]大于target的时候,需要将sum[mid]减小,所以更新right,使得right = mid - 1
  • 当left > right得时候,退出循环,然后返回left

对应的代码:

class Solution {
    public int findIndex(int[] sum,int left,int right,int target){
        int mid;
        while(left <= right){
            mid = (left + right) / 2;
            if(sum[mid] > target){
                //target小于sum[mid]得时候,表示target在sum[mid]得左边,所以更新right,使其等于mid - 1
                right = mid - 1;
            }else{
                /*
                当sum[mid]小于target,表示target在mid得右边,所以使left = 
                mid + 1,同时当sum 等于target得时候,为了将连续子数组长度
                尽可能小,需要将left右移,再次进行left = mid + 1,所以将在
                sum[mid]小于等于target得时候更新left
                */
                left = mid + 1;
            } 
            
        }
        return left;
    }
    
    public int minSubArrayLen(int target, int[] nums) {
        if(nums.length <= 0)
            return 0;
        int[] sum = new int[nums.length + 1];
        int i,low = 0,res = 0,index;
        sum[0] = nums[0];//[0,0]的前缀和为nums[0]
        for(i = 1; i < nums.length; ++i){
            sum[i] = nums[i] + sum[i - 1];
        }
        //利用二分查找进行查找j - 1的下标
        for(;low < nums.length; ++low){
            if(sum[low] <= target){
                if(sum[low] == target && (res == 0 ||low + 1< res))
                   res = low + 1;
            }else{
                //如果当前[0,i]的连续和大于target,进行二分查找
                index = findIndex(sum,0,low - 1,sum[low] - target);//由于该方法参数right的值使low - 1,所以导致left不会发生越界的情况
                if(res == 0 || low - index + 1 < res)
                    res = low - index + 1;
            }
        }
        return res;
    }
 

}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值