LeetCode——二分专题

LeetCode——二分

69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

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

示例 :

输入: 4
输出: 2

输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 
     由于返回类型是整数,小数部分将被舍去。
class Solution {
public:
    int mySqrt(int x) {
        int l = 0, r = x;
        while(l < r){
            int mid = l + (long long)r + 1>> 1;
            if(mid <= x/mid) l = mid;  // 查找t^2 <= x
            else r = mid - 1;
        }
        return l;
    }
};
35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 :

输入: [1,3,5,6], 5
输出: 2

输入: [1,3,5,6], 2
输出: 1

输入: [1,3,5,6], 7
输出: 4

输入: [1,3,5,6], 0
输出: 0
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        if(nums.empty() || target > nums.back()) return nums.size();
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] >= target) r = mid;  // 查找t>=x的第一个数
            else l = mid + 1; 
        }
        return l;
    }
};
34. 在排序数组中查找元素的第一个和最后一个位置

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

你的算法时间复杂度必须是 O(log n) 级别。

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

示例 :

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

输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.empty()) return {-1, -1};
        return {getFirst(nums, target), getLast(nums, target)};
    }
    int getFirst(vector<int>& nums, int target){
        int l = 0, r = nums.size()-1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return nums[l] != target ? -1 : l;
    }
    int getLast(vector<int>& nums, int target){
        int l = 0, r = nums.size()-1;
        while(l < r){
            int mid = l + r + 1 >> 1;
            if(nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        return nums[l] != target ? -1 : l;
    }
};
74. 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

示例 :

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3
输出:true

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 13
输出:false

输入:matrix = [], target = 0
输出:false

注:二维数组下标转换
在这里插入图片描述

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty() || matrix[0].empty()) return false;
        int n = matrix.size(), m = matrix[0].size();
        int l = 0, r = n*m-1;
        while(l < r){
            int mid = l + r >> 1;
            if(matrix[mid/m][mid%m] >= target) r = mid;  // t>=x
            else l = mid + 1; 
        }
        return matrix[l/m][l%m] == target ? true : false;
    }
};
153. 寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 :

输入: [3,4,5,1,2]
输出: 1

输入: [4,5,6,7,0,1,2]
输出: 0
class Solution {
public:
    int findMin(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] <= nums.back()) r = mid;  // nums[t]<=nums.back
            else l = mid + 1;
        }
        return nums[l]; 
    }
};
33. 搜索旋转排序数组

给你一个升序排列的整数数组 nums ,和一个整数 target 。

假设按照升序排序的数组在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

示例 :

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

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

输入:nums = [1], target = 0
输出:-1
class Solution {
public:
    int search(vector<int>& nums, int target) {
        if(nums.empty()) return -1;
        // 找到最小值
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] <= nums.back()) r = mid;  // nums[t] <= nums.back()
            else l = mid + 1;
        }
        if(target <= nums.back()) r = nums.size() - 1;
        else l = 0, r--;
        // 单调二分
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] >= target) r = mid;  // t >= x
            else l = mid + 1;
        }
        return nums[l] == target ? l : -1;
    }
};
278. 第一个错误的版本

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

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

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

示例:

给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。 
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int l = 1, r = n;
        while(l < r){
            int mid = l + (long long)r >> 1;  // n为最大整数,溢出
            if(isBadVersion(mid)) r = mid;
            else l = mid + 1; 
        }
        return l;
    }
};
162. 寻找峰值

峰值元素是指其值大于左右相邻值的元素。

给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。

数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞。

示例 :

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5 
解释: 你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。
说明:
你的解法应该是 O(logN) 时间复杂度的。
class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int mid = l + r >> 1;
            if(nums[mid] < nums[mid + 1]) l = mid + 1;
            else r = mid;
        }
        return l;
    }
};
287. 寻找重复数

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 :

输入: [1,3,4,2,2]
输出: 2

输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size()-1;
        int l = 1, r = n;
        while(l < r){
            int cnt = 0;
            int mid = l + r >> 1;
            for(int x : nums)
                if(x <= mid) cnt++;  // 统计中位数及左边数字的个数
            if(cnt > mid) r = mid;
            else l = mid+1; 
        }
        return l;
    }
};
275. H 指数 II

给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照 升序排列 。编写一个方法,计算出研究者的 h 指数。

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

示例:

输入: citations = [0,1,3,5,6]
输出: 3 
解释: 给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
     由于研究者有 3 篇论文每篇至少被引用了 3 次,其余两篇论文每篇被引用不多于 3 次,所以她的 h 指数是 3。

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

思路:

  • 至少存在h个数≥h
    即看倒数第h个数是否≥h
class Solution {
public:
    int hIndex(vector<int>& nums) {
        int l = 0, r = nums.size();
        while(l < r){
            int mid = l + r + 1>> 1;
            if(nums[nums.size() - mid] >= mid) l = mid;  // 左边都满足nums[h]>=h,判断倒数第h个数是否≥h
            else r = mid - 1;
        }
        return l;
    }
};

