关键 :
初始化左闭右开
l
向右排除mid元素 l=mid+1
检查最终位置是否满足预期
<1>使用左闭右开区间
1.初始化时也要左闭右开
2.好处是从而循环条件是l
非常适合找到条件为第一个…的问题
如:第一个 == target :
while(l=0 && l
第一个>target
第一个≥target
<3>对于向左收缩的问题转换为向右收缩
如:最后一个<target 转换为:找到第一个≥target 位置的左侧位置
最后一个<=target 转化为:找到第一个>target 位置的左侧位置
最后一个 == target 转化为:找到第一个>target位置的左侧位置,并判断该位置是否==target
int BiSearch(int target,vector<int>&nums){
if(nums.empty())return -1;
int l=0,r=nums.size(); // 1.初始化确定左闭右开区间
while(l<r) //区间的左闭右开决定退出条件为l
{
int mid = (l+r)>>1; //此处也经常用 l+(r-l)/2 以防止溢出
if(排除mid元素向右收缩)//括号里为排除当前mid元素的条件
l=mid+1;
}else{
r=mid;
}
//退出时 r==l 因此返回l或r均可
检查l是否为有效位置,且nums[l]满足条件
//如 l>=0 && l
}
int Equal(int target,vector<int>nums){
if(nums.empty()) return -1;
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)/2;
if(nums[mid]<target){
l=mid+1;
}else if(nums[mid]>target){
r=mid;
}else{
return mid;
}
}
return -1;
}
int FirstEqual(int target,vector<int>nums){
if(nums.empty()) return -1;
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)/2;
if(nums[mid]<target){
l=mid+1;
}else {
r=mid;
}
}
return l<nums.size() && nums[l]==target?l:-1;
}
int FirstGreaterEqual(int target,vector<int>nums){
if(nums.empty()) return -1;
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<target){
l=mid+1;
}else{
r=mid;
}
}
return l;
}
int FirstGreater(int target,vector<int>&nums){
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<=target){
l=mid+1;
}else{
r=mid;
}
}
return l;
}
int LastEqual(int target,vector<int>&nums){
//namely the first left pos of first greater position;
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<=target){
l=mid+1;
}else{
r=mid;
}
}
int pos=l-1;
return pos>=0 && pos<nums.size()&&target==nums[pos]?pos:-1;
}
int LastEqualSamller(int target,vector<int>&nums){
//namely the left position of the first greater position.
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<=target){
l=mid+1;
}else{
r=mid;
}
}
return l-1;
}
int LastSmaller(int target,vector<int>&nums){
int l=0,r=nums.size();
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<target){
l=mid+1;
}else{
r=mid;
}
}
return l-1;
}
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty()) return false;
int m=matrix.size();
int n=matrix[0].size();
//左闭右开区间 模板
int l=0,r=m*n;
while(l<r){
int mid=(l+r)>>1;
if(matrix[mid/n][mid%n]<target){
l=mid+1;
}else{
r=mid;
}
}
return l>=0 && l<m*n && matrix[l/n][l%n]==target?true:false;
}
中位数定义:
对于有序集合C {C0 C1 … Cn-1},其中位数将其均分为左右数量相等(n为偶数)或(左集合数量 == 右集合数量+1 )(n为奇数)
即
中位数将集合均分,均分点为i,则
C{C0 C1 ... Cn-1} == >Cleft: {C0 C1... Ci} | Cright:{Ci+1....Cn-1}
<1>左右数量相等
count(Cleft)=i+1,count(Cright)=n-1-i-1+1=n-i-1
当n为奇数 count(left)==count(right)+1==> i+1 == n-i-1+1
当n为偶数 count(left)==count(right)==> i+1 == n-i-1
<2> max{left}<=max{right}
假设有序集合{C}由有序集合{A} {B} 合并而成,即
C=A +B=Cleft+Cright={Cleft ... Cright}
则有
Aleft=A ∩ Cleft,Aright=A ∩ Cright
同理,
Bleft=B ∩ Cleft,Bright=B ∩ Cright
故
A={Aleft | Aright} ={A0 A1... Ai | Ai+1....Am-1}
B={Bleft | Bright}={B0 B1....Bj | Bj+1 ... Bn-1}
i,j分别为A B集合的左右子集合切分点。
故问题转换为在AB中寻找切分点i,j使得:
左边 | 右边
Cleft | Cright
A0 A1 ...Ai | Ai+1 .... Am-1
B0 B1 ...Bj | Bj+1 .... Bn-1
切分点满足条件是:
<1> 左边数量 == 右边数量(m+n为偶数) 或(左边数量 == 右边数量+1 (m+n)为奇数)
i+1+j+1=m-1-i-1+1+n-1-j-1+1==>2(i+j)=m+n-4 (m+n)为偶数
i+j+2=m-1-i-1+1+n-1-j-1+1 + 1==>2(i+j)=m+n-3 (m+n)为奇数
<2>max{left}<=min{right}
Ai<=Bj+1 (1)
Bj<=Ai+1 (2)
当条件(1)不满足,即Ai>Bj+1,可以通过较小i来减小Ai,并增大Bj+1
当条件(2)不满足,即Bj>Ai+1,可以通过增大i来增大Ai+1,并减少Bj
因此可通过二分搜索,来确定满足<2>条件的i,j.
<3> 边界条件 i∈[-1,m-1] ,j∈[-1,n-1]
集合A有可能全部属于left,或right。
因此i∈[-1,m-1] 当i=-1时,Aleft=∅ , 当i=m-1时,Aright=∅
同理j∈[-1,n-1]
故在搜索i时,利用<1>得到j后,除最终满足条件<2>外,还需有j∈[-1,n-1]。
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size();
int n=nums2.size();
auto &A=nums1;
auto &B=nums2;
//对i在[-1,m-1]中遍历 i=-1代表Aleft为空集,i=m-1代表Aright为空集
//此时要确保j在区间[-1,n-1]中 j=-1代表Bleft为空集,j=n-1代表Bright为空集
//采用左闭右闭方式i∈[-1,m-1]
int left=-1,right=m-1;
while(left<=right){
int i=(left+right)/2;
int j=((m+n)&1)==0? ((m+n-4)/2)-i:((m+n-3)/2)-i;
//should ensure j∈[-1,n]
//排除左区间,即Bj>Ai+1,i要增大 且确保j在有效区间内
if((j>=0&&j<n &&(i+1)>=0 && (i+1)<m && B[j]>A[i+1])||j>(n-1)){
left=i+1;
//排除右区间,且确保j在有效区间内
}else if((i>=0 && i<m &&(j+1)>=0 && (j+1)<n && A[i]>B[j+1])||j<-1){
right=i-1;
}else{
break;
}
}
//找到切分点:
int i=(left+right)/2;
int j=(m+n)%2==0? ((m+n-4)/2)-i:((m+n-3)/2)-i;
double Aleft=INT_MIN,Aright=INT_MAX,Bleft=INT_MIN,Bright=INT_MAX;
if(i>=0 && i<m) Aleft=A[i]; //Aleft不是空集
if(i+1>=0 && i+1<m) Aright=A[i+1]; //ARight 不是空集
if(j>=0 && j<n) Bleft=B[j]; //Bleft不是空集
if(j+1>=0 && j+1<n)Bright=B[j+1];//Bright不是空集
if((m+n)%2==0)return ((double)max(Aleft,Bleft)+(double)min(Aright,Bright))/2;
else return max(Aleft,Bleft);
}
原始数组为{a0 a1 a2 a3 a4 … an-2 an-1}
旋转后{ an-i…an | a0 a1 a2…an-i-1}==》{Aleft | Aright}
旋转后,数组分为Aleft区间与Aright区间,且{Aleft}>{Aright}
故对区间[Ai,Aj] 当[Ai,Aj]在Aleft或Aright中,均有Ai
class Solution1 {
//在递增有序区间内查找targe
int biSearch(int left, int right,vector<int>&nums,int target){
if(left>right || left<0 || right>=nums.size()) return -1;
int l=left,r=right+1;
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<target){
l=mid+1;
}else{
r=mid;
}
}
if(l>=left && l<=right && nums[l]==target) return l;
else return -1;
}
//在包含旋转点的无序区间查找
int search(int l,int r,vector<int>&nums,int target){
if(l>r || l<0 || r>=nums.size()) return -1;
int mid=(l+r)>>1;
if(nums[mid]==target) return mid;
else {
//[left,mid]是否有序
bool leftSorted= nums[l]<=nums[mid];
//[mid,right]是否有序
bool rightSorted=nums[mid]<=nums[r];
if(leftSorted && rightSorted){
return biSearch(l,r,nums,target);
}else if(leftSorted){
int ret = biSearch(l,mid-1,nums,target);
if(ret>=0) return ret;
else{
return search(mid+1,r,nums,target);
}
}else if(rightSorted){
int ret= biSearch(mid+1,r,nums,target);
if(ret>=0){
return ret;
}else{
return search(l,mid-1,nums,target);
}
}
}
return -1;
}
public:
int search(vector<int>& nums, int target) {
if(nums.size() == 0) return -1;
return search(0,nums.size()-1,nums,target);
}
};
类似于33,对于区间[ai,aj]只有ai 同样判断左右区间是否有序,有序区间的最小元素就是第一个元素 峰值在当前查询点递增方向的区间 此题存在一个隐形的递增关系,即子数组长度L增加时,长度为L的子数组里最大的子数组和也在增加。 此题的隐含递增关系类似于二分查找。 应用二分查找的分而治之思路,同时注意满二叉树的判断条件,以及满二叉 240 Search a 2D Matrix II 275 H-Index II 将给定数组倒序插入有序数列中,则插入位置的前面都是小于当前数字元素 方法2: 原理同315,依然可用归并排序,只是存在2倍关系逆序。 注意这道题与315 493 等求逆序数不同是,并不要求存在位置顺序上的要求。前两道求逆序数的题目要求小于自己的元素在空间位置上也是排在自己后面,所以才使用归并排序,保证左右子区间里的元素在数组里左右相对顺序不变。而本题只是求区间起点大于本区间终点,且距离最近的那个区间。class Solution {
//有序区间搜索方法,二分查找
bool biSearch(int left,int right,vector<int>&nums,int target){
if(left>right || left<0 || right>=nums.size()) return -1;
int l=left,r=right+1;
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<target){
l=mid+1;
}else{
r=mid;
}
}
if(l>=0 && l<nums.size() && nums[l]==target) return true;
return false;
}
//无序区间搜索,分而治之
bool search(int l,int r,vector<int>&nums,int target){
if(l<0 || r>=nums.size() || l>r) return false;
int mid=(l+r)>>1;
if(nums[mid] == target){
return true;
}else{
//[l,mid]有序,二分搜索
if(nums[l]<nums[mid]){
bool b= biSearch(l,mid-1,nums,target);
if(b) return b;
}else{
//区间无序,继续迭代
bool b=search(l,mid-1,nums,target);
if(b) return b;
}
//[mid,r]有序,二分查找
if(nums[mid]<nums[r]){
bool b=biSearch(mid+1,r,nums,target);
if(b) return b;
}else{
//[mid,r]无序,继续迭代
bool b=search(mid+1,r,nums,target);
if(b) return b;
}
}
return false;
}
public:
bool search(vector<int>& nums, int target) {
if(nums.empty()) return false;
return search(0,nums.size()-1,nums,target);
}
};
旋转有序数组最小值1
class Solution {
int findMin(int l,int r,vector<int>&nums){
//分而治之之前保证区间最少有3个元素,从而避免边界情况讨论
if(l==r) return nums[l];
if(l+1==r) return min(nums[l],nums[r]);
int mid=l+(r-l)/2;
//左右区间均有序
if(nums[mid]<nums[r] && nums[l]<nums[mid]){
return nums[l];
//左区间有序
}else if(nums[l]<nums[mid]){
return min(nums[l],findMin(mid+1,r,nums));
//右区间有序
}else if(nums[mid]<nums[r]){
return min(nums[mid],findMin(l,mid-1,nums));
}else{
//左右区间均无序
return min(nums[mid],min(findMin(l,mid-1,nums),findMin(mid+1,r,nums)));
}
}
public:
int findMin(vector<int>& nums) {
if(nums.empty()) return -1;
if(nums.size()==1) return nums[0];
return findMin(0,nums.size()-1,nums);
}
};
旋转有序数组最小值2
class Solution {
int findMin(int l,int r,vector<int>&nums){
//分治之前,保证区间最少有三个元素,避免对边界情况处理
if(l==r) return nums[l];
if(l==r-1) return min(nums[l],nums[r]);
int mid=l+(r-l)/2;
//左右区间均有序
if(nums[l]<nums[mid] && nums[mid]<nums[r]){
return nums[l];
//左区间有序
}else if(nums[l]<nums[mid]){
return min(nums[l],findMin(mid+1,r,nums));
}else if(nums[mid]<nums[r]){ //右区间有序
return min(findMin(l,mid-1,nums),nums[mid]);
}else{ //左右区间都可能无序
return min(nums[mid],min(findMin(l,mid-1,nums),findMin(mid+1,r,nums)));
}
}
public:
int findMin(vector<int>& nums) {
if(nums.empty()) return -1;
return findMin(0,nums.size()-1,nums);
}
};
寻找峰值
class Solution {
int findPeakElement(int l,int r,vector<int>&nums){
if(l==r) return l;
if(l==r-1) return nums[l]>nums[r]?l:
//通过mid相邻元素,哪边大就往哪边找
int mid=l+(r-l)/2;
if(nums[mid]<nums[mid+1]){
return findPeakElement(mid+1,r,nums);
}else if(nums[mid]<nums[mid-1]){
return findPeakElement(l,mid-1,nums);
}else{
return mid;
}
}
public:
int findPeakElement(vector<int>& nums) {
if(nums.empty()) return -1;
return findPeakElement(0,nums.size()-1,nums);
}
};
最小尺寸子数组和
当L=1, max_subArray_sum_of_L=max{nums}
当L=n, max_subArray_sum_of_L=sum{nums}
因此当L < == {1…n} ,max_subArray_sum< =={max{nums},sum{nums}}
由此可使用二分查找,遍历1…n找到第一个使得max_subArray>=s的Lclass Solution {
public:
vector<unsigned int>dp;
//求所有长度为len的子数组里,最大的子数组和
unsigned int maxSubArraySumofLen(int len,vector<int>&nums){
vector<unsigned int>subArrays;
for(int i=len-1;i<nums.size();++i){
if(i-len>=0)subArrays.push_back(dp[i]-dp[i-len]);
else{
subArrays.push_back(dp[i]);
}
}
return *max_element(subArrays.begin(),subArrays.end());
}
int minSubArrayLen(int s, vector<int>& nums) {
//长度为n,对应最大子数组和为sum(nums)
//长度 1,对应最大子数组和为max(nums)
//初始化用于辅助子数组求和的dp数组
dp.resize(nums.size());
for(int i=0;i<nums.size();++i){
if(i==0)dp[i]=nums[i];
else dp[i]=dp[i-1]+nums[i];
}
//当长度l由1-->n逐渐增大,则对应长度为l的子数组中子数组和最大的也逐渐增大
//因此,遍历l from 1...n 找到第一个其最大子数组和>=s的元素
if(nums.empty()) return 0;
int n=nums.size();
int left=1,right=n+1; //[left,right)//左闭右开
while(left<right){
int mid=(left+right)>>1;
if(maxSubArraySumofLen(mid,nums)<s){
left=mid+1;
}else{
right=mid;
}
}
return left>=1&&left<=n?left:0;
}
};
最大子数组和
<1>对于给定数组,其最大子数组和∈[max{num},sum{num}]
<2>故当给定划分个数m时,其最大子数组和∈[max{num},sum{num}]
若i<==遍历[max{num},sum{num}],检查数组在允许最大子数组和为i时
是否能划分为m个。
如果可以划分,则尝试降低i。
若不能划分,则需要增加i。
class Solution {
public:
//给定划分的个数m,和子数组可能最大的和。
//判断是否能将该数字划分为m个子数组。由于子数组是连续的,此处可以用贪心法
bool canSplit(vector<int>&nums,int m,long max_subsum){
long currSum=0;
int currCnt=0;
for(int i=0;i<nums.size();++i){
if(currSum+nums[i]<=max_subsum){
currSum+=nums[i];
if(i==nums.size()-1) ++currCnt;
}else{
++currCnt;
currSum=nums[i];
if(i==nums.size()-1)++currCnt;
}
if(currCnt>m) return false;
}
return true;
}
int splitArray(vector<int>& nums, int m) {
//m的最大值是1,对应的是最大和,即sum(nums)
//m的最大值是n,对应的是最小和,即max(nums)
//即返回的结果∈[max(nums),sum]
//对于给定x∈[max(nums),sum],即限制m个子数组最大和不超过x,
//如果不能分割为m个,则应该增大x。
//如果能分割为m个,则应该尝试减小x
//因此相当于在区间[max(nums),sum]中找到第一个x使得canSplit(nums,m,x)成立
//从而转化为二分查找算法
long left=*max_element(nums.begin(),nums.end());
long right=0;
for(auto i:nums) right+=i;
right+=1;//采用左闭右开区间
while(left<right){
long mid=(left+right)>>1;
if(canSplit(nums,m,mid)==false){ //mid时不能分割,应该增大
left=mid+1;
}else{
right=mid;
}
}
return left;
}
};
完全二叉树节点数量
树的节点总数公式 int countNodes(TreeNode* root) {
if(root==NULL) return 0;
//判断当前是否为满二叉树,如果是,直接套用公式
int left_high=1,right_high=1;
auto pl=root,pr=root;
while(pl->left){
++left_high;
pl=pl->left;
}
while(pr->right){
++right_high;
pr=pr->right;
}
if(left_high == right_high){ //左右等高为满二叉树
return pow(2,left_high)-1;
}
return 1+countNodes(root->left)+countNodes(root->right);
}
此题巧妙在从右上角开始搜索
当目标小于当前位置时,其必然在当前位置的左侧区域,故列坐标–
当目标大于当前位置时,其必然在当前位置的下侧区域,故横坐标++ bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty()) return false;
//从右上角开始搜索,当目标比当前小,则列坐标--,比当前大,行坐标++
int i=0,j=matrix[0].size()-1;
while(i<matrix.size() && j>=0){
int curr=matrix[i][j];
if(target == curr) return true;
else if(target<curr) --j;
else ++i;
}
return false;
}
此题的难点是理解H-index的定义:
假设作者的H-index为i,则
对于citations[i],至少有N-i篇文章>= citations[i];
即需要citations[i]>=N-i
也就是在递增数列
{citations[0]-(N-0) ,citations[1]-(N-1) ,citations[2]-(N-2)… citations[i]-(N-i)…citations[N-1]-1 }找到第一个>=0的位置class Solution {
public:
int hIndex(vector<int>& citations) {
//对于citations[i],至少有N-i篇文章>= citations[i];
//需要citations[i]>=N-i
if(citations.empty()) return 0;
// vector
寻找重复数字
class Solution {
int count_less_equal(int m , vector<int>&num){
int cnt=0;
for(auto i:num){
if(i<=m)++cnt;
}
return cnt;
}
public:
int findDuplicate(vector<int>& nums) {
if(nums.size() <=1) return -1;
//[b0 b1 b2.... x x a0 a1 a2 ... an]
//假设重复数字为x,则数组中对于任意>=x的数字a,小于等于a的数字个数>a
//对于数组中任意数字b
寻找逆序数个数!!!
,故该位置索引即为该插入数字的逆序数。 vector<int> countSmaller(vector<int>& nums) {
//思路一:从右向左依次将数字插入有序数列中,则插入位置索引即为坐标比起小的元素个数
vector<int> sortedNums;
if(nums.empty()) return {};
vector<int>ret(nums.size(),0);
for(int i=nums.size()-1;i>=0;--i){
if(sortedNums.empty()){
sortedNums.push_back(nums[i]);
ret[i]=0;
}else{
//找到sortedNums中第一个>= nums[i]的位置
int l=0,r=sortedNums.size();
while(l<r){
int mid=(l+r)>>1;
if(sortedNums[mid]<nums[i]){
l=mid+1;
}else{
r=mid;
}
}
sortedNums.insert(sortedNums.begin()+l,nums[i]);
ret[i]=l;
}
}
return ret;
}
利用归并排序,将每个区间分成左右两个子区间,并且每个子区间分别进行有序排序后,对左区间每个元素aL,利用二分查找找到右有序区间中class Solution{
//利用归并排序,划分左右两个有序区间,并依次找到左区间元素在右区间的逆序数
vector<int>res;
int count_less(int left_border,int right_border,int target,vector<pair<int,int>>&nums){
int l=left_border;
int r=right_border+1;
while(l<r){
int mid=(l+r)>>1;
if(nums[mid].first<target){
l=mid+1;
}else{
r=mid;
}
}
return l-left_border;
}
void mergeSort(int lo,int hi ,vector<pair<int,int>>&nums){
if(lo>=hi) return;
int mid_sort=(lo+hi)>>1;
mergeSort(lo,mid_sort,nums);
mergeSort(mid_sort+1,hi,nums);
for(int i=lo;i<=mid_sort;++i){
int a=count_less(mid_sort+1,hi,nums[i].first,nums);
res[nums[i].second]+=a;
}
//此时cmp的参数必须为const,养成const &cmp的好习惯!!!
auto cmp=[](const pair<int,int>&a,const pair<int,int>&b)->bool{
return a.first<b.first;
};
//直接调用C++ API,原地对两有序数组进行合并
inplace_merge(nums.begin()+lo,nums.begin()+mid_sort+1,nums.begin()+hi+1,cmp);
}
public:
vector<int> countSmaller(vector<int>& nums) {
if(nums.empty()) return {};
if(nums.size()==1) return {0};
vector<pair<int,int>>pairs;
for(int i=0;i<nums.size();++i){
pairs.push_back(pair<int,int>(nums[i],i));
}
res.resize(nums.size(),0);
mergeSort(0,nums.size()-1,pairs);
return res;
}
};
两倍逆序数
class Solution{
int res=0;
void mergeSort(int lo,int hi,vector<int>&nums){
//归并排序经常用于逆序对求解
if(lo>=hi) return ;
int mid_sort=(lo+hi)>>1;
mergeSort(lo,mid_sort,nums);
mergeSort(mid_sort+1,hi,nums);
//此时[lo mid] [mid+1,hi] 分别有序
//对[lo,mid] 中每一个元素i,在[mid+1,hi]中找到
//也即在[mid+1,hi]中找到第一个>=i/2的位置
for(int i=lo;i<=mid_sort;++i){
//二分查找 在[mid+1,hi]中找到第一个>=i/2的位置
long target=nums[i];
int l=mid_sort+1,r=hi+1;
while(l<r){
int mid=(l+r)>>1;
if((long)nums[mid]*2<target){
l=mid+1;
}else{
r=mid;
}
}
int cnt=l-mid_sort-1;
if(cnt>0){
res+=cnt;
}
}
inplace_merge(nums.begin() + lo,nums.begin() + mid_sort + 1,nums.begin() + hi + 1);
}
public:
int reversePairs(vector<int>&nums){
if(nums.size()<=1) return 0;
mergeSort(0,nums.size()-1,nums);
return res;
}
};
最近逆序区间
即将区间起点排序后,得到数列{a},对于每一个区间的终点b,在{a}中找到第一个大于等于b的元素class Solution {
public:
vector<int> findRightInterval(vector<vector<int>>& intervals) {
map<int,int>start_points;
for(int i=0;i<intervals.size();++i){
//题目已经保证无重复区间起始点
start_points[intervals[i][0]]=i;
}
vector<int>ret(intervals.size(),-1);
for(int i=0;i<intervals.size();++i){
//对于每个区间,找到最近的一个起始点>=其结束点的
auto lower=start_points.lower_bound(intervals[i][1]);
if(lower != start_points.end()){
ret[i]=lower->second;
}
}
return ret;
}
};