前言
本文主要整理了二分的三种常用模板,以及力扣上面二分题目的汇总。
文章目录
一、二分模板
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[困难]
问题
已知一个长度为 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[中等]
问题
已知存在一个按非降序排列的整数数组 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[中等]
问题
给你一个整数数组 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 个最小距离。一对 (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;
}
};