C++,STL,stack 栈:从简单栈到单调栈,解决经典栈问题

  • Post author:
  • Post category:其他



容器适配器

是一个封装了

序列容器

的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。将介绍 3 种容器适配器,分别是 stack、queue、priority_queue:


stack<T>

:是一个默认封装了 deque<T> 容器的适配器类模板,默认实现的是一个后入先出(Last-In-First-Out,LIFO)的压入栈。stack<T> 模板定义在头文件 stack 中。


stack<T,Container=deque<T>>

模板类提供了 2 个参数,

通过指定第二个模板类型参数,我们可以使用出 deque 容器外的其它序列式容器,只要该容器支持 empty()、size()、back()、push_back()、pop_back() 这 5 个成员函数即可。序列式容器中同时包含这 5 个成员函数的,有 vector、deque 和 list 这 3 个容器。因此,stack 适配器的基础容器可以是它们 3 个中任何一个。例如,下面展示了如何定义一个使用 list 基础容器的 stack 适配器:

std::stack<std::string, std::list<int>> values;
  • 1、栈

1. 首先,明白一下栈常用的方法。具体的形象化表示可如下图。

stack 常用的一些成员函数

empty() //判断是否为空,为空返回true
size() //返回stack元素个数

top() //返回栈顶元素
push() //在栈顶添加元素
pop() //删除栈顶元素

2、栈的应用


  • 判断字符串是否合法,利用先进后出进行模拟

【题目】字符串中只有字符'(‘和’)’。合法字符串需要括号可以配对。比如:

输入:”()”

输出:true

