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

2289. 使数组按非递减顺序排列

给你一个下标从 0 开始的整数数组 nums 。在一步操作中,移除所有满足 nums[i - 1] > nums[i]nums[i] ,其中 0 < i < nums.length

重复执行步骤,直到 nums 变为 非递减 数组,返回所需执行的操作数。

示例 1:

输入:nums = [5,3,4,4,7,3,6,11,8,5,11]
输出:3
解释:执行下述几个步骤:
- 步骤 1 :[5,3,4,4,7,3,6,11,8,5,11] 变为 [5,4,4,7,6,11,11]
- 步骤 2 :[5,4,4,7,6,11,11] 变为 [5,4,7,11,11]
- 步骤 3 :[5,4,7,11,11] 变为 [5,7,11,11]
[5,7,11,11] 是一个非递减数组,因此,返回 3 。

示例 2:

输入:nums = [4,5,7,7,13]
输出:0
解释:nums 已经是一个非递减数组,因此,返回 0 。
单调栈&dp

提示 1

元素 x x x 会被左边某个比他大的元素 y y y 给删除(如果存在的话)。

我们需要计算在删除 x x x 之前,删除了多少个 y y y 小的元素,从而算出删除 x x x 的时刻(第几步操作)。

答案可以转换成当前元素所有(能被删除的)元素被删除的时刻的最大值

提示 2

[ 20 , 1 , 9 , 1 , 2 , 3 ] [20,1,9,1,2,3] [20,1,9,1,2,3] 为例。

时刻一 20 20 20 删掉 1 1 1 9 9 9 删掉 1 1 1

时刻二 20 20 20 删掉 9 9 9 9 9 9 删掉 2 2 2;

时刻三 20 20 20 接替了 9 9 9 的任务,来删除数字 3 3 3

虽然说数字 3 3 3 是被 20 20 20 删除的,但是由于 20 20 20 立马接替了 9 9 9,我们可以等价转换看作 3 3 3 是被 9 9 9 删除的,也就是它左边离它最近且比它大的那个数。

该等价转换不会影响数字被删除的时刻。

因此元素 x x x 会被左边第一个比他大的元素 y y y 给删除

也就变成找上一个更大元素 -> 从左往右(上一个)遍历单调递减(更大)栈

提示 3

再考虑这个例子 [ 9 , 1 , 2 , 3 , 4 , 1 , 5 ] [9,1,2,3,4,1,5] [9,1,2,3,4,1,5]

5 5 5 应该被 9 9 9 删除。根据题目要求,在删除 5 5 5 之前,需要把 5 5 5 前面不超过 5 5 5 的元素都删除,然后才能删除 5 5 5。所以在删除 5 5 5 之前,我们需要知道 9 9 9 5 5 5 之间的所有元素被删除的时刻的最大值,这个时刻加一就是删除 5 5 5 的时刻。

提示 4

对于一串非降的序列,该序列的每个元素被删除的时刻是单调递增的。(假设序列左侧有个更大的元素去删除序列中的元素)

利用这一单调性,我们只需要存储这串非降序列的最后一个元素被删除的时刻。(单调栈的弹出正确性,因为弹出序列可以看作以当前元素结尾的非降序列)

某一段区间会包含若干个非降序列,也就包含了若干个最后一个元素被删除的时刻,提示 3 中所需要计算的最大值必然在这些时刻中。

提示 5

我们可以用一个单调递减栈存储元素及其被删除的时刻,当遇到一个不小于栈顶的元素 x x x 时**(也即出现了提示4中的非降序列)**,就不断弹出栈顶元素,**并取弹出元素被删除时刻的最大值,**这样就得到了提示 3 中所需要计算的时刻的最大值 m a x T maxT maxT

然后将 x x x m a x T + 1 maxT+1 maxT+1 入栈。注意如果此时栈为空,说明前面没有比 x x x 大的元素, x x x 无法被删除,即 m a x T = 0 maxT=0 maxT=0,这种情况需要将 x x x 0 0 0 入栈。