参考链接:


扩展分类

练习题

题型一:在数组中查找符合条件的元素的下标
一般而言这个数组是有序的,也可能是半有序的(旋转有序数组或者山脉数组)。

题目提示与题解
704. 二分查找(简单)二分查找的最原始问题,使用本题解介绍的方法就要注意,需要后处理。
34. 在排序数组中查找元素的第一个和最后一个位置(中等)查找边界问题,题解(有视频讲解)
33. 搜索旋转排序数组(中等)题解,利用局部单调性,逐步缩小搜索区间(其它问题类似)
81. 搜索旋转排序数组 II(中等)题解
153. 寻找旋转排序数组中的最小值(中等)题解
154. 寻找旋转排序数组中的最小值 II(中等)题解
300. 最长上升子序列(中等)特别经典的一道「动态规划」,二分查找的思路是基于「动态规划」的状态定义得到,代码很像第 35 题,题解
275. H 指数 II(中等)题解
852. 山脉数组的峰顶索引(简单)利用局部单调性,逐步缩小搜索区间。
1095. 山脉数组中查找目标值(中等)官方题解(有视频讲解)题解
4. 寻找两个正序数组的中位数(困难))官方题解(有视频讲解)题解
658. 找到 K 个最接近的元素(中等)题解,这个问题二分的写法需要做复杂的分类讨论,可以放在以后做

题型二:在一个有范围的区间里搜索一个整数
定位一个有范围的整数,这件事情也叫「二分答案」或者叫「二分结果」。如果题目要求的是一个整数,这个整数有明确的范围,可以考虑使用二分查找。

题目提示与题解
69. 平方根(简单)题解,在一个整数范围里查找一个整数,也是二分查找法的应用场景。
287. 寻找重复数(中等)题解,在一个整数范围里查找一个整数。这个问题二分查找的解法很反常规,知道即可。
374. 猜数字大小(简单)题解
1300. 转变数组后最接近目标值的数组和题解

