力扣——二分专题

前言

本文主要整理了二分的三种常用模板,以及力扣上面二分题目的汇总。


一、二分模板

1 模板一

  • 初始条件:left = 0, right = length-1
  • 循环条件:left <= right
  • 终止条件:left > right
  • 搜索区间:[left, right]
  • 向右查找:left = mid + 1
  • 向左查找:right = mid -1
int binarySearch(vector<int>& nums, int target){
  if(nums.size() == 0)
    return -1;

  int left = 0, right = nums.size() - 1;
  while(left <= right){
    int mid = left + (right - left) / 2;
    if(nums[mid] == target) 
    	return mid; 
    else if(nums[mid] < target) 
    	left = mid + 1; 
    else  
    	right = mid - 1; 
  }

  // End Condition: left > right
  return -1;
}

例题

  • x 的平方根
    问题:给你一个非负整数 x ,计算并返回 x 的 算术平方根 。由于返回类型是整数,结果只保留 整数部分 ,小数部分将被舍去 。注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
    在这里插入图片描述
    题解
    class Solution {
    public:
        int mySqrt(int x) {
            if(x<2) return x;
            int l = 1,r=x/2;
            while(l<=r){
                long m=l+(r-l)/2;
                if(m*m==x) 
                    return m;
                else if(m*m<x)
                    l = m+1;
                else if(m*m>x)
                    r = m-1;            
            }
            return r;//跳出循环后,l = r + 1,向下取整,取r
        }
    };
    

2 模板二——寻找左侧边界

  • 初始条件:left = 0, right = length
  • 循环条件:left < right
  • 终止条件:left == right
  • 搜索区间:[left, right)
  • 向右查找:left = mid + 1
  • 向左查找:right = mid
    返回的left表示比target小的元素有几个
int binarySearch(vector<int>& nums, int target){
  if(nums.size() == 0)
    return -1;

  int left = 0, right = nums.size();
  while(left < right){
    int mid = left + (right - left) / 2;
    if(nums[mid] < target)  
    	left = mid + 1; 
    else  
    	right = mid; 
  }
  return nums[left] == target ? left : -1;
}

例题

  • 在排序数组中查找元素的第一个和最后一个位置
    问题:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ans(2,-1);
        if(nums.size()==0) return ans;
        int l=0,r=nums.size();
        while(l<r){//寻找左边界
            int m = l+(r-l)/2;
            if(target>nums[m])
                l=m+1;
            else
                r = m;  //当=的时候,缩小右边界
        }
        if(l<=nums.size()-1&&target==nums[l]) ans[0] = l;
        l=0,r=nums.size();
        while(l<r){//寻找右边界
            int m = l+(r-l)/2;
            if(target>=nums[m])
                l=m+1; //当=的时候,扩大左边界
            else
                r = m;
        }
        if(l-1>=0&&target==nums[l-1]) ans[1]=l-1;
        return ans;

    }
};
  • 找到 K 个最接近的元素
    问题:给定一个 排序好 的数组 arr ,两个整数 k 和 x ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。整数 a 比整数 b 更接近 x 需要满足:|a - x| < |b - x| 或者|a - x| == |b - x| 且 a < b
class Solution {
public:
    vector<int> findClosestElements(vector<int>& arr, int k, int x) {
        int l=0,r=arr.size()-k;
        while(l<r){
            int m=l+(r-l)/2;
            if(x - arr[m] > arr[m+k]-x) //巧妙之处
                l = m+1;
            else
                r = m;
        }
        vector<int> ans(arr.begin()+l,arr.begin()+l+k);
        return ans;


    }
};

3 模板三

  • 初始条件:left = 0, right = length-1
  • 循环条件:left +1< right
  • 终止条件:left +1 == right
  • 搜索区间:[left, right]
  • 向右查找:left = mid
  • 向左查找:right = mid