/**
     * [9   1   2   4    3    5    5]
     *
     * 9
     * -----------------------------6
     * ------------------------5
     * -------------4
     * -------------------3
     * --------2
     * ----1
     *
     *---------------------------------
     * 单调递减队列
     *到 1 的时候 9 在队列中 所以 1是 需要被删除的 时间点为 1
     *到 2 的时候 9 1 在队列中,要删除2需要先删除1, 时间点为 t(1)+1
     *到 4 的时候 9 2 在队列中,要删除4需要先删除2, 时间点为 t(2)+1
     *到 3 的时候 9 4 在队列中,要删除3不需要先删除其他的值,所以删除3的时间点为1
     *到 5 的时候 9 4 3 在队列中,要删除5 需要先删除 4,3,所以删除5的时间点为t(4)+1
     *到 6 的时候 9 5 在队列中,要删除6 需要先删除 5,所以删除6的时间点为t(5)+1
     */
class Solution {
public:
    int totalSteps(vector<int>& nums) {
        int ans = 0;
        stack<pair<int,int>> stk;
        for(int i =0;i<nums.size();i++){
            int maxpopT = 0;//要删除当前num 的时间点
            while(!stk.empty()&&nums[i]>=stk.top().first){//单调递减队列
                maxpopT = max(maxpopT,stk.top().second);//左边小于等于num的值都需要在num被删除之前删掉,此时maxT为删掉左边这些小与等于num的数的最大时间
                stk.pop();
            }
            maxpopT = stk.empty()?0:maxpopT+1;//stack不空,左边有比num值大的情况,num需要被删除,为空,说明num不用删除
            ans = max(ans,maxpopT);
            stk.emplace(nums[i],maxpopT);
        }
        return ans;
    }
};
class Solution {
public:
    int totalSteps(vector<int>& nums) {
        int ans = 0, n = nums.size();
        vector<int> dp(n);
        deque<int> stk;
        for(int i = 0; i < n; ++i){
            int x = nums[i];
            while(stk.size() && nums[stk.back()] <= x){
                dp[i] = max(dp[i], dp[stk.back()]);
                stk.pop_back();
            }
            ++dp[i];
            if(stk.empty()) dp[i] = 0;
            stk.push_back(i);
        }
        return *max_element(begin(dp), end(dp));
    }
};
316. 去除重复字母

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例 1:

输入:s = "bcabc"
输出:"abc"

示例 2:

输入:s = "cbacdcbc"
输出:"acdb"
单调栈&哈希表

单调递增栈 实现 字典序最优

记录字母最后出现位置下标哈希表 实现 维护字典序最优过程中 每个字母至少出现一次

查询给定字符是否在一个序列中存在的方法。根本上来说,有两种可能:

  • 有序序列: 可以二分法,时间复杂度大致是 O ( N ) O(N) O(N)

  • 无序序列: 可以使用遍历的方式,最坏的情况下时间复杂度为 O ( N ) O(N) O(N) 。我们也可以使用空间换时间的方式,使用 N N N 的空间 换取 O ( 1 ) O(1) O(1) 的时间复杂度。

对枚举的每个元素

  • 如果栈顶元素不是最后一次出现,就按照单增栈规则维护最优字典序,弹出栈顶元素直到符合规则

  • 如果栈顶元素是最后一次出现,说明该元素只能出现在最终序列中的该位置,当前元素不管字典序直接入栈

  • 如果遇到栈中已存在元素,直接忽略当前元素,因为栈中已是最优字典序,如果按照规则再次弹出维护,最后结果也不会更优,只有可能不变或者更差(栈中要被弹出的某个更大元素必须在那个位置,不能弹出,此时字典序变差)

  • 注意怎么查找字符串中有无特定字符/子串?npos

    s.find(substr/char) != s.npos
    //或者
    s.find(substr/char) != string::npos
    
class Solution {
public:
    string removeDuplicateLetters(string s) {
        //哈希表记录每个字符最后出现位置
        unordered_map<char,int> lastpos;
        for(int i=0;i<s.length();i++){
            lastpos[s[i]] = i;
        }
        //直接用最后返回结果作为栈操作,push_back()和pop_back()
        string ans = "";
        for(int i=0;i<s.length();i++){
            char c = s[i];
            if(ans.find(c)!=ans.npos) continue;//遇到栈中已存在元素,直接忽略
            //如果栈顶元素不是必须出现在该处(在原串中最后一次出现),就按字典序维护栈
            while(!ans.empty()&&c<ans.back()&&lastpos[ans.back()]>i){
                ans.pop_back();
            }
            ans.push_back(c);
        }
        return ans;
    }
};