解释:(),()(),(())是合法的。)(,()(,(()是非法的。

#include <stack>
using namespace std;
bool checkvalid(string s){
    if (s.empty())
        return true;
    if (s.size() % 2 != 0) // s.size()&1 == 1 为奇数
        return false;

    stack<char> sch;
    int len = s.size();
    for (int i = 0; i < len; i++){
        char c = s[i];
        if (c == '(')
            sch.push(c);
        if (c == ')'){
            if (sch.empty())
                return fasle;
            sch.pop();
    }
    return sch.empty();
}   

注意要考虑特殊情况。字符串为空;字符串只有 1 个或者奇数个。

上面的解法即是,用栈进行模拟,从左边往右边读单个字符,如果是左括号,则压入栈内,如果是右括号,先判断栈是否为空,如果为空则返回false,否则的话,删除栈顶元素(即两个括号相消)。

延申两个方向,1、上面的解法,我们可以看到时间复杂度是 O(n),最差情况下,空间复杂度是O(n)。即每个元素都要进入栈内。


深度延申

,怎样降低空间复杂度。我们可以直接用计数器来判断。

栈中元素都相同时,实际上没有必要使用栈,只需要记录栈中元素个数。

#include <stack>
using namespace std;
bool checkvalid(string s){
    if (s.empty())
        return true;
    if (s.size() % 2 != 0) // s.size()&1 == 1 为奇数
        return false;

    int coutStack = 0;
    int len = s.size();
    for (int i = 0; i < len; i++){
        char c = s[i];
        if (c == '(')
            coutStack++;
        if (c == ')'){
            if (cooutStack < 0)
                return fasle;
            coutStack--;
    }
    return coutStack == 0;
}   

上面这个空间复杂度会降成 O(1);


广度延申

,给定一个只包括 ‘(‘,’)’,'{‘,’}’,'[‘,’]’ 的字符串,判断字符串是否有效。有效字符串需满足:

左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合;注意空字符串可被认为是有效字符串

//用栈,把三种情况都举例出来比较
 bool isValid(string s) {
        if (s.length() & 0x01) {
            return false;
        }
        stack<char> t;
        for (auto c : s) {
            if (c == '{' || c == '[' || c == '(') {
                t.push(c);
            } else if (c == ')') {
                if (t.empty() || t.top() != '(') {
                    return false;
                }
                t.pop();
            } else if (c == '}') {
                if (t.empty() || t.top() != '{') {
                    return false;
                }
                t.pop();
            } else if (c == ']') {
                if (t.empty() || t.top() != '[') {
                    return false;
                }
                t.pop();
            } else {
                return false;
            }
        }

        return t.empty();
    }
//用 map 去存储。比较的时候,直接取值
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (n % 2 == 1) {
            return false;
        }

        unordered_map<char, char> pairs = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };
        stack<char> stk;
        for (char ch: s) {
            if (pairs.count(ch)) {
                if (stk.empty() || stk.top() != pairs[ch]) {
                    return false;
                }
                stk.pop();
            }
            else {
                stk.push(ch);
            }
        }
        return stk.empty();
    }
};
  • 单调栈的应用

【题目】一个整数数组 A,找到每个元素:右边第一个比我小的下标位置,没有则用 -1 表示。

输入:[5, 2]

输出:[1, -1]

解释:因为元素 5 的右边离我最近且比我小的位置应该是 A[1],最后一个元素 2 右边没有比 2 小的元素,所以应该输出 -1。

单调栈的意思就是,在栈内保持元素是单调的(单调增或者单调减)。

以 {1,2,4,9,4,0,5}  为例,进行模拟。

Step 1. 首先将 A[0] = 1 的下标 0 入栈。

Step 2. 将 A[1] = 2 的下标 1 入栈。满足单调栈。

Step 3. 将 A[2] = 4 的下标 2 入栈。满足单调栈。

Step 4. 将 A[3] = 9 的下标 3 入栈。满足单调栈。

Step 5. 将 A[4] = 4 的下标 4 入栈时,不满足单调性,需要将 A[3] = 9 从栈中弹出去。下标 4 将栈中下标 3 弹出栈,记录 A[3] 右边更小的是 index = 4。

Step 6. 将 A[5] = 0 的下标 5 入栈时,不满足单调性,需要将 A[4] = 4 从栈中弹出去。下标 5 将下标 4 弹出栈,记录 A[4] 右边更小的是 index = 5。A[5] = 0 会将栈中的下标 0, 1, 2 都弹出栈,因此也需要记录相应下标右边比其小的下标为 5,再将 A[5] = 0 的下标 5 放入栈中。

Step 7. 将 A[6] = 5 的下标 6 放入栈中。满足单调性。

Step 8. 此时,再也没有元素要入栈了,那么栈中的元素右边没有比其更小的元素。因此设置为 -1.

vector<int> finalResult(vector<int>& nums) {
    
    int len = nums.size();
    if(len == 0) return{};

    stack<int> snums;
    vector<int> result(len);

    for (int i = 0; i < len; i++) {
        const int x = nums[i];
        while (!result.empty() && nums[snums.top()] > x) {
            result[snums.top()] = i;
            snums.pop();
        }
        snums.push(i);
    }

    while (!snums.empty()) {
        result[snums.top()] = -1;
        snums.pop();
    }

    return result;
}

每个元素只入栈一次,出栈一次,所以时间复杂度为 O(N),而空间复杂度为 O(N),因为最差情况可能会把所有的元素都入栈。相比较暴力搜索的O(n2)的时间复杂度,在数据量很大的情况下,效果还是很好的。


字典序最小的 k 个数的子序列

【题目】给定一个正整数数组和 k,要求依次取出 k 个数,输出其中数组的一个子序列,需要满足:1. 长度为 k;2.字典序最小。

输入:nums = [3,5,2,6], k = 2

输出:[2,6]

解释:在所有可能的解:{[3,5], [3,2], [3,6], [5,2], [5,6], [2,6]} 中,[2,6] 字典序最小。

vector<int> finalResult(vector<int>& nums, int& k) {
	int len = nums.size();
	if (!len || k == 0) return {};

	vector<int>	result(k);
	stack<int> stnum;

	for (int i = 0; i < len; i++) {
		const int x = nums[i];
		int right = len - i;

		while (!stnum.empty() && x < stnum.top() && (right + stnum.size()) < k) {
			stnum.pop();
		}
		stnum.push(x);
	}
	while (stnum.size() > k) {
		stnum.pop();	
	}

	for (int i = k-1; i >= 0; i++) {
		result[i] = stnum.top();
		stnum.pop();
	}
	return result;
}

4. 边界问题:我们还是需要小心题目的边界。

Case 1:假设数组右边有一个最小的数,这个最小的数会把左边的数全部都消掉,然后递增栈里面就只剩下这 1 个数了。这跟题意有点不符合,题意需要的是找到 k = 2 个出来。

解决办法:不过你可以想一想,是不是可以控制一下消去的数目。当剩下的数字个数与栈中的元素刚好能凑够 k 个数时,就不能再消除了,

Case 2:如果数组是一个升序的数组,那么此时所有的元素都会被压栈。栈中的数目有可能远远超出 k 个。

解决办法:只需要把栈中的多出来的数字弹出来即可。



版权声明:本文为JACKSONMHLK原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。