数据结构与算法总结3(个人原创,带详细注释代码)

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
滑动窗口

O ( N ) O(N) O(N),不表

前缀和&二分

题目解读为:找出区间和大于target的最小的任意子数组,因此用前缀和思路

预先遍历求出前缀和数组

单次遍历前缀和数组 O ( N ) O(N) O(N)固定前缀和终点作为被减数,那么作为减数的前缀和起点的选取,使得二者相减的区间和大于target,并取最短长度,因为前缀和是单调的,因此可以用二分 O ( l o g N ) O(logN) O(logN)

问题转换为:在前缀和数组下标 [0,i] 范围内找到满足「值小于等于 s−t」的最大下标,充当子数组左端点的前一个值。

利用前缀和数组的「单调递增」(即具有二段性),该操作可使用「二分」来做。

O ( N l o g N ) O(NlogN) O(NlogN)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int ans = INT_MAX;
        vector<int> prefix(nums.size()+1);
        for(int i=0;i<nums.size();i++) prefix[i+1] = prefix[i]+nums[i];
        for(int i=1;i<prefix.size();i++){//固定右端点[1,n],选取左端点[0,i-1]
            int need = prefix[i]-target;
            int left = 0, right = i-1;
            while(left<=right){
                int mid = (right-left)/2+left;
                if(prefix[i]-prefix[mid]>=target) left = mid+1;
                else right = mid-1;
            }
            if(right>=0&&prefix[i]-prefix[right]>=target) ans = min(ans,i-right);
        }
        return ans==INT_MAX?0:ans;
    }
};
1004. 最大连续1的个数 III

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k0 ,则返回 数组中连续 1 的最大个数

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。
滑动窗口
前缀和&二分

同上题,将该问题进行如下的转化,即:

找到 0 个数 <= k 的最长任意子区间。

任意子区间 -> 区间和

如何从区间和得到 0 的个数?区间长度 - 区间和(因为区间和只将区间中的1累加)

一遍遍历固定前缀和右端点,对左端点二分查找,尽量找到最靠左的左端点,因为前缀和的单调性在于:左端点越靠左,区间越长,0的个数越多

关键思路:

  • 要求任意子区间上 0 的个数 -> 区间和求得任意子区间上 1 的个数 -> 用区间长度减前者得到 0 个数
  • 二分查找左边界转移方程单调性:当前mid满足条件时,right转移为mid,因为需要找靠左的区间左边界,而单调性是:左边界越靠左,区间越长,0的数量也越多,因此二分区间右边界右侧区间为满足条件的区间
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        vector<int> prefix(nums.size()+1,0);
        for(int i=1;i<prefix.size();i++){
            prefix[i] = prefix[i-1]+nums[i-1];
        }
        int ans = 0;
        for(int i=1;i<prefix.size();i++){
            int left = 0,right = i;
            while(left<right){
                int mid = (right-left)/2+left;
                //注意这里转移方程,prefix[i]-prefix[mid]为[mid,i-1]区间和,其长度为i-1-mid+1 = i-mid,二者相减为0的个数,小于k时right指针转移,因为区间左端点的选取,越靠右,0的个数越少,因此尽量找到更多的0,使得区间长度更长
                if(i-mid-prefix[i]+prefix[mid]<=k) right = mid;
                else left = mid+1;
            }
            ans = max(ans,i-right);
        }
        return ans;
    }
};
动态规划(多维度模板,背)

定义 f [ i , j ] f[i,j] f[i,j] 代表考虑前 i i i 个数(并以 A [ i ] A[i] A[i] 为结尾的),最大翻转次数为 j j j 时,连续 1 的最大长度。

  • 如果 A [ i ] A[i] A[i] 本身就为 1 的话,无须消耗翻转次数, f [ i ] [ j ] = f [ i − 1 ] [ j ] + 1 f[i][j]=f[i−1][j]+1 f[i][j]=f[i1][j]+1
  • 如果 A [ i ] A[i] A[i] 本身不为 1 的话,由于定义是必须以 A [ i ] A[i] A[i] 为结尾,因此必须要选择翻转该位置, f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 f[i][j]=f[i−1][j−1]+1 f[i][j]=f[i1][j1]+1

dp中有几个状态量,就初始化几维数组,就有几重循环

比如该题,有以下标i [0,n-1]结尾的1长度,还有翻转次数j [0,k](这里不是[0,k-1]),因此需要创建二维数组,每一个维度的大小就是其取值范围,也是其循环范围

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        vector<vector<int>> dp(nums.size()+1,vector<int>(k+1,0));//dp[0]表示i为-1,省去了初始化,i在dp中范围为[1,nums.size()]
        int ans = 0;
        for(int i=0;i<nums.size();i++){
            for(int j=0;j<=k;j++){
                if(nums[i]==1) dp[i+1][j] = dp[i][j]+1;
                else dp[i+1][j] = j==0?0:dp[i][j-1]+1;//注意边界条件 j==0
                ans = max(ans,dp[i+1][j]);
            }
        }
        return ans;
    }
};