也可以栈中加个大于所有元素的哨兵,就不用每次判断栈非空了

class Solution {
public:
    string removeDuplicateLetters(string s) {
        unordered_map<char,int> lastpos;
        for(int i=0;i<s.length();i++){
            lastpos[s[i]] = i;
        }
        string ans = "";
        ans.push_back('z'+1);//大于所有元素
        for(int i=0;i<s.length();i++){
            char c = s[i];
            if(ans.find(c)!=ans.npos) continue;
            while(c<ans.back()&&lastpos[ans.back()]>i){
                ans.pop_back();
            }
            ans.push_back(c);
        }
        return ans.substr(1,ans.length()-1);//结果取子串
    }
};
402. 移掉k位数字

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

示例 1 :

输入:num = "1432219", k = 3
输出:"1219"
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。

示例 2 :

输入:num = "10200", k = 1
输出:"200"
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。

示例 3 :

输入:num = "10", k = 2
输出:"0"
解释:从原数字移除所有的数字,剩余为空就是 0 。
字典序 —— 单调栈

和316思路一样:

从左到右遍历维护 单调递增栈 实现 字典序最优

如果不满足单增栈了,就弹栈并维护k直至0结束

class Solution {
public:
    string removeKdigits(string num, int k) {
        string ans = "";
        ans.push_back('0'-1);//哨兵使得不用判断栈空
        for(int i=0;i<num.size();i++){
            while(k>0&&num[i]<ans.back()){//按照字典序,从左到右遍历维护单调递增栈
                ans.pop_back();
                k--;
            }
            //if(num[i]=='0'&&ans.length()==1) continue; 如果0打头,就跳过插入该元素
            if(k==0){
                ans+=num.substr(i,num.length()-1);
                break;
            }
            ans.push_back(num[i]);
        }
        while(k>0&&ans.length()>1){
            ans.pop_back();
            k--;
        }
        //维护单调栈过程中照常插入0,最后再取从第一个非0开始的子串
        int i =1;
        while(i<ans.length()){
            if(ans[i]!='0') break;
            i++;
		}
        ans = ans.substr(i);//注意substr用法,如果第二个参数不提供,则获取从位置i到字符串结尾的子串
        return ans.length()==0?"0":ans;
    }
};
456. 132模式

给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]nums[j]nums[k] 组成,并同时满足:i < j < knums[i] < nums[k] < nums[j]

如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false

示例 1:

输入:nums = [1,2,3,4]
输出:false
解释:序列中不存在 132 模式的子序列。

示例 2:

输入:nums = [3,1,4,2]
输出:true
解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。

示例 3:

输入:nums = [-1,3,2,0]
输出:true
解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。
暴力

维护 132模式 中间的那个数字 3,因为 3 在 132 的中间的数字、也是最大的数字。我们的思路是个贪心的方法:我们要维护的 1 是 3 左边的最小的数字; 2 是 3 右边的比 3 小并且比 1 大的数字。

从左到右遍历一次,遍历的数字是 n u m s [ j ] nums[j] nums[j] 也就是 132 模式中的 3。根据上面的贪心思想分析,我们用一个变量维护 3 左边最小的元素,然后使用暴力在 n u m s [ j + 1.. N − 1 ] nums[j+1..N−1] nums[j+1..N1] 中找到 132 模式中的 2 就行。

这个思路比较简单,也能 AC。

class Solution(object):
    def find132pattern(self, nums):
        N = len(nums)
        numsi = nums[0]
        for j in range(1, N):
            for k in range(N - 1, j, -1):
                if numsi < nums[k] and nums[k] < nums[j]:
                    return True
            numsi = min(numsi, nums[j])
        return False
单调栈

先正序遍历一遍生成每个元素的左最小值(也就是每个元素的1),然后倒序(从右到左)遍历维护一个单调递减栈,遍历遇到比栈顶元素大的元素时,作为3,此时对该3找到其右边(也就是栈中)最大的2,也即一直弹栈直到栈顶元素比当前元素大,那么弹出的最后一个元素就是 最优的2(对于该3为右边最大的较小值),此时比较2>1即可