int binarySearch(vector<int>& nums, int target){
    if (nums.size() == 0)
        return -1;

    int left = 0, right = nums.size() - 1;
    while (left + 1 < right){
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if(nums[left] == target) return left;
    if(nums[right] == target) return right;
    return -1;
}

例题

  • 寻找峰值
    问题:峰值元素是指其值严格大于左右相邻值的元素。给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。你可以假设 nums[-1] = nums[n] = -∞ 。你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int l=0,r=nums.size()-1;
        while(l+1<r){
            int m=l+(r-l)/2;
            if(m+1<nums.size()&&nums[m]<nums[m+1])
                l = m;
            else 
                r = m;
        }
        return nums[l]>nums[r]?l:r; //最后剩余两个元素,l,r,返回大的一个下标

    }
};

二、基础二分

1.搜索插入位置[简单]

搜索插入位置

问题

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。必须使用时间复杂度为 O(log n) 的算法。
在这里插入图片描述

题解

直接使用二分模板就好了,注意若没有找到,left为应该插入的位置。

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0,right = nums.size()-1,fin = -1 ;
        while(left<=right){
            int mid = (left + right)/2;
            if(nums[mid] <= target){
                left = mid + 1;
                fin = mid;
            }
            else right = mid - 1;
        }
        if(fin == -1 || nums[fin] != target) return left;
        return fin;

    }
};

2.寻找旋转排序数组中的最小值[中等]

寻找旋转排序数组中的最小值

问题

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
在这里插入图片描述

题解

比较 mid 和 right 的值。
如果 nums[mid] <= nums[right] 右半段一定有序,最小值可能在左边,或者刚好mid位置
如果 nums[mid] > nums[right] 左半段一定有序,最小值一定在右边
当 left == right 的时候,退出循环,找到最小值

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        while(left < right){
            int mid = left + (right-left)/2;
            if(nums[mid] <= nums[right]){
                right = mid; //右半段一定是有序的,乱序在左边
            }else {
                left = mid + 1; //左边一定是有序的,乱序在右边
            }
        } 
        return nums[left];
    }
};

3 .寻找旋转排序数组中的最小值II[困难]

寻找旋转排序数组中的最小值II

问题

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
在这里插入图片描述

题解

对比上一道题,需要额外判断等于的情况
比较 mid 和 right 的值。
如果 nums[mid] < nums[right] 右半段一定有序,最小值可能在左边,或者刚好mid位置
如果 nums[mid] > nums[right] 左半段一定有序,最小值一定在右边
如果 nums[mid] = nums[right] right-- 直到打破这种僵局
当 left == right 的时候,退出循环,找到最小值

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        while(left < right){
            int mid = left + (right - left)/2;
            if(nums[mid] < nums[right]){
                right = mid; //右侧有序,最小值在左边或者mid的位置
            }else if(nums[mid] > nums[right]){
                left = mid + 1; //左侧有序,最小值在右侧的部分
            }else if(nums[mid] == nums[right]){
                right --;
            }
        }
        return nums[left];

    }
};

4 .搜索旋转排序数组[中等]

搜索旋转排序数组

问题

整数数组 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 。

在这里插入图片描述

题解

一定有半段是有序的,只对有序的部分进行判断

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if (target == nums[mid]) return mid;
            if(nums[mid] <= nums[right]){//右侧一定有序
                if(target > nums[mid] && target <= nums[right])
                    left = mid + 1;
                else 
                    right = mid -1;
            }else{//左侧一定有序
                if(target >= nums[left] && target < nums[mid])
                    right = mid -1;
                else
                    left = mid + 1;
            }
        }
        return -1;
    }
   
};

5 .搜索旋转排序数组II[中等]

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

在这里插入图片描述

题解

一定有半段是有序的,只对有序的部分进行判断
比上一道题多出重复的判断

public:
    bool search(vector<int>& nums, int target) {
         //首先找到乱序最小值的位置
        int left = 0, right = nums.size()-1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(nums[mid] == target) return true;
            else if(nums[mid] < nums[right]){//右侧一定有序
                if(target > nums[mid] && target <= nums[right])
                    left = mid + 1;
                else
                    right = mid -1;
            }else if(nums[mid] > nums[right]){//左侧一定有序
                if(target < nums[mid] && target >= nums[left])
                    right = mid -1;
                else 
                    left = mid + 1;
            }else{
                right--;
            }
        }
        return false;

    }
};

