算法笔记【一】二分

这里的二分模板来源于 《算法竞赛进阶指南》和Acwing,这里只是对模板的用法及Leetcode的例题讲解。 同时欢迎各位加入Acwing社区

所谓二分算法,就是不断的缩减区间的范围去寻找目标值

二分模板

整数二分

模板一

  1. 会将区间划分为[l,mid] 和[mid+1,r]两个区间,最终结果会落在左半区间

  2. left指针和right指针最终都会落在相同的点上,可以通过两个指针指向的值判断是否是有解

int l = 0;
int r = nums.length-1;
while(l < r ){
    int mid = l+r>>1;
    if(check(mid)){
        r = mid;
    }else{
        l = mid+1;
    }
}

模板二

  1. 会将区间划分为[l,mid-1] 和[mid,r]两个区间,最终结果会落在右半区间

  2. left指针和right指针最终都会落在相同的点上,可以通过两个指针指向的值判断是否是有解

int l = 0;
int r = nums.length-1;
while(ll < r){
    int mid = l+r+1 >>1;
    if(check(mid)){
        l = mid;
    }else{
        r = mid-1;
    }
}

浮点数二分

这个用的比较少,这里的k指的是题目的精度

double find(int left,int right){
    double eps = le-k+2;
    while(right - left > eps){
        double mid = (right+left)/2;
        if(check(mid)){
            r = mid;
        }else{
            l = mid;
        }
    } 
    reutrn l;
}

写出正确的二分

  1. 判断题目是否可以使用二分,二分不仅仅适用于单调的序列,而且适用于有二段性的序列

  2. 通过判断答案所落在的区间,以及mid 归属于那一半区间

  3. 写出check函数,选用不同的模板

  4. 二分终止条件就是 l == r, 可以通过l或者是r指向的值是否是预期值,判断有没有解

704. 二分查找

题目描述

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

思路

这道题就是二分查找的模板题。

首先分析题目,这是一个有序的数组,查找指定值,满足二分的基本条件

这里是一个升序的数组,如果说 num[mid] >= target,那么最终答案会落在左半区间,因为这又是一个升序的数组,如果说mid指向的元素都比目标值大了,那么后面的只可能更大。

最终两个指针都会指向同一个位置,可以通过判断最后指向的位置是否为目标值,来判断是否有解

class Solution {
    public int search(int[] nums, int target) {
        int l = 0;
        int r = nums.length-1;
        while(l < r){
            int mid  = l+r >>1;
            if(nums[mid]>= target){
                r = mid;
            }else{
                l  = mid+1;
            }
        }
        if(nums[l] == target){
            return l;
        }
        return -1;
    }

}

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

题目描述

该题同样是可以用暴力解的,但是这里就不给出解法

在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

思路

一般来说,看到有序的数组,都要去想一想能不能用二分

  1. 如果说查找开始位置,在有解的情况之下,一定会落在候选区间的左半边,所以说我们选择r = mid的这个模板,那么check函数就顺理成章的写出来了,nums[mid]>=target,这样答案就会落在左半区间,那么就得到的就是开始位置

  2. 如果说查找结束位置,与分析开始位置是一致的,在有解的情况之下,一定会落在候选区间的右半边,所以说我们选择l=mid的这个模板,那么check函数就是nums[mid]<=target,这样答案就会落在右半区间,那么得到的就是结束位置

代码

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] ans  = new int[]{-1,-1};
        if(nums.length == 0){
            return new int[]{-1,-1};
        }
        // >= target 中最小的那一个
        int l = 0;
        int r = nums.length-1;
        while(l<r){
            int mid  = l+r>>1;
            if(nums[mid]>=target){
                r = mid;
            }else{
                l = mid+1;
            }
        }

        if(nums[l] != target){
            return ans;
        }
        // <= target 中最大的那一个
        ans[0] = l;
        l = 0;
        r = nums.length-1;
        while(l<r){
            int mid = l+r+1>>1;
            if(nums[mid]<=target){
                l=mid;
            }else{
                r =mid-1;
            }
        }
        if(nums[l]!=target){
            return ans;
        }
        ans[1] =l;
        return ans;

    }
}

69.X的平方根

题目描述

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

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

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

思路

二分模板题
和上述两个题都是同样的分析方法

代码

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

33.搜索旋转排序数组

题目描述

整数数组 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,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

思路

