单调栈
[ code ]

本篇博客的内容是单调栈。顾名思义,单调栈就是元素单调排序的栈,分为单调递增栈和单调递减栈。

当往栈中存入新元素时,为了维护栈的单调性,对于单调递增栈,如果栈顶元素大于新元素,那么要不断地弹出栈顶元素直到栈空了或者栈顶元素小于新元素。对于单调递减栈则是弹出小于新元素的栈顶元素。

在之前的这篇博客中曾经提到了两道用单调栈解决的题目:一个是接雨水,另一个是柱状图中最大的矩形。做完这两道题之后我感觉还是不太会用单调栈(主要是不知道到底什么情况下可以用),所以这篇博客里记录了LeetCode中一些有关单调栈的问题的题解,通过找一找这些问题的共性来熟悉单调栈的使用方法。

NO. 316 去除重复字母

题目:

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

例如:

输入: “bcabc” 输出:“abc”

题解:

题目的需求是删除重复的字母,且保证删除之后的字符串是所有可能结果中字典序最小的。这道题可以用贪心算法+单调栈来解(虽然不看题解完全想不到就是了)。

首先可以用一个长度为26的数组记录下每个字母出现的次数,这样就能知道需要删除哪些字母。其次,为了保证结果是字典序最小,那么肯定需要尽量删除排序靠前的,且值较大的元素。我们在遍历字符串的时候,用一个单调递增栈存储遍历过的字符。当栈顶元素a大于当前遍历到的元素b,且元素a在未被遍历到的部分还会出现(说明a可以被删掉),那么就将a删掉,并且在最初建立的数组中将a对应的值减一。

于此同时,我们需要注意的是栈中不可以出现重复的元素,因此还需要另一个长度为26的数组来记录栈中元素的出现情况。

代码如下:

class Solution {
public:
    string removeDuplicateLetters(string s) {
        vector<int> vis(26), num(26);

        //记录每个单词出现的次数
        for (char ch : s) {
            num[ch - 'a']++;
        }

        //单调递增栈
        string stk;
        for (char ch : s) {
            //栈中是否有这个字符
            if (!vis[ch - 'a']) {
                while (!stk.empty() && stk.back() > ch) {
                    //栈顶元素比当前元素大
                    //且该元素后面还会出现
                    //就把他删了
                    if (num[stk.back() - 'a'] > 0) {
                        vis[stk.back() - 'a'] = 0;
                        stk.pop_back();
                    } else {
                        break;
                    }
                }
                //将当前字符push进去
                vis[ch - 'a'] = 1;
                stk.push_back(ch);
            }
            //出现次数-1
            num[ch - 'a'] --;
        }
        return stk;
    }
};

NO.321 拼接最大数

题目:

给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。

求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。

一个例子:

输入: nums1 = [3, 4, 6, 5], nums2 = [9, 1, 2, 5, 8, 3], k = 5

输出: [9, 8, 6, 5, 3]

题解:

题目需要从两个数组里选出共k个数字,并将他们按照原顺序拼接,得到最大值。

假设我们从nums1中选取i个数字,那么从nums2中要选取(k-i)个数字。从单个数组中选数字的过程可以采用单调递减栈来解决,当当前元素大于栈顶元素时,就弹出栈顶元素。在维护单调递减栈的过程中,需要保证栈中的元素个数大于等于我们从nums1(或nums2)中选取的元素的个数,这个可以通过统计栈中已经被删除的元素数量来实现,比如对于nums1来说,我们共需要删掉nums1.size() - i 个数字。如果在遍历结束之前就删够了元素,那么就接受所有未被遍历到的元素,并退出循环,如果遍历结束之后还没有删够,那么只返回从栈底往上数的前i个元素。

这一步结束之后,我们已经获取了k个数字,接下来需要将他们合并,在保持原顺序的同时获得最大值。这个合并类似于归并排序中的合并过程,但是如果双方数组里有一模一样的元素,那么需要通过判断后续元素的大小来决定优先选择哪一个。

代码有些长,需要写多个函数,包括:单调栈选取元素,合并元素,以及当合并时遇到相同的元素需要优选选择哪一个的判断函数。

class Solution {
public:
    vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
        vector<int> rtn(k,0);
        int m = nums1.size();
        int n = nums2.size();
        
        //需要从nums1中选择的最少的数字
        int minnum = max(0,k-n);
        //最多
        int maxnum = min(k,m);
        //从nums1中选取i个数字,从nums2中选取k-i个
        