**正确性:**如果当前枚举元素不符合单调递减要求,可以作为3,那么弹栈直到找到2的过程中,被弹出的元素不影响正确性,因为最后一个被弹出的元素由于单调递减栈缘故,是最大的2,相较于前面弹出的值拿去与1比大小,是最优的

或者换一个角度看,枚举过程中要找的是。 找 栈中已有的 2 左边 第一个比 2 大的元素 3。这个元素3,是此时的2的最优的3。

**为什么最优?**因为对于每个3而言,1已经是固定的,从右往左遍历只能是递增的,因此贪心策略使得离2最近的3对应的1最小,最有可能贪心得到132组合

因此这个过程两次用到贪心:

  1. 贪心得到3右边最大的2,也就是弹栈的最后一个元素
  2. 贪心得到2左边最小的1,也就是找离2最近的3

以上两点合二为一,造就了本题 正序遍历获得每个位置最小前缀+倒序单调递减栈 的解法

#define reversedecreasestack stk
class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        vector<int> leftmin = {INT_MAX};
        stack<int> stk;
        stk.push(INT_MAX);
        for(int i=1;i<nums.size();i++){
            leftmin.push_back(min(leftmin[i-1],nums[i-1]));
        }
        //for(int n:leftmin) cout<<n<<' ';
        for(int i=nums.size()-1;i>=0;i--){
            int rightmaxmin = INT_MIN;
            while(nums[i]>stk.top())
            {
                rightmaxmin = stk.top();
                stk.pop();
            }
            if(rightmaxmin>leftmin[i]) return true;
            stk.push(nums[i]);
        }
        return false;
    }
};
239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
单调队列

如下图:当4出现后,在其之前出现的更小值2,1永远也不可能成为滑动窗口内最大值

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

又因为要随着滑动窗口的推进,弹出窗口头部元素,因此不能只用单调栈,需要有两个元素出口,一个用于在新元素加入时,维护单调性,另一个用于随着时间推移维护元素正确性,因此使用单调队列,即双端队列,从尾部加入元素维护其单调性的同时,从头部弹出窗口外的过期元素

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        deque<int> dq;
        for(int i=0;i<nums.size();i++){
            while(!dq.empty()&&nums[i]>nums[dq.back()]) dq.pop_back();//单调性
            if(!dq.empty()&&dq.front()==i-k) dq.pop_front();//弹出过期元素
            dq.push_back(i);
            if(i>=k-1) ans.push_back(nums[dq.front()]);//单调性使得队列头部始终为窗口最大值
        }
        return ans;
    }
};
215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

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

示例 1:

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

示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
构建堆

https://www.cnblogs.com/hello-shf/p/11393655.html#_label3

最小堆中所有父节点值<子节点值,最大堆相反,可以在O(1)时间返回堆头部元素:堆中最大/最小元素

用数组模拟二叉树,手动构建堆,以下两个操作:

  • 添加元素&上浮:在数组结尾添加新元素,然后不断和其父节点比较并交换,直到无法上浮或者下标为0(到达堆顶)
  • 删除元素&下沉:只能删除堆顶元素,将堆顶元素(下标为0)与堆底元素(数组最后的元素)交换,然后数组大小(最大下标)-1模拟删除(这里也可选物理删除pop_back()),然后不断将换上来的那个元素与两个子元素比较,交换并下沉

注意上浮时候,当前节点的父节点下标为 (i-1)/2 而非 i/2