参考188. 买卖股票的最佳时机 IV中对维度空间优化

由于上面dp[i+1]只用到dp[i] 的状态,也就是其上一轮的状态,因此这个「上一轮」可以滚动化

去掉i维度,相当于循环的时候就是在滚动了,上一轮的状态存储在尚未更新的元素格子中

由于 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] 需要从 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 转移过来,因此这里倒序遍历,就可以使得上一轮的 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 还没有更新成 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j1]

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        vector<int> dp(k+1,0);
        int ans = 0;
        for(int i=0;i<nums.size();i++){
            for(int j=k;j>=0;j--){//注意对j倒序遍历
                if(nums[i]==1) dp[j] = dp[j]+1;
                else dp[j] = j==0?0:dp[j-1]+1;
                ans = max(ans,dp[j]);
            }
        }
        return ans;
    }
};
162. 寻找峰值

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

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

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

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:

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

示例 2:

输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5 
解释:你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。
二段性二分

证明 1 :对于任意数组而言,一定存在峰值(一定有解)

根据题意,我们有「数据长度至少为 1」、「越过数组两边看做负无穷」和「相邻元素不相等」的起始条件。

我们可以根据数组长度是否为 1 进行分情况讨论:

  1. 数组长度为 1,由于边界看做负无穷,此时峰值为该唯一元素的下标;
  2. 数组长度大于 1,从最左边的元素 nums[0] 开始出发考虑:
    • 如果 nums[0]>nums[1],那么最左边元素 nums[0] 就是峰值(结合左边界为负无穷);
    • 如果 nums[0]<nums[1],由于已经存在明确的 nums[0] 和 nums[1] 大小关系,我们将 nums[0] 看做边界, nums[1] 看做新的最左侧元素,继续往右进行分析:
      • 如果在到达数组最右侧前,出现 nums[i]>nums[i+1],说明存在峰值位置 i(当我们考虑到 nums[i],必然满足 nums[i] 大于前一元素的前提条件,当然前一元素可能是原始左边界);
      • 到达数组最右侧,还没出现 nums[i]>nums[i+1],说明数组严格递增。此时结合右边界可以看做负无穷,可判定 nums[n−1] 为峰值。

综上,我们证明了无论何种情况,数组必然存在峰值。

证明 2 :二分不会错过峰值

其实基于「证明 1」,我们很容易就可以推理出「证明 2」的正确性。

整理一下由「证明 1」得出的推理:如果当前位置大于其左边界或者右边界,那么在当前位置的右边或左边必然存在峰值。

换句话说,对于一个满足 nums[x]>nums[x−1] 的位置,x 的右边(包含x)一定存在峰值;或对于一个满足 nums[x]>nums[x+1] 的位置,x (包含x)的左边一定存在峰值。

因此这里的「二段性」其实是指:在以 mid 为分割点的数组上,根据 nums[mid] 与 nums[mid±1] 的大小关系,可以确定其中一段满足「必然有解」,另外一段不满足「必然有解」(可能有解,可能无解)。

如果不理解为什么「证明 2」的正确性可以由「证明 1」推导而出的话,可以重点看看「证明 1」的第 2 点的证明。

至此,我们证明了始终选择大于边界一端进行二分,可以确保选择的区间一定存在峰值,并随着二分过程不断逼近峰值位置。

二分的本质是「二段性」而非「单调性」,也即每次二分都能判断答案在一个位置的左边或者右边,从而收缩另一边

本题中循环不变量:right右侧为峰值及其右侧,left左侧为峰值左侧,中间为未知

因此当 nums[mid] > nums[mid+1] 时,mid(包含)及其左侧可能为峰值,那么mid(包含)及其右侧必然为峰值及其右侧,所以移动right到mid-1,left同理

因此循环结束时候,right右侧第一个数就是峰值及其右侧的第一个数,就是峰值

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0,right = nums.size()-2;//nums.size()-1必然为峰值或者在峰值右侧,不予考虑
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>nums[mid+1]) right = mid-1;
            else left = mid+1;
        }
        return left;
    }
};
153. 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。

示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
二段性二分

right右侧为最小值及其右侧,蓝色

left左侧为最小值左侧(不包含最小值),红色

每次将mid与nums.back()比较二段性在于:

  • 小于nums.back()的元素,必然在最小值右侧(有可能为最小值),因此移动right指针染成蓝色
  • 大于nums.back()的元素,必然在最小值左侧(不包含最小值),因此移动left指针染成红色

nums.back()必然在最小值右侧,或者为最小值,因此其天生为蓝色,在right右侧,不用考虑