        for(int i = minnum; i <= maxnum; i++)
        {
            vector<int> sub1 = select_num(nums1,i);
            vector<int> sub2 = select_num(nums2,k-i);
            vector<int> cur = merge(sub1,sub2);
            //比较二者的值
            if(comp(cur,rtn))
                rtn = cur;
        }
        return rtn;
    }

    vector<int> select_num(vector<int> nums, int k)
    {
        if(k == 0)
            return {};
        //单调递减栈
        vector<int> stk;

        //需要删除的数字
        int cnt = nums.size() - k; 

        for (int it : nums) 
        {
            //栈非空,数字还没删够,且栈顶元素小于当前元素
            while (cnt != 0 && !stk.empty() && it > *stk.rbegin()) 
            {
                stk.pop_back();
                cnt--;
            }
            stk.push_back(it);
        }

        //如果还没删完
        while (cnt != 0) {
            stk.pop_back();
            cnt--;
        }
        return stk;
    }
    
    //合并数组
    vector<int> merge(vector<int>& sub1, vector<int>& sub2) {
        if(sub1.size() == 0)
            return sub2;
        if(sub2.size() == 0)
            return sub1;
        vector<int> rtn;
        int st1 = 0,st2 = 0;
        while(st1 < sub1.size() && st2 < sub2.size())
        {
            if(sub1[st1] > sub2[st2])
            {
                rtn.push_back(sub1[st1]);
                st1++;
            }
            else if(sub1[st1] < sub2[st2])
            {
                rtn.push_back(sub2[st2]);
                st2++;
            }
            else //注意,如果此时两个数组中的元素相等,需要进行额外的判断
            {
                if(comp2(sub1,sub2,st1,st2))
                {
                    rtn.push_back(sub1[st1]);
                    st1++;
                }
                else
                {
                    rtn.push_back(sub2[st2]);
                    st2++;
                }
            }
        }
        while(st1 < sub1.size())
        {
            rtn.push_back(sub1[st1]);
            st1++;
        }
        while(st2 < sub2.size())
        {
            rtn.push_back(sub2[st2]);
            st2++;
        }
        return rtn;
    }

    bool comp(vector<int> cur, vector<int> rtn) {
        int k = cur.size();
        for(int i = 0; i < k; i++)
        {
            if(cur[i] > rtn[i])
                return true;
            else if(cur[i] < rtn[i])
                return false;
        }
        return false;
    }

    //当合并数组时有两个相等的元素,判断应该选取哪一个
    //返回true则选择sub1中的元素,反之选择sub2中的元素
    //需要比较后面的元素的大小才能决定
    bool comp2(vector<int> sub1, vector<int> sub2, int idx1, int idx2)
    {
        while(idx1 < sub1.size() && idx2 < sub2.size())
        {
            if(sub1[idx1] == sub2[idx2])
            {
                idx1++;
                idx2++;
            }
            else if(sub1[idx1] > sub2[idx2])
                return true;
            else
                return false;
        }
        if(idx1<sub1.size())
            return true;
        return false;
    }
};

NO. 739 最大温度

题目:

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指在第 i 天之后,才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

一个例子:

输入: temperatures = [73,74,75,71,69,72,76,73]

输出: [1,1,4,2,1,1,0,0]

题解:

这道题可以用单调递减栈来解决(比前两道要简单很多)。在遍历temperatures的时候维护一个单调递减栈,栈内存储的是日期(就是temperatures元素对应的下标)。如果当前气温大于栈顶值对应的气温,说明这一天的气温比前面的某几天要高,那么就将小于当前元素的栈顶都pop出来,并算出相应的日期差。

代码如下:

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        stack<int> stk; //单调递减
        vector<int> rtn(n,0);
        for(int i = 0; i < n; i++)
        {
            while(!stk.empty() && temperatures[stk.top()] < temperatures[i])
            {
                //int cur = stk.top();
                rtn[stk.top()] = i-stk.top();
                stk.pop();
            }
            stk.push(i);
        }
        return rtn;
    }
};

对比这几道题的题目可以发现,题目要求里都出现了类似于“求最大最小值”、“保留数组原顺序”、“不能打乱相对顺序”的要求。所以如果遇到需要求最大/最小值,且无法进行排序操作的场合,可以考虑采用单调栈来处理。而且如果需要处理的是“最大值”或“较大值”,就用单调递减栈(后两题),反之就用单调递增栈(第一题)。


Reference