6.供暖气[中等]

供暖气

问题

冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。

在加热器的加热半径范围内的每个房屋都可以获得供暖。

现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。

说明:所有供暖器都遵循你的半径标准,加热的半径也一样。

在这里插入图片描述

题解

首先,对于每一个房子,找到最小的大于等于该位置的供暖。
然后,计算前一个和当前位置供暖到该位置的距离,选择最小的那个,即为当前房子最小供暖距离。
找到所有房子供暖距离最大的

class Solution {
public:
    int findRadius(vector<int>& houses, vector<int>& heaters) {
        int ans = 0;
        sort(houses.begin(),houses.end());
        sort(heaters.begin(),heaters.end());
        int dis = INT_MAX;
        for(int i = 0; i < houses.size(); i++){
            //对于每一个房子,找到最小的大于等于该位置的供暖
            int left = 0 ,right = heaters.size()-1;
            while(left <= right){
                int mid = (left + right) / 2;
                if(houses[i]>heaters[mid]){
                    left = mid + 1;
                }else if(houses[i]<heaters[mid]){
                    right = mid -1;
                }else{
                    left = mid;
                    break;
                }
            }
            int pre = left - 1 >= 0 ? houses[i]-heaters[left-1] : INT_MAX;
            int next = left < heaters.size() ? heaters[left]-houses[i]: INT_MAX;
            dis = min(pre,next);
            ans = max(ans,dis);
        }
        return ans;
    }
};

7.H 指数 II[中等]

H 指数II

问题

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。且其余的 n - h 篇论文每篇被引用次数 不超过 h 次。

提示:如果 h 有多种可能的值,h 指数 是其中最大的那个。

请你设计并实现对数时间复杂度的算法解决此问题。

在这里插入图片描述

题解

引用次数是按照升序排列的,典型的二分,如果直接套用二分模板的话如下:

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int l = 0, r = citations.size()-1,fin = -1;
        while(l <= r){
            int mid = (l + r)/2;
            int dis = citations.size() - mid;
            if(citations[mid] <= dis){
                l = mid + 1;
                fin = mid;
            }else{
                r = mid - 1;
            }
        }
        if(fin == -1 || citations[fin]!=citations.size()-fin)//没有找到的情况
             return citations.size()-l;
        return citations.size()-fin;
    }
};

或者更简洁一些的写法,这里就需要对边界条件有很好的判断。

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int left = 0, right = citations.size()-1,fin = -1;
        while(left <= right){
            int mid = left + (right - left)/2;
            int dis = citations.size()-mid;
           if(citations[mid] >= dis){
               right = mid -1;
              
           }else{
                left = mid + 1;
           }
        }
      
        return citations.size() - left;

    }
};

8.小张刷题计划[中等]

小张刷题计划

问题

为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。

在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。

我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。
在这里插入图片描述

题解

题意为,给定一个M,把数组分成M份,设计一种方式,找到所有M份中的数值和最大的那个,让这个数尽可能小(每组可减去一个最大值,不过这不重要,二分思想是不变的)。
思路:
对数组所有数值相加的总和进行二分,mid代表分成所有M份数组的最大值,使用check函数检验,当采用此mid划分出的段数小于M时,说明当前mid值太大了,提前就分完了,需要减小mid的值;当采用此mid划分出的段数大于M时,说明当前mid值太小了,M天内无法分完所有数值,需要增大mid的值。