class Solution {
public:
    vector<int> heap;
    void sinkdown(int i){
        int left,right,swappos;
        while(1){
            left = i*2+1,right = i*2+2;//左右两个子元素
            swappos = i;
            //先判断左右儿子不超过堆长度,再判断是否下沉
            if(left<heap.size()&&heap[left]<heap[swappos])  swappos = left;
            if(right<heap.size()&&heap[right]<heap[swappos])  swappos = right;
            //如果可以下沉,就把下沉后的下标作为新的基点进入下一轮下沉循环,否则退出
            if(swappos!=i){
                swap(heap[swappos],heap[i]);
                i = swappos;
            }
            else break;
        }
    }
    void sinkdown(int i,int heapsize){
        int left = i*2+1,right = i*2+2;
        int swappos = i;
        if(left<heapsize&&heap[left]>heap[swappos])  swappos = left;
        if(right<heapsize&&heap[right]>heap[swappos])  swappos = right;
        if(swappos!=i){
            swap(heap[swappos],heap[i]);
            sinkdown(swappos,heapsize);
        }
    }
    void floatup(int i){
        while(i>0&&heap[i]<heap[(i-1)/2]){//注意上浮时候,当前节点的父节点下标为 `(i-1)/2` 而非 `i/2`
            swap(heap[i],heap[(i-1)/2]);
            i = (i-1)/2;
        }
    }
    
    //构建大小为k的小根堆,将所有元素判断与堆顶大小->是否加入堆,最后堆顶元素即为第k大的元素
    int findKthLargest(vector<int>& nums, int k) {
        for(int num:nums){
            if(heap.size()<k){//数组底部加入元素,然后将该元素上浮直到停下
                heap.push_back(num);
                floatup(heap.size()-1);
            }
            else if(num>heap[0]){//堆顶部与数组底部元素交换,在数组底部删除(物理或者手动控制堆大小数值),将堆顶元素不断下沉
                heap.push_back(num);
                swap(heap[0],heap.back());
                heap.pop_back();
                sinkdown(0);
            }
        }
        return heap[0];
    }
    
    //在原数组上构建大根堆,然后删除k-1个元素后得到堆顶为第k大的元素
    int findKthLargest(vector<int>& nums, int k) {
        heap = nums;
        int heapsize = heap.size();
        for(int i = heap.size()/2;i>=0;i--){//原数组上构建大根堆,注意从heap.size()/2开始,逆序遍历直到头部,每个元素都下沉
            sinkdown(i,heapsize);
        }
        for(;k>1;k--){//删除k-1个堆顶元素,这里没有物理删除,而是用heapsize标记堆大小进行删除
            swap(heap[0],heap[--heapsize]);
            sinkdown(0,heapsize);
        }
        return heap[0];
    }
};
347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

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

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

优先队列/小根堆

排序是O(nlogn),用哈希表统计数字频次,维护大小为k的小根堆,每次插入新元素的时间复杂度是O(logk),因此总的 时间复杂度为O(nlogk)

为什么是小根堆?虽然找的是前k大,但是如果是大根堆,需要将所有元素都插入一遍

小根堆O(1)返回队列中最小值,因此将小于该值的元素直接略过,不用插入

class Solution {
public:
    struct cmp{//结构体定义仿函数,注意>号定义了小根堆,<为大根堆
        bool operator()(const pair<int,int> &a,const pair<int,int> &b){
            return a.second>b.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        priority_queue<pair<int,int>,vector<pair<int,int> >,cmp> pq;//底层容器为数组
        for(int num:nums) mp[num]++;
        for(auto &p:mp){
            if(pq.size()<k) pq.push(p);
            else if(p.second>pq.top().second){
                pq.push(p);
                pq.pop();
            }
        }
        vector<int> ans;
        while(!pq.empty()){
            ans.push_back(pq.top().first);
            pq.pop();
        }
        return ans;
    }
};
桶排序

说人话就是用哈希表统计频次后,将频次作为下标,对应数字作为值,填入一个拉链数组/桶,然后倒序遍历取k个元素就是频次最多的k个元素

怎么确定拉链数组的下标范围?统计频数时候用一个变量maxcount记录最大频数作为数组上界

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        int maxcount = INT_MIN;//统计最大频数作为反数组(桶)的最大下标
        for(int num:nums){
            mp[num]++;
            maxcount = max(maxcount,mp[num]);
        }
        vector<vector<int>> bucket(maxcount+1);//构建拉链vector<vector<int>>数组/桶
        for(auto &a:mp){
            bucket[a.second].push_back(a.first);
        }
        vector<int> ans;
        for(int i = maxcount;k>0;i--){//倒序遍历桶的后k个下标元素就是答案了
            for(int j:bucket[i]){
                ans.push_back(j);
                k--;
            }
        }
        return ans;
    }
};
1696. 跳跃游戏 VI