具体情况见下图

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0,right = nums.size()-2;//nums.back()必为最小值或者其右侧
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>nums.back()) left = mid+1;
            else right = mid-1;
        }
        return nums[right+1];
    }
};
33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

二段性二分

这题在上题的基础上,将找最小值换成了找target,那就先找最小值,再比较最小值和target的大小关系,选定一个区间做普通的二分查找

class Solution {
public:
    int search(vector<int>& nums, int target) {
        //先找到最小值
        int left = 0,right = nums.size()-2;
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>nums.back()) left = mid+1;
            else right = mid-1;
        }
        //然后选定搜索区间做普通二分
        if(target<=nums.back()) left = right+1,right = nums.size()-1;
        else left = 0;
        
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>=target) right = mid-1;
            else left = mid+1;
        }
        return nums[right+1]==target?right+1:-1;
    }
};
81. 搜索旋转排序数组 II

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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

你必须尽可能减少整个操作步骤。

恢复二段性

这题与33相比,数组元素是可能重复的,这样选取nums.back()作为mid比较元素时,可能丢失二段性

比如nums = [1,2,2,2,3,4],旋转为[2,3,4,1,2,2]时候,第一个2与最后的2是相等的,无法判断是在最小值1的左侧还是右侧,二段性失效

因此多做一步:将旋转后数组头部与nums.back()相等的元素忽略,再二分寻找最小值

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-2;
        //因此多做一步:**将旋转后数组头部与nums.back()相等的元素忽略**
        while(left<nums.size()&&nums[left] == nums.back()) left++;
        
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>nums.back()) left = mid+1;
            else right = mid-1;
        }
        if(target<=nums.back()) left = right+1,right = nums.size()-1;
        else left = 0;
        while(left<=right){
            int mid = (right-left)/2+left;
            if(nums[mid]>=target) right = mid-1;
            else left = mid+1;
        }
        return nums[right+1]==target;
    }
};
410. 分割数组的最大值

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

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

示例 1:

输入:nums = [7,2,5,10,8], k = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。 
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

示例 2:

输入:nums = [1,2,3,4,5], k = 2
输出:9

示例 3:

输入:nums = [1,4,4], k = 3
输出:4
答案上构建二分+原数组判断

「使……最大值尽可能小」是二分搜索题目常见的问法。

看到**「最大化最小值」或者「最小化最大值」就要想到二分答案**,这是一个固定的套路。

为什么?首先元素和是一个有范围的数,「元素和的最大值」越小,需要划分出的段数就越多,反之越少(单调性 -> 二分)。例如示例 1 的 nums=[7,2,5,10,8],在最大和为 15 时,至少要划分 3 段,比如 [7,2,5],[10],[8]。而在最大和为 18 时,只需要划分 2 段,比如 [7,2,5],[10,8]。

本题中,我们注意到:当我们选定一个值 x**(构建:在答案的取值范围上列举二分),我们可以线性地验证是否存在一种分割方案,满足其最大分割子数组和不超过 x(原数组判断)**。策略如下:

贪心地模拟分割的过程,从前到后遍历数组,用 sum 表示当前分割子数组的和,cnt 表示已经分割出的子数组的数量(包括当前子数组),那么每当 sum 加上当前值超过了 x,我们就把当前取的值作为新的一段分割子数组的开头,并将 cnt 加 1。遍历结束后验证是否 cnt 不超过 m。

这样我们可以用二分查找来解决。二分的上界为数组 nums 中所有元素的和,下界为数组 nums 中所有元素的最大值。通过二分查找,我们可以得到最小的最大分割子数组和,这样就可以得到最终的答案了。

如何确定最后二分出来的答案,一定就是某个子数组的和呢?

不会出现下面这种情况吗:不存在某个子数组的和为二分的答案,但是所有子数组和都满足要求

  • 先不考虑二分,那么答案肯定是在 数组最大值和数组和之间 [max(nums),sum(nums)],也就是可以确保题目要求的答案一定恰好等于区间内的某个数
  • 循环结束时left和right相等,而left-1就会划分出更多的子数组,说明left是满足题意的最小值,所以必然存在一个子数组的和是left
  • 因为check是cnt<=k,所以如果mid被分成k段但不是子数组和,他还会继续尝试缩小。且如果答案符合,那答案-1肯定不符合(不然他还会继续缩小)
class Solution {
public:
    int splitArray(vector<int>& nums, int k) {
        int sum = 0,maxnum = 0;
        for(int num:nums) sum+=num,maxnum = max(maxnum,num);
        int left = maxnum,right = sum;
        auto check = [&](int &ck)->bool{
            int tmpsum = 0;
            int partition = 1;
            for(int i=0;i<nums.size();i++){
                tmpsum+=nums[i];
                if(tmpsum>ck){
                    tmpsum = nums[i];
                    partition++;
                }
            }
            return partition<=k;
        };
        while(left<=right){
            int mid = (right-left)/2+left;
            if(check(mid)) right = mid-1;//注意这里的单调性,列举的元素和最大值越小,划分段数最小值越大,如果最小划分段数 >k ,就是时候列举更大的元素和了,因此left设为mid+1,left左边区间为不可能的元素和
            else left = mid+1;
        }
        return left;
    }
};
875. 爱吃香蕉的珂珂

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。

珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

答案上构建二分+原数组判断

容易想到暴力法:对每个速度都遍历一遍piles,求出该速度下吃完piles所需时间,O(n*maxpiles)

如何优化?关键点:速度是一个有范围且单调的整数,因此可以使用「二分查找法」找到这个有范围的整数

与275. H 指数II一样,这里不是对题目所给的遍历数组做二分,而是自己构建所求量的单调范围在这个构建的单调范围上对所求量进行二分

比如这题要求速度,我们构建其范围为[1,maxpile],这个范围内取值是单调有序的,因此在这上面做二分,对每个速度验证其对于题目所给的数组上的可行性

本题的可行性验证:对于二分到的速度speed,验证该速度下吃完所有香蕉所花时间是否 <= h即可

//O(Nlog(maxpiles))
class Solution {
public:
    int minEatingSpeed(vector<int>& piles, int h) {
        int maxpile = INT_MIN;
        for(auto &pile:piles) maxpile = max(maxpile,pile);//先求出单堆香蕉最大值作为速度上界
        
        int left = 1,right = maxpile;//速度下界为1
        
        auto confirmspeed = [&](int speed){
            bool feasible;
            int sum = 0;
            for(auto &pile:piles){
                sum += pile/speed + (pile%speed>0);//对每堆香蕉,求速度speed下耗时的「上界」
                if(sum>h) return false;
            }
            return true;
        };
        while(left<=right){
            int mid = (right-left)/2+left;
            if(confirmspeed(mid)) right = mid-1;
            else left = mid+1;
        }
        return left;
    }
};
275. H 指数 II

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

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少h 篇论文分别被引用了至少 h 次。

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

示例 1:

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

示例 2:

输入:citations = [1,2,100]
输出:2

提示:

  • n == citations.length
  • 1 <= n <= 105
  • 0 <= citations[i] <= 1000
  • citations升序排列
答案上构建二分+原数组判断

一开始写成这样,返回的是citations中第一个满足n>=citations[i]+i的下标left(左右开区间),left的转移条件是if(citations[mid]+mid<=n),因此left左侧(包含)的元素始终满足以上(citations[mid]+mid单调性),那么循环结束时left所指就是其左边满足n>=citations[i]+i区间的最右元素

这样无法通过[0,1,3,5,6,7,8],应该为4,但是只返回3,因为4没在citations中出现,定义域忽略了

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n  = citations.size();
        //n>=citations[i]+i
        //citations[n-i]>=i
        //x = n-i i = n-x
        auto lower_bound = [&](){
            int left = -1,right = n;
            while(left+1<right){
                int mid = (right-left)/2+left;
                if(citations[mid]+mid<=n) left = mid;//注意这里left的转移条件,导致了其结束时所指位置就是该区间的最右元素
                else right = mid;
            }
            return left;
        };
        return citations[lower_bound()];
    }
};

由于h指数是一个有范围的整数,因此可以使用「二分查找法」找到这个有范围的整数在范围[0,n]的h指数而非题目给的citations数上二分

因此考虑覆盖了所有h因子的[0,n],表示了h因子的取值范围,在这个上面二分

对于每个二分的h因子,判断其在题目所给citations上的有效性

判断mid是否为可能的h因子:citations中从后往前选取mid个元素,那么这些元素需citation均 >= mid,那么这组元素的第一个下标为n-mid,只需判断citations[n-mid]是否 >= mid即可,移项变成mid-citations[n-mid]<=0,这里的0即为模板中的target,而mid-citations[n-mid]又是在[0,n]中单增的,所以移动左指针,left左边元素(左右闭区间,不包含)均小于target,那么这些元素的最大(下标最右)值,也就是循环结束时left-1

注意:肯定有0个论文的引用因子>0,因此left初始值设为1,表示[0,n]中下标0性质已知,right初始化为右边界n

class Solution {
public:
    int hIndex(vector<int>& citations) {
        int n  = citations.size();
        //n>=citations[i]+i
        //citations[n-i]>=i
        //x = n-i i = n-x
        auto lower_bound = [&](){
            int left = 1,right = n;//注意初始化为h因子可能取值范围[0,n],但是0是肯定的因此left=1
            while(left<=right){
                int mid = (right-left)/2+left;
                if(mid-citations[n-mid]<=0) left = mid+1;
                else right = mid-1;
            }
            return left-1;
        };
        return lower_bound();
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值