class Solution {
public:
    bool check(vector<int>& time,int T, int m){//判断在T下是否可以完成分组
        int curm = 1,leftSize = T,maxn = 0,flag = true;
        for(int i=0; i<time.size();i++){
            maxn = max(maxn,time[i]);
            if(time[i] <= leftSize){//直接加入
                leftSize -= time[i];
            }else if(flag && time[i] <= leftSize + maxn){
                leftSize += maxn;
                flag = false;
                i--;
            } else{
                curm++;
                leftSize = T;
                maxn = 0;
                flag = true;
                i--;
            }
        }
        return curm <= m;
    }
    int minTime(vector<int>& time, int m) {
        //对时间总和二分,找到各个数组和的最大值
        int l = 0, r = 0 ;
        for(int i=0; i<time.size();i++){
            r += time[i];
        }
        while(l <= r){
            int mid = (l + r)/2;
            if(check(time,mid,m)){//如果在当前最大值内可以完成分组,则可以缩小mid 的值
                r = mid -1;
            }else{ //值太小了,无法完成分组
                l = mid + 1;
            }
        }
        return l;
    }
};

三、更多练习

1.寻找重复数[中等]

寻找重复数

问题

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。
你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。

在这里插入图片描述

题解

因为限制了数值范围是1 到 n 之间,共n+1个数,所以必然有一个数出现两次,那么我们可以以出现次数作为二分,从1到n,存在某个数t,使得小于t的时候,出现的总次数递增且小于当前数值,大于t的时候,出现的总次数大于当前数值

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        //对每个数字出现的次数进行二分,
        //每个数字出现的次数按照数字顺序单调,
        //对于目标target,当i<tagtet count<=i,当i>target,count>i
        int l=1,r=nums.size();
        while(l<r){
            int m=l+(r-l)/2,count=0;
            for(int i=0;i<nums.size();i++){
                if(nums[i]<=m)
                 count++;
            }
            if(count<=m)
                l = m +1;
            else
                r = m;
        }
        return l;

    }
};

2.找出第 k 小的距离对[困难]

找出第 k 小的距离对

问题

给定一个整数数组,返回所有数对之间的第 k 个最小距离。一对 (A, B) 的距离被定义为 A 和 B 之间的绝对差值。

在这里插入图片描述

题解

class Solution {
public:
   int check(vector<int> nums, int dis) {
        int left = 0, cnt = 0;
        for (int right = 0; right < nums.size(); ++right) {
            while (nums[right] - nums[left] > dis) {
                left++;
            }
            cnt += right - left;
        }
        return cnt;
    }

    int smallestDistancePair(vector<int>& nums, int k) {
        sort(nums.begin(),nums.end());
        int l=0,r = nums[nums.size()-1] - nums[0];
        //对距离二分,给定一个距离mid,求小于该mid的数对
        //如果数对小于k,则增加mid
        //反之减小mid
        while(l<r){
            int m = l+(r-l)/2;
            if(check(nums,m)<k)
                l = m + 1;
            else
                r = m;
        }
        return l;

    }
};

3.分割数组的最大值[困难]

分割数组的最大值

问题

给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。

设计一个算法使得这 m 个子数组各自和的最大值最小。

在这里插入图片描述

题解

class Solution {
public:
    int check(vector<int>& nums, int mid){
        int count = 1,sum=0;
        for(int i=0;i<nums.size();i++){  
            if(nums[i]>mid)
             return INT_MAX;
            if(sum+nums[i]<=mid){
                sum += nums[i];
            }else{
                count++;
                sum = 0;
                i--;  
            }

        }
        return count;
    }
    int splitArray(vector<int>& nums, int m) {
        int l=0,r=0;
        for(int i=0;i<nums.size();i++){
            r+=nums[i];
        }
        //对数组的总和进行二分,mid为最小的数组值
        //如果采用当前mid正好划分结束,则当前mid即为所求
        //如果采用当前mid需要的段数少于m,说明Mid太大了,需要减小
        //如果采用当前mid需要的段数大于m,说明Mid太小了,需要增大
        int mid,ans=INT_MAX;
        while(l<r){
            mid = l+(r-l)/2;
            int count = check(nums,mid);
            if(count>m)
                l = mid +1;
            else
                r = mid;
        }
        return l;

    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值