给你一个下标从 0 开始的整数数组 nums 和一个整数 k

一开始你在下标 0 处。每一步,你最多可以往前跳 k 步,但你不能跳出数组的边界。也就是说,你可以从下标 i 跳到 [i + 1, min(n - 1, i + k)] 包含 两个端点的任意位置。

你的目标是到达数组最后一个位置(下标为 n - 1 ),你的 得分 为经过的所有数字之和。

请你返回你能得到的 最大得分

示例 1:

输入:nums = [1,-1,-2,4,-7,3], k = 2
输出:7
解释:你可以选择子序列 [1,-1,4,3] (上面加粗的数字),和为 7 。

示例 2:

输入:nums = [10,-5,-2,4,0,3], k = 3
输出:17
解释:你可以选择子序列 [10,4,3] (上面加粗数字),和为 17 。
递归dp

一、启发思考:寻找子问题

看示例 2,nums=[10,−5,−2,4,0,3], k=3

我们要解决的问题是:从下标 0 跳到下标 n−1=5,经过的所有数字之和最大是多少?

思考「最后一步」发生了什么,有 3 种选择:

从 4 跳到 5,我们需要知道:从 0 跳到 4,经过的所有数字之和最大是多少?
从 3 跳到 5,我们需要知道:从 0 跳到 3,经过的所有数字之和最大是多少?
从 2 跳到 5,我们需要知道:从 0 跳到 2,经过的所有数字之和最大是多少?
由于这 3 种选择,都把原问题变为和原问题相似的、规模更小的子问题,所以可以用递归解决。

注 1:从右往左倒着思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。

注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「枚举选哪个」。

二、递归怎么写:状态定义与状态转移方程

因为要解决的问题都形如「从 0 跳到 i,经过的所有数字之和最大是多少」,所以定义 dfs(i) 表示从 0 跳到 i,经过的所有数字之和的最大值。

如果从 j 跳过来,那么有 dfs(i)=dfs(j)+nums[i],其中 max⁡(i−k,0)≤j≤i−1

枚举 j,取转移来源的最大值,即

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

递归边界:dfs(0)=nums[0]。

递归入口:dfs(n−1),也就是答案。

class Solution {
public:
    int maxResult(vector<int>& nums, int k) {
        function<int(int)> dfs = [&](int i)->int{
            if(i==0) return nums[0];
            int mx = INT_MIN;
            for(int j=max(0,i-k);j<i;j++){//在上一次跳过来的区间中遍历,往前递归
                int z = dfs(j);
                mx = max(mx,z);//选取最大值
            }
            return mx+nums[i];
        };
        return dfs(nums.size()-1);
    }
};
递推
class Solution {
public:
    int maxResult(vector<int>& nums, int k) {
        vector<int> dp(nums.size());
        dp[0] = nums[0];
        for(int i=1;i<nums.size();i++){
            int mx = INT_MIN;
            for(int j = max(0,i-k);j<i;j++){
                mx = max(mx,dp[j]);
            }
            dp[i] = nums[i]+mx;
        }
        return dp[nums.size()-1];
    }
};
单调队列优化递推

递推过程中,每轮循环都要对 i 找 [i-k,i-1] 中的dp子问题最大值,这个求最值过程可以优化成单调队列

递推过程中对长度为 k 的窗口维护单调递减队列

每次取队列头部元素即为窗口内最大值

class Solution {
public:
    int maxResult(vector<int>& nums, int k) {
        vector<int> dp(nums.size());
        deque<int> dqueue;
        dp[0] = nums[0];
        for(int i=1;i<nums.size();i++){
            while(!dqueue.empty()&&dp[i-1]>=dp[dqueue.back()]) dqueue.pop_back();
            dqueue.push_back(i-1);
            if(dqueue.front()<i-k) dqueue.pop_front();//弹出窗口外的元素
            dp[i] = nums[i]+dp[dqueue.front()];//队列头即为最大值
        }
        return dp[nums.size()-1];
    }
};
2788. 按分隔符拆分字符串

给你一个字符串数组 words 和一个字符 separator ,请你按 separator 拆分 words 中的每个字符串。

返回一个由拆分后的新字符串组成的字符串数组,不包括空字符串