题型三:复杂的二分查找问题(判别条件需要遍历数组)
「力扣」上还有这样一类问题:目标变量和另一个变量有相关关系(一般而言是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测(满足某种意义上的单调性)。这样的问题的判别函数通常会写成一个函数的形式。

这一类问题可以统称为「 最大值极小化 」问题,最原始的问题场景是木棍切割问题,这道题的原始问题是「力扣」第 410 题。

解题的思路是这样的:

  • 分析出题目要我们找一个整数,这个整数有范围,所以可以用二分查找;
  • 分析出单调性,一般来说是一个变量 a 的值大了,另一个变量 b 的值就变小,而「另一个变量的值」 b 有限制,因此可以通过调整 a 的值达到控制 b 的效果;
  • 这一类问题的题目条件一定会给出 连续、正整数 这样的关键字,如果没有,问题场景也一定蕴含了这两个关键信息。

以下给出的问题无一例外。

题目提示与题解
410. 分割数组的最大值(困难)题解
875. 爱吃香蕉的珂珂(中等)题解
LCP 12. 小张刷题计划(中等)(题解在第 410 题题解里)
1482. 制作 m 束花所需的最少天数(中等)(题解在第 1300 题题解里)
1552. 两球之间的磁力(中等)

LeetBook总结

  • 704. 二分查找(简单)

    class Solution {
    public:
        int search(vector<int>& nums, int target) {
            if(nums.empty()) return -1;
            int l = 0, r = nums.size() - 1;
            while(l < r){
                int mid = l + r >> 1;
                if(nums[mid] >= target) r = mid;
                else l = mid + 1;
            }
            return nums[l] == target ? l : -1; 
        }
    };
    
  • 374. 猜数字大小

    /** 
     * Forward declaration of guess API.
     * @param  num   your guess
     * @return 	     -1 if num is lower than the guess number
     *			      1 if num is higher than the guess number
     *               otherwise return 0
     * int guess(int num);
     */
    
    class Solution {
    public:
        int guessNumber(int n) {
            int l = 1, r = n;
            while(l < r){
                int mid = l + (long long)r >> 1;
                int res = guess(mid);
                if(res <= 0) r = mid;
                else l = mid + 1;
            }
            return l;
        }
    };
    
  • 81. 搜索旋转排序数组 II(中等)
    【分析】与lc33类似,多了重复数字的情况,只需去掉旋转数组结尾和开头相同的部分,剩下的与lc33完全相同。

    class Solution {
    public:
        bool search(vector<int>& nums, int target) {
            if(nums.empty()) return false;
            // 去掉结尾和开头相同的部分
            int t = nums.size() - 1;
            while(t > 0 && nums[t] == nums[0]) t--;
            // 找最小值
            int l = 0, r = t;
            while(l < r){
                int mid = l + r >> 1;
                if(nums[mid] <= nums[t]) r = mid;
                else l = mid + 1;
            }
            if(target <= nums[t]) r = t;  // 答案在右边
            else l = 0, r--;  // 答案在左边
            // 单调二分
            while(l < r){
                int mid = l + r >> 1;
                if(nums[mid] >= target) r = mid;
                else l = mid + 1;
            }
            return nums[l] == target;
        }
    };
    
  • 154. 寻找旋转排序数组中的最小值 II(中等)

    【分析】与lc153类似,多了重复数字的情况,只需去掉旋转数组结尾和开头相同的部分,剩下的与lc153完全相同。

    class Solution {
    public:
        int findMin(vector<int>& nums) {
            int t = nums.size()-1;
            while(t > 0 && nums[t] == nums[0]) t--;  // 去掉结尾与开头相同的部分
            int l = 0, r = t;
            while(l < r){
                int mid = l + r >> 1;
                if(nums[mid] <= nums[t]) r = mid;
                else l = mid + 1;
            }
            return nums[l];
        }
    };
    
  • 852. 山脉数组的峰顶索引(简单)

    【思路】找峰顶,判断是否有 M ≥ M+1

    class Solution {
    public:
        int peakIndexInMountainArray(vector<int>& nums) {
            int l = 0, r = nums.size() - 1;
            while(l < r){
                int mid = l + r >> 1;
                if(nums[mid] >= nums[mid+1]) r = mid;  // M >= M+1
                else l = mid + 1;
            }
            return l;
        }
    };
    
  • 1095. 山脉数组中查找目标值(中等)

    【思路】先找到峰顶,左右区间找target

      /**
       * // This is the MountainArray's API interface.
       * // You should not implement it, or speculate about its implementation
       * class MountainArray {
       *   public:
       *     int get(int index);
       *     int length();
       * };
       */
      
      class Solution {
      public:
          int findInMountainArray(int target, MountainArray &nums) {
              // 找到峰顶,最大值
              int l = 0, r = nums.length() - 1;
              while(l < r){
                  int mid = l + r >> 1;
                  if(nums.get(mid) >= nums.get(mid+1)) r = mid;
                  else l = mid + 1;
              }
              int t = l;  // t为峰顶
              // 在左侧递增区间找target
              l = 0, r = t;
              while(l < r){
                  int mid = l + r >> 1;
                  if(nums.get(mid) >= target) r = mid;
                  else l = mid + 1; 
              }
              if(nums.get(l) == target) return l;
              // 在右侧递减区间找target
              l = t + 1, r = nums.length() - 1;
              while(l < r){
                  int mid = l + r >> 1;
                  if(nums.get(mid) <= target) r = mid;
                  else l = mid + 1;
              }
              if(nums.get(l) == target) return l;
              else return -1;
          }
      };
    
  • 658. 找到 K 个最接近的元素(中等)

    【思路】只寻找左边界即可。

    假设 mid 是左边界,则当前区间覆盖的范围是 [mid, mid + k -1]. 如果发现 a[mid] 与 x 距离比 a[mid + k] 与 x 的距离要大,说明解一定在右侧,否则在左侧。

    class Solution {
    public:
        vector<int> findClosestElements(vector<int>& nums, int k, int x) {
            int l = 0, r = nums.size() - k;  // 保证mid+k不超出范围
            while(l < r){
                int mid = l + r >> 1;
                if(x - nums[mid] <= nums[mid+k] - x) r = mid;
                else l = mid + 1; 
            }
            return vector<int>(nums.begin()+l, nums.begin()+l+k);
        }
    };
    
  • 1300. 转变数组后最接近目标值的数组和
    【思路】二分查找确定这个整数值(阈值越大,转变数组和越大,具有单调性,可用二分,对比lc658)
    如果选择一个阈值 value ,使得它对应的 sum 是第 1 个大于等于 target 的,那么目标值可能在 value 也可能在 value - 1
    在这里插入图片描述

     class Solution {
     public:
         int findBestValue(vector<int>& arr, int target) {
             int l = 0, r = 0;
             for(int num : arr)
                 r = max(r, num);  // 找数组中的最大值作为右边界
             // 二分找到第一个大于等于target的阈值val
             while(l < r){  
                 int mid = l + r >> 1;
                 int sum = calSum(arr, mid);
                 if(sum >= target) r = mid;
                 else l = mid + 1;
             }
             // 找最接近target左右两边最小的数
             int sum1 = calSum(arr, l - 1);
             int sum2 = calSum(arr, l);
             if(target - sum1 <= sum2 - target)  // k-x1 <= x2-k
                 return l - 1;
             return l;
         }
         int calSum(vector<int>& arr, int val){  // 计算转变数组的和
             int sum = 0;
             for(int num : arr)
                 sum += min(num, val);
             return sum;
         }
     };
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值