从题目中进行分析,数组中没有重复元素,原来的数组升序的,旋转之
后,我们很容易发现,前半段都是大于新数组的第一个数,后半段都是小于新数组的第一个数,仍然具有二段性,可以使用二分。那么说现在就用两个问题,首先,就是如何去寻找这个分界点,其次,就是分界点确定之后,如果去运用我们的二分模板去寻找答案

  1. 分界点获取

    • 遍历:分界点,后面的元素一定是比分界点出的元素小的
    • 二分:整个序列具有二段性,前半段具有大于num[0] 的性质,后半段具有小于num[0]的性质
  2. 找到分界点之后,如果说target比第一个元素大,那么答案区间一定在多半段,反之,一定在右半段。不管是那一段,都是单调的,合理分析。既可以得到答案

代码

class Solution {
    public int search(int[] nums, int target) {
        if(nums.length <= 0 ){
            return -1;
        }
        int l = 0 ;
        int r = nums.length-1;
        while(l <  r){
            int mid  = l+r+1>>1;
            if(nums[mid]>=nums[0]) l = mid;
            else  r = mid-1;          
        }
        if(target >= nums[0]) l = 0 ;
        else{
            l  =r+1;
            r = nums.length-1;
        }
        while(l < r){
            int mid  = l + r >> 1;
            if(nums[mid]>=target){
                r = mid;
            }else{
                l = mid +1;
            }
        }
        if(nums[r] == target ){
            return r;
        }
        return -1;
    }
}

81.搜索旋转排序数组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

思路
相比于上题,数组中有了重复元素,使得不具有二段性,而没有二段性的原因就是旋转之后,新数组的头和尾中有了重复元素,如果说取出了这些重复元素,数组就重新具有了二段性,如果说数组尾和数组头是一致的,就压缩数组的右边界,然后就和上题一样的做法,之后就不在赘述

代码一

直接遍历,时间复杂度 O(n)

class Solution {
    public boolean search(int[] nums, int target) {
        for(int i  = 0;i<nums.length;i++){
            if(nums[i] == target){
                return true;
            }
        }
        return false;
    }
}

代码二

二分,最坏情况下是O(N),相比之下,更加推荐使用代码一,代码量更加短,时间复杂度一致

class Solution {
    public boolean search(int[] nums, int target) {
        if(nums.length == 0){
            return false;
        }
        int R  = nums.length - 1;
        while(R >= 0 && nums[R] == nums[0] ){
            R--;
        }
        if(R < 0) return nums[0] == target;
        
        int r = R;
        int l = 0;
        while(l < r){
            int mid  = l+r+1 >> 1;
            if(nums[mid] >= nums[0]) l = mid;
            else r = mid-1;
        }
        if(target >= nums[0]){
            r = l;
            l = 0;
        }else{
            l++;
            r = R;
        }
        while(l < r){
            int mid  = l+r>>1;
            if(nums[mid]>=target) r = mid;
            else l = mid+1;
        }
        if(nums[r] == target) return true;
        return false;
    }
}

852.山脉数组的峰顶索引

题意描述

符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i] arr[i] > arr[i+1] > ... > arr[arr.length - 1].
给你由整数组成的山脉数组 arr ,返回任何满足arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]的下标 i 。

思路

这个数组实际上是具有二段性,前面一段是单调递增的,后面一段是单调递减的。我们需要找到分界点下标。如果说当前元素,比他的前一个元素要大的话,说明答案会在右半边

代码

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0;
        int r = n-2;
        while(l<r){
            int mid = l+r+1>>1;
            if(arr[mid]>arr[mid-1]){
                l = mid;
            }else{
                r = mid-1;
            }
        }
        return l;
    }
}

278.第一个错误的版本

题目描述

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数

思路

  1. 从题意中分析,整个序列具有二段性,前面一段是没有错误的版本,后面一段是错误的版本,所以可以用二分法去寻找中间的那个分割点
  2. 分析mid指针所指向的位置,如果说这个位置是错误的版本,那么答案最终回落至左半区间,选择r = mid,的这个二分版本
  3. 只需要将题目中提供的函数作为check函数即可
  4. l == r 就是答案

代码实现

时间复杂度 : O(logn)

空间复杂度 : O(l)

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int l = 1;
        int r = n;
        while(l<r){
            long tem  =(long) l+r>>1;
            int mid = (int)tem;
            if(isBadVersion(mid)){
               r = mid; 
            }else{
                l = mid+1;
            }
        }
        return l;       
    }
}
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

佩奇inging

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值