注意

  • separator 用于决定拆分发生的位置,但它不包含在结果字符串中。
  • 拆分可能形成两个以上的字符串。
  • 结果字符串必须保持初始相同的先后顺序。

示例 1:

输入:words = ["one.two.three","four.five","six"], separator = "."
输出:["one","two","three","four","five","six"]
解释:在本示例中,我们进行下述拆分:

"one.two.three" 拆分为 "one", "two", "three"
"four.five" 拆分为 "four", "five"
"six" 拆分为 "six" 

因此,结果数组为 ["one","two","three","four","five","six"] 。

示例 2:

输入:words = ["$easy$","$problem$"], separator = "$"
输出:["easy","problem"]
解释:在本示例中,我们进行下述拆分:

"$easy$" 拆分为 "easy"(不包括空字符串)
"$problem$" 拆分为 "problem"(不包括空字符串)

因此,结果数组为 ["easy","problem"] 。

示例 3:

输入:words = ["|||"], separator = "|"
输出:[]
解释:在本示例中,"|||" 的拆分结果将只包含一些空字符串,所以我们返回一个空数组 [] 。 
stringstream 和 getline 实现 split

具体看这个

c++ string没有实现split方法,可以用stringstream字符流和getline实现该功能

getline()的原型是istream& getline ( istream &is , string &str , char delim );

其中 istream &is 表示一个输入流,

例如,可使用cin;

string str ; getline(cin ,str)

也可以使用 stringstream

stringstream ss("test#") ; getline(ss,str)

char delim表示遇到这个字符停止读入,通常系统默认该字符为’\n’,也可以自定义字符

//自己实现的split方法

vector<string> split(string str, char del) {
	stringstream ss(str);//先对待分割字符串创建流
	string temp;
	vector<string> ret;
	while (getline(ss, temp, del)) {//第一个参数作为字符流,第二个参数作为接收的字符串,第三个参数作为读入即停止的字符
		ret.push_back(temp);//while循环直到getline读完即可,全自动,不用管ss尾部没有结束字符,系统自动停止
	}
	return ret;
}
//本题
class Solution {
public:
    vector<string> splitWordsBySeparator(vector<string>& words, char separator) {
        vector<string> res;
        for (string &word : words) {
            stringstream ss(word);
            string sub;
            while (getline(ss, sub, separator)) {
                if (!sub.empty()) {
                    res.push_back(sub);
                }
            }
        }
        return res;
    }
};
普通模拟
class Solution {
public:
    vector<string> splitWordsBySeparator(vector<string>& words, char separator) {
        vector<string> ans;
        for (string &word : words) {
            word += separator;//这一步省去了很多corner case的处理,让结尾也有分隔符,记住这个思想
            string str = "";
            for (char ch : word) {//注意到string可以看做char数组,因此char element可以range-based for loop
                if (ch != separator) {
                    str += ch;
                } else {
                    if (!str.empty()) {
                        ans.push_back(std::move(str));
                        str.clear();//clear()
                    }
                }
            }
        }
        return ans;
    }
};
98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

中序遍历

二叉搜索树的中序遍历序列为一个严格递增数组

因此我们只需要用一个变量pre维护中序遍历中上一个节点的值,将当前节点值与其比较即可

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        long pre = LONG_MIN;//用全局变量,维护访问序列中上一个节点的值
        function<bool(TreeNode*)> check = [&](TreeNode* root)->bool{
            if(!root) return true;
            if(!check(root->left)||root->val<=pre) return false;//左,中
            pre = root->val;//维护访问序列中上一个节点的值
            return check(root->right);//右
        };
        return check(root);
    }
};
后序遍历

注意将所有的变量设置为long

与前序遍历中将节点值范围往下传相反,自底向上将节点值范围往上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看起来对于当前节点,只需要左边返回一个最大值,右边返回一个最小值,比较当前节点值即可

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是不对的,比如将上图的4改成6,此时5的左子树返回的是3而非6

因此左右子树需要返回取值范围,用范围来保证左子树的最大值和右子树的最小值的正确性

取到左右子树的范围后,只需比较当前节点值是否满足 > 左子树最大值,< 右子树最小值,符合以上的为二叉搜索树

