leetcode 100热题二分篇
本篇还包含了其他关于二分的题目
4. 寻找两个正序数组的中位数
算法
本题其实和二分关系不大 核心是找出第k个数 其中k = (M + N) / 2 (此时是针对一个数组的情况 ) 对于两个数组就要对于各自k/2 的数进行讨论.以下为具体思路:
给定两个有序的数组,找中位数(n + m) / 2
,等价于找第k
小的元素,k = (n + m) / 2
- 1、当一共有偶数个数时,找到第
total / 2
小left
和第total / 2 + 1
小right
,结果是(left + right / 2.0)
- 2、当一共有奇数个数时,找到第
total / 2 + 1
小,即为结果
如何找第k
小?
- 1、默认第一个数组比第二个数组的有效长度小
- 2、第一个数组的有效长度从
i
开始,第二个数组的有效长度从j
开始,其中[i,si - 1]
是第一个数组的前k / 2
个元素,[j, sj - 1]
是第二个数组的前k - k / 2
个元素 - 3、当
nums1[si - 1] > nums2[sj - 1]
时,则表示第k
小一定在[i,n]
与[sj,m]
中 - 4、当
nums1[si - 1] <= nums2[sj - 1]
时,则表示第k
小一定在[si,n]
与[j,m]
中
代码
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int tot = nums1.size() + nums2.size();
//偶数 中位数为中间两个数的平均数
if(tot % 2 == 0){
int left = find(nums1,0,nums2,0,tot / 2);
int right = find(nums1,0,nums2,0,tot / 2 + 1);
return (left + right) / 2.0;//2.0 是为了保证输出值为浮点数
}else return find(nums1,0,nums2,0,tot / 2 + 1);
}
int find(vector<int>& nums1,int i,vector<int>& nums2,int j,int k)
{
// 保证前一个数组大小比第二个数组小,[i, nums1.size() - 1] 表示数组 1 的长度,[j, nums2.size() - 1] 表示数组 2 的长度
if(nums1.size() - i > nums2.size() - j) return find(nums2,j,nums1,i,k);
// 如果第一个数组遍历结束,则返回第二个数组
if(nums1.size() == i) return nums2[j + k - 1];//k从1开始
// 当取出第 1 个元素
if(k == 1) return min(nums1[i],nums2[j]);
//第一个数组长度较小 si 取min(nums1.size(),i + k / 2) 防止越界
int si = min((int)nums1.size(),i + k / 2),sj = j + k - k / 2;
//当nums1[si - 1] > nums2[sj - 1]时,则表示第k小一定在[i,n]与[sj,m]中
//此时[j,sj] 是要删去的部分
if(nums1[si - 1] > nums2[sj - 1]) return find(nums1,i,nums2,sj,k - (sj - j));
else return find(nums1,si,nums2,j,k - (si - i));
}
};
33.搜索旋转排序数组
这道题其实是要我们明确「二分」的本质是什么。
「二分」不是单纯指从有序数组中快速找某个数,这只是「二分」的一个应用。
「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。
经过旋转的数组,显然前半段满足 >= nums[0]
,而后半段不满足 >= nums[0]
。我们可以以此作为依据,通过「二分」找到旋转点。
找到旋转点之后,再通过比较 target
和 nums[0]
的大小,确定 target
落在旋转点的左边还是右边。
代码
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0,r = nums.size() - 1;
//首先二分两端区间的旋转点
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(nums[mid] >= nums[0]) l = mid;
else r = mid - 1;
}
//判断target在旋转点的左边还是右边
if(target >= nums[0]) l = 0;//在左边
else l = r + 1,r = nums.size() - 1;//在右边
// 在区间 [l, r] 内二分 target
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;
}
};
34.在排序数组中查找元素的第一个和最后一个位置
解题思路
(二分) O(logn)
题目要求找两个位置,显然需要做两次二分
二分左端点的条件:找到第一个大于等于 target 的数
二分结束,如果左端点对应的值不是 target,说明 target不存在,直接返回 −1,−1,否则继续二分右端点
二分右端点的条件:找到最后一个小于等于 target 的数
时间复杂度
二分的时间复杂度为 O(logn)
代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1,-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;
}
if(nums[r] != target) return {-1,-1};
int L = r;//记录左端点
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 {L,r};
}
};
2529.正整数和负整数的最大计数
二分查找
思路与算法
由于数组呈现非递减顺序,因此可通过二分查找定位第一个数值大于等于 0 的位置 pos1 及第一个数值大于等于 1 的下标 pos2。假定 n 表示数组长度,且数组下标从 0,则负数的个数为 pos1,正数的个数为 n−pos2,返回这两者的较大值即可。
代码
class Solution {
public:
//返回>=val 的数的第一个下标
int lowerBound(vector<int>& nums,int val)
{
int l = 0,r = nums.size();
while(l < r)
{
int mid = (l + r) >> 1;
if(nums[mid] >= val) r = mid;
else l = mid + 1;
}
return l;
}
int maximumCount(vector<int>& nums) {
int neg = lowerBound(nums,0);//负整数个数
int pos = nums.size() - lowerBound(nums,1);//正整数个数
return max(neg,pos);
}
};
2300.咒语和药水的成功对数
为了方便,我们将 spells
记为 a
,将 potions
记为 b
,将 success
记为 t
。
对于每个 a[i],有多少个 b[j] 满足 a[i]×b[j]⩾t,等价于问数组 b
中值大于等于 a[i]t 的个数,这容易让我们想到先对数组 b
排升序,再通过二分找到满足该条件的最小下标,从该下标到数组结尾,均为满足条件的 b[j]。
代码
class Solution {
public:
vector<int> successfulPairs(vector<int>& spells, vector<int>& potions, long long success) {
int n = spells.size(),m = potions.size();
vector<int> res(n);
sort(potions.begin(),potions.end());
for(int i = 0;i < n;i++)
{
//这里 *1.0 是为了转换为浮点数
double target = success * 1.0 / spells[i];
int l = 0,r = m - 1;
while(l < r)
{
int mid = l + r >> 1;
if(potions[mid] >= target) r = mid;
else l = mid + 1;
}
// 最后特判一下 不满足 则res[i] = 0
if((long long)spells[i] * potions[r] >= success) res[i] = m - r;
}
return res;
}
};
275.H指数II
基本分析
为了方便,将 citations
记为 cs
。
所谓的 h
指数是指一个具体的数值,该数值为“最大”的满足「至少发表了 x
篇论文,且每篇论文至少被引用 x
次」定义的合法数,重点是“最大”。
用题面的实例 1 来举个 🌰,给定所有论文的引用次数情况为 cs = [0,1,3,5,6]
,可统计满足定义的数值有哪些:
- h=0,含义为「至少发表了 0 篇,且这 0 篇论文至少被引用 0 次」,空集即满足,恒成立;
- h=1,含义为「至少发表了 1 篇,且这 1 篇论文至少被引用 1 次」,可以找到这样的组合,如
[1]
,成立; - h=2,含义为「至少发表了 2 篇,且这 2 篇论文至少被引用 2 次」,可以找到这样的组合,如
[3, 5]
,成立; - h=3,含义为「至少发表了 3 篇,且这 3 篇论文至少被引用 3 次」,可以找到这样的组合,如
[3, 5, 6]
,成立; - h=4,含义为「至少发表了 4 篇,且这 4 篇论文至少被引用 4 次」,找不到这样的组合,不成立;
- …
实际上,当遇到第一个无法满足的数时,更大的数值就没必要找了。一个简单的推导:
至少出现 k 次的论文数不足 k 篇 => 至少出现 k+1 次的论文必然不足 k 篇 => 至少出现 k+1 次的论文必然不足 k+1 篇(即更大的 h 不满足)。
计数
首先,仍能使用「计数」的方式进行求解,该求解为线性复杂度,且不要求数组有序。
根据分析,最大的 h
不超过 n。
假设我们预处理出引用次数所对应的论文数量 cnt
,其中 cnt[a] = b
含义为引用次数 恰好 为 a
的论文数量有 b
篇。
那么再利用 h
是“最大”的满足定义的合法数,我们从 n 开始往前找,找到的第一个满足条件的数,即是答案。
具体的,创建 cnt
数组,对 cs
进行计数,由于最大 h
不超过 n,因此对于引用次数超过 n 的论文,可等价为引用次数为 n,即有计数逻辑 cnt[min(c, n)]++
。
再根据处理好的 cnt
,从 n 开始倒序找 h
。
由于我们处理的 cnt[a]
含义为引用次数 恰好 为 a
,但题目定义则是 至少。同时「至少出现 k+1 次」的集合必然慢「至少出现 k 次」要求(子集关系),我们可以使用变量 tot
,对处理过的 cnt[i]
进行累加,从而实现从 恰好 到 至少 的转换。
代码
class Solution {
public:
int hIndex(vector<int>& citations) {
int n = citations.size();
vector<int> cnt(n + 1,0);
for(int c : citations) cnt[min(c,n)]++;
for(int i = n,tot = 0;i >= 0;i--)
{
//完成从恰好到至少的转化
tot += cnt[i];
//满足每篇论文至少被引用 x 次
if(tot >= i) return i;
}
return -1;
}
};
二分答案(线性 check
)
除了线性复杂度的「计数」做法,我们还容易想到「二分」。
其中最容易想到的是「二分答案」,该做法复杂度为 O(nlogn),同样并不要求数组有序。
我们发现对于任意的 cs
(论文总数量为该数组长度 n),都必然对应了一个最大的 h
值,且小于等于该 h
值的情况均满足,大于该 h
值的均不满足。
那么,在以最大 h
值为分割点的数轴上具有「二段性」,可通过「二分」求解该分割点(答案)。
最后考虑在什么值域范围内进行二分?
一个合格的二分范围,仅需确保答案在此范围内即可。
再回看我们关于 h
的定义「至少发表了 x
篇论文,且每篇论文至少被引用 x
次」,满足条件除了引用次数,还有论文数量,而总的论文数量只有 n,因此最大的 h
只能是 n 本身,而不能是比 n 大的数,否则论文数量就不够了。
综上,我们只需要在 [0,n] 范围进行二分即可。对于任意二分值 mid
,只需线性扫描 cs
即可知道其是否合法。
代码
class Solution {
public:
int hIndex(vector<int>& citations) {
int n = citations.size();
int l = 0,r = n;
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(check(citations,mid)) l = mid;
else r = mid - 1;
}
return r;
}
bool check(vector<int>& citations,int x)
{
int cnt = 0;
for(int c : citations)
{
if(c >= x) cnt++;
}
return cnt >= x;
}
};
- 时间复杂度:对 [0,n] 做二分,复杂度为 O(logn);
check
函数需要对数组进行线性遍历,复杂度为 O(n)。整体复杂度为 O(nlogn) - 空间复杂度:O(1)
二分下标(根据与 cs[i] 关系)
在上述二分中,我们没有利用本题的「数组有序」的特性。
根据对 h
定义,若 cs 升序,我们可推导出:
- 在最大的符合条件的分割点 x 的右边(包含分割点),必然满足 cs[i]>=x
- 在最大的符合条件的分割点 x 的左边,必然不满足 cs[i]>=x
因此,我们可以利用 分割点右边数的个数与分割点 cs[x]的大小关系进行二分 。
假设存在真实分割点下标 x,其值大小为 cs[x],分割点右边的数值个数为 n−x,根据 H 指数
的定义,必然有 cs[x]>=n−x 关系:
- 在分割点 x 的右边:cs[i] 非严格单调递增,数的个数严格单调递减,仍然满足 cs[i]>=n−i 关系;
- 在分割点 x 的左边:cs[i] 非严格单调递减,数的个数严格单调递增,x 作为真实分割点,必然不满足 cs[i]>=n−i 关系。
利用此「二段性」进行二分即可,二分出下标后,再计算出数的个数。
代码
class Solution {
public:
int hIndex(vector<int>& citations) {
int n = citations.size();
//二分下标
int l = 0,r = n - 1;
while(l < r)
{
int mid = (l + r) >> 1;
//满足分割点右边数的个数与分割点 cs[x]
if(citations[mid] >= n - mid) r = mid;
else l = mid + 1;
}
// n - r(分割点右边数的个数) 就是 答案 特判一下 [0] 特例
return citations[r] >= n - r ? n - r : 0;
}
};
240.搜索矩阵II
代码
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(),n = matrix[0].size();
int i = 0,j = n - 1;//从右上角开始
while(i < m && j >= 0)//还有剩余元素
{
if(matrix[i][j] == target) return true;
else if(matrix[i][j] > target) j--;//这一列元素大于target 排除
else i++; //这一行元素小于target 排除
}
return false;
}
};