递归到空节点时,返回{LONG_MAX,LONG_MIN}来保证父节点值进行比较时肯定为二叉搜索树,

如果当前树不为二叉搜索树,返回{LONG_MIN,LONG_MAX}保证父节点值进行比较时肯定为二叉搜索树,也可作为结果的bool false返回

class Solution {
public:
    pair<long,long> postorder(TreeNode* root){
        if(!root) return pair(LONG_MAX,LONG_MIN);//保证叶子节点在验证左右节点时肯定是二叉搜索树
        auto [left_min,left_max] = postorder(root->left);
        auto [right_min,right_max] = postorder(root->right);
        if(root->val<=left_max||root->val>=right_min) return pair(LONG_MIN,LONG_MAX);
        return {min((long)root->val,left_min),max((long)root->val,right_max)};
    }
    bool isValidBST(TreeNode* root) {
        auto [left,right] = postorder(root);
        return left!=LONG_MIN;
    }
};
543. 二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。

示例 2:

输入:root = [1,2]
输出:1
递归/树dp

dp:如果能找到和原问题相似的子问题,并建立起他们的联系,那就可以用递归解决该问题

递归的部分与求二叉树最大深度做法一致

这题坑点在最长直径不一定过根节点必须在所有内部节点中都比较:以该点作为折点,可能的最长直径

也就是 求最大深度的过程中逐节点更新直径,从 当前节点 拐弯 的最大链长就等于 左右子树的最大链长+2

我当时是这么想的:递归三部曲

  • 返回值:当前子树的最大深度(链长),作为其父亲树的求直径组件
  • 递归体内逻辑:已经获得了左右子树的最大深度(链长),那么需要考虑两种全局最大直径的可能性
    1. 有可能以该节点为根节点的子树,左右子树最大链长+2 == 全局最大直径
    2. 也有可能将当前节点子树最大链长(max(左右子树最大链长)+1)转发给父亲节点,在父亲节点的递归体逻辑内判断是否最大直径,也即上面的返回值情况
      • 比如已求得当前节点的左子树最大深度(链长),那么该数值要么联合右子树的数值并+2,要么+1转发给父亲节点
    3. 所以递归体内用1看能不能更新全局最大深度即可
  • 其实每个节点就两种可能:直径在这里转弯,直径不在这里转弯
  • 结束条件:叶子节点
class Solution {
public:
    int dfs(TreeNode* root,int &ans){
        if(!root) return -1;//叶节点的深度为0,因为考虑到后面要加上与其父节点的链长1,因此这里返回值设成-1
        int left = dfs(root->left,ans);//左子树的最大链长
        int right = dfs(root->right,ans);//右子树的最大链长
        //在这一步,对于全局变量 最大直径来说,有两种可能,一种是以当前节点作为根节点,左右子树的最大链长连在一起形成的直径,或者将该节点作为普通节点,向上转发该子树的最大链长
        ans = max(ans,left+right+2);
        return max(left,right)+1;
    }
    int diameterOfBinaryTree(TreeNode* root) {
        int ans = 0;
        dfs(root,ans);
        return ans;
    }
};
124.二叉树中的最大路径和

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

递归/树dp

和上面那题一样,在每个节点处判断两种可能:

  • 以这个节点作为根节点的子树条件下,计算有可能的最大路径和,也就是递归三部曲中,当前递归层内要做的事情

  • 把当前节点作为最大路径和中的一环,传递子树最大路径和给父节点,也就是返回值

  • 注意用max(0,…)来消除子树路径和中可能的负数,也即不走路径和为负数的子树路径

  • 在递归函数中,你计算了 leftright 的和,并比较它们与0的大小,建议使用 max(left, 0)max(right, 0) 来确保负值被忽略。

class Solution {
public:
    int recursion(TreeNode* root,int &ans){
        if(!root) return 0;
        int left = max(0,recursion(root->left,ans));//注意这里,用max(0,...)来消除子树路径和中可能的负数
        int right = max(0,recursion(root->right,ans));
        ans = max(ans,left+right+root->val);
        return max(left,right)+root->val;
    }
    int maxPathSum(TreeNode* root) {
        int ans = root->val;
        recursion(root,ans);
        return ans;
    }
};
  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值