Leetcode刷题笔记--栈和队列


前言

栈和队列是最常见的数据结构。他们是两个针锋相对却又相互关联的数据结构。栈的特点是后进先出,队列是先进先出。其他的不多说,知道这两点就完事了,直接开始!


一、栈和队列的基本用法

首先介绍栈的基本用法以及常见的函数。如下所示:

#include <stack>

void basicStack() {
    // 创建一个整型栈
    std::stack<int> intStack;

    // 向栈中添加元素
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);

    // 获取栈顶元素并弹出
    std::cout << "Top element: " << intStack.top() << std::endl;
    intStack.pop();

    // 获取栈中元素数量
    std::cout << "Size of stack: " << intStack.size() << std::endl;

    // 检查栈是否为空
    if (intStack.empty()) {
        std::cout << "Stack is empty" << std::endl;
    } else {
        std::cout << "Stack is not empty" << std::endl;
    }
}

接下来是队列的基本用法,简直和栈一模一样,只不过栈获取栈顶元素的函数是top(),而队列获取的是队首元素或者队尾元素,使用的函数为front()和back()。

#include <queue>

void basicQueue() {
    // 创建一个整型队列
    std::queue<int> intQueue;

    // 向队列中添加元素
    intQueue.push(10);
    intQueue.push(20);
    intQueue.push(30);

    // 获取队首元素并弹出
    std::cout << "Front element: " << intQueue.front() << std::endl;
    intQueue.pop();

    // 获取队列中元素数量
    std::cout << "Size of queue: " << intQueue.size() << std::endl;

    // 检查队列是否为空
    if (intQueue.empty()) {
        std::cout << "Queue is empty" << std::endl;
    } else {
        std::cout << "Queue is not empty" << std::endl;
    }
}

接下来我们看两个有趣的题目。

1.1用栈实现队列

1.用栈实现队列
这个问题很好想,首先用一个栈肯定是没法实现的,所以我们用两个栈,即可轻松实现。我们设置一个入栈stackin和一个出栈stackout进行模拟。对于push函数,只需要stackin.push(x)即可,主要的难点在于pop()函数的实现。
如下图所示,我们原本push了三个元素,如果我们想执行pop函数,需要先移除1,因为1是先进入的,这样的话,我们直接把所有元素都放到出栈中,就可以依次pop啦。

基本实现流程
代码应该这样写:

while (!stackin.empty())
{	
	stackout.push(stackin.top());
	stackin.pop();
}
stackout.pop();

但是这样还不行,我们还需要考虑另外一种情况,因为pop()函数只会处理一个元素,如果我做一次pop函数之后,再执行一下push(4),再想执行pop()函数,是不是就会出现问题了呢?执行的代码如下:

	obj->push(1);
	obj->push(2);
	obj->push(3);
	obj->pop();
	obj->push(4);
	obj->pop();

如果按照上面的pop函数,第二次pop会移除4,而正确的答案是2.出现这个问题的原因就是出栈stackout的元素不为空,我们就不能再将stackin的元素放进来了。只有当stackout为空时候,我们才能把stackin的元素全部放进来,这样才能解决问题。最终的pop()函数如下:

int pop() {
	if (stackout.empty())
	{
		while (!stackin.empty())
		{	
			stackout.push(stackin.top());
			stackin.pop();
		}
		
	}
	int ans = stackout.top();
	stackout.pop();
	return ans;
}

其余的函数实现就很简单了,就不赘述了。

1.2用队列实现栈

2.用队列实现栈

这个题目和上面的简直一模一样,但是用上面的思路两个 队列是行不通的,因为两个队列串起来并没有改变元素的出入顺序,这取决于队列的特性,不过我们转念一想,直接一个队列,每次pop时候,把队头元素放到队尾,只剩一个元素,这样顺序就和栈的出栈顺序一样了。这样新加入的元素排列在队尾,我们要push的话,每次都把前面的元素都放到队尾,剩者一个就可以了。

 int pop() {
        int size = que.size();
        size--;
        while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
            que.push(que.front());
            que.pop();
        }
        int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
        que.pop();
        return result;
    }

二、栈的拿手好戏–匹配类题目

1.有效的括号

首先我们分析一下成为有效的括号的条件。经过分析可知,要想作为一个有效的括号,那么需要满足这些情况。

  1. ( 必须在 )前面出现,【 必须在】前面出现,{ 必须在 } 前面出现,否则直接不合法了。
  2. 最晚出现的左括号必须和下一个最早出现的右括号匹配。
  3. 不能有多余的括号。

咱们看下面的代码,有一些亮点,当出现左括号的时候,我们直接在栈中添加右括号,这样的话,就只需要比较右括号是否相同,就能判断是否对应了。如果刚开始就出现了右括号,而没有相对应的左括号出现过,那么就直接返回false了。最后为了解决第三种情况,返回st.empty()即可。

bool isValid(string s) {
    if (s.size() % 2 == 1) return false;
        stack<char> st;
        for (int i = 0; i < s.size(); i++){
            if (s[i] == '(') st.push(')');
            else if (s[i] == '{') st.push('}');
            else if (s[i] == '[') st.push(']');
            else if (st.empty() || st.top() != s[i]) return false;
            else st.pop();
        }
        return st.empty();
    }

2.删除字符串中的所有相邻重复项
这种题目就叫做消消乐题目,把相同的元素全消了,就留下不同的元素。比如 abba,检查到bb相同,消除之后字符串变为aa,继续消除,字符串全消除完了。这和栈的匹配问题非常相似。栈的好处在于,它可以比较栈顶元素和输入元素,这样的话,就可以很大程度上简化比较的流程。如下,这一句关键代码直接拿下本题。

if(st.size() && st.back() == s[i]) 

本题有个有趣的地方,如果使用栈的话,返回的字符串的顺序在栈中是正确的,但是我们想要拿出来,就变成反序的了,在转化起来有些麻烦,我们转念一想,由于 std::string类本身就提供了类似「入栈」和「出栈」的接口,因此我们直接将需要被返回的字符串作为栈即可。简直是妙哉!代码如下。

string removeDuplicates(string s) {
	 string st;
        int n = s.size();
        for(int i = 0; i < n; i++)
        {
            if(st.size() && st.back() == s[i]) 
            {
                st.pop_back();
            }
            else st += s[i];
        }
        return st;
    }

3.检查替换后的词是否有效

这个题目和上面简直一样,也是消消乐,遇到一组abc就消除,然后看消除之后的能否继续组成abc,如果最后全消除完了,就说明是有效的。对于这种消消乐题目,栈已经不得不展示一波操作了。
现在的问题在于,之前我们进行匹配的题目,都是两个元素相互匹配,比如说括号,和相同的元素。但是这个题目居然是三个字母才能组成一对儿,这就相对于之前有些变化了。那我可以先比较两个,咱先找找有没有bc,有的话看看把b弹出,看看栈顶是不是a,是的话把a也弹出来,不是的话,直接返回false了,因为我们总是优先弹出检测到的abc,这都检测不到,说明已经寄了。
同时我们还要注意,检测栈顶元素或者出栈操作时候,务必要判断是否为空栈!这写的if else有点多。。。但是过了,还超过百分之九十多!其实把判断是否为空栈的代码去掉,代码已经很简洁了。

bool isValid(string s) {
	stack<char>st;
	for (int i = 0; i < s.size(); i++)
	{	
		if (!st.empty()) {
			if (st.top() == 'b' && s[i] == 'c') {
				st.pop();
				if (!st.empty()) {
					if (st.top() == 'a') st.pop();
					else return false;
				}
				else return false;
			}
			else st.push(s[i]);
		}
		else st.push(s[i]);
	}
	return st.empty();
    }

4.逆波兰表达式求值
这道题简直是太经典了!简直是栈的量身定制题目。不过先别着急解题。首先我们需要搞懂的是, 什么是逆波兰表达式。。。这咱没听过,只能百度了。

逆波兰表达式(Reverse Polish Notation,RPN),也称为后缀表达式,是一种数学表达式的表示方法,其中操作符在操作数之后。逆波兰表达式不需要括号来标识运算的优先顺序,因为它使用后缀表示法,每个操作符具有固定数量的操作数。

本题最关键的地方在于,一个运算符连接的只能是两个数,并且返回一个数。所以,我们在遍历字符串数组的时候,需要判断是运算符还是数字。如果是数字的话,我们就把它放到栈中,而每当遇到运算符,这时候需要弹出栈顶的两个元素,然后经过计算,返回一个结果到栈中,这样循环往复。最终栈中只剩一个元素,也就是我们要的结果!

int evalRPN(vector<string>& tokens) {
	stack<int>stk;
	for (const string& token : tokens)
	{
		if (token == "+" || token == "-" || token == "*" || token == "/") {
			int num2 = stk.top(); stk.pop();
			int num1 = stk.top(); stk.pop();
			if (token == "+") stk.push(num1 + num2);
			else if (token == "-") stk.push(num1 - num2);
			else if (token == "*") stk.push(num1 * num2);
			else if (token == "/") stk.push(num1 / num2);
		}
		else stk.push(stoi(token));

	}
	return stk.top();
    }

三、单调栈–拿下“下一个”

单调栈的主要思想是利用栈来维护一个单调递增或单调递减的序列。当新元素入栈时,会将栈内所有不满足单调性要求的元素弹出,保持栈内的元素仍然满足单调性。这样做的好处是,可以快速找到当前元素右边(或左边)第一个比它大(或小)的元素。

在单调栈中,存放的数据是有序的。

递增栈:栈底元素大于栈顶元素。
递减栈:栈底元素小于栈顶元素。

接下来。我们模拟一个单调递增栈:将一个数组从左到右依次入栈:10,3,7,4,12。然后模拟每一步的过程。直接上图。
每次比较遍历到的元素根栈顶元素的大小。若栈为空或者入栈元素值小于栈顶元素,则入栈,否则,把闭入栈元素小的元素全部出栈。
模拟过程
这样的话,我们就可以得到上面模拟过程的代码,也就是单调栈问题的基本模板。

    int n = nums.size();
    // 创建一个单调递增栈,存储元素下标
    stack<int> monoStack;
    // 从左向右遍历数组
    for (int i = 0; i < n; ++i) {
        // 单调递增栈中的元素依次弹出,直到栈顶元素小于当前元素
        while (!monoStack.empty() && nums[i] > nums[monoStack.top()]) {
			//根据题意添加的内容
            monoStack.pop();
        }
        // 当前元素入栈
        monoStack.push(i);

写到这里,我们已经明白了单调栈可以干什么了。要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n),通常情况下,暴力解法是O(n2)。能减少时间的核心就是,用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用单调栈来记录我们遍历过的元素。

所以单调栈题目的关键词是:右边(左边)第一个比当前元素大(小)的元素。 但是就这一句话,就包含了很多信息。

1.右边还是左边的不同是遍历顺序的不同
2.大小的不同是递增栈还是递减栈的不同。

所以做题必须明确这两点,这样的话,就可以很快的解决题目了。同时,我们应该注意,单调栈中可以储存元素的值,也可以储存元素的下标,通常来说,储存下标的的通用性更强,所以我们之后的代码储存元素的下标。

接下来我们直接看一下单调栈的经典习题,来逐步理解相关过程。

3.1 单调栈之小试牛刀

1.每日温度

我们想要找到每一天温度的下一个更高温度的下标,就相当于寻找每个元素的下一个更大元素。就可以按照单调栈的模板来进行修改。首先我们的问题是,我们应该在元素入栈还是出栈时候进行操作?
我们使用的是单调递增栈,所以出栈的判断应该是当前元素大于栈顶元素,也就是说,出栈就证明了一件事情,我们找到了被踢出去元素的下一个更大元素,这个时候,我们就应该记录一下元素的下标了,以便返回相应的结果。
但是单调栈最不容易让人理解的地方在于,他并不是按顺序返回相应的结果的。 怎么理解这句话呢?举个栗子。

假如temperatures = [30,20,50,70,60],我们模拟一下相关情况。首先30入栈,接着20入栈,当遇到50 的时候,20会被弹出,这个时候我们就得到了20的下一个更大元素,就是50,接着30也被弹出,所以30的下一个更大元素也是50,接着50入栈,接着遇到70,也就是50的下一个最大元素,50弹出,70入栈,然后60入栈,没有比60和70更大的元素,这俩元素就呆在栈中。
在这里插入图片描述

我们模拟了整个过程,咱们关注的点在于,每当一个元素出栈,这个出栈元素才有对应的结果,所以并不是按照顺序产生结果的,而是按照对应有一个出栈的下标,有一个结果。所以,关键的来了,如果我们用vector储存每个元素的结果的话,那必然是在出栈判定的while循环中,进行一个ans[a] = b的操作,而不会使用push_back这种操作。
然后我们看上面的例子,那70和60都没出栈,说明没有下一个更大元素,也不会操作他们俩位置的ans数组,这样的话,我们必须对ans进行初始化了,默认所有元素都找不到,置0,这样就可以了。
分析了那么多,代码已经是小菜一碟啦。

vector<int> dailyTemperatures(vector<int>& temperatures) {
	int n = temperatures.size();
	stack<int>st;
	vector<int>ans(n, 0);
	for (int i = 0; i < n; i++)
	{
		while (!st.empty() && temperatures[i] > temperatures[st.top()])
		{
			int num = i - st.top();
			ans[st.top()] = num;
			st.pop();

		}
		st.push(i);
	}
	return ans;
    }

2.下一个更大元素
这个题目稍微变了一下,不在自家找下一个更大元素,跑别人家找去了而且我们要找到数组一和数组二的对应关系。我们仔细分析一下,这不就是找nums2的下一个更大元素嘛,找完之后对应一下,看nums1要哪个,给他便是了。
如果我们使用哈希表,nums2的元素当作key,把他的下一个更大元素当作value,那么很简单的便可以和nums1里面的值对应上了!简直是太妙了!
第一步,使用哈希表和单调栈处理nums2,但是我们需要注意的是,对没有出栈元素的处理,因为哈希表是无序表所以我们不可能提前将他置1,只能在处理一步,将栈中剩余元素弹出,对应的哈希值为-1。之后一行代码就能对应nums1了!哈希真好用。

vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
	stack<int>st;
	vector<int>ans;
	unordered_map<int, int>map;
	int n = nums2.size();
	for (int i = 0; i < n; i++)
	{
		while (!st.empty() && nums2[i] > nums2[st.top()])
		{
			map[nums2[st.top()]] = nums2[i];
			st.pop();
		}
		st.push(i);
	}
	//处理栈中剩余的元素
	while (!st.empty()) {
		map[nums2[st.top()]] = -1; // 栈中剩余元素没有下一个更大元素
		st.pop();
	}
	for (auto num : nums1) {
		ans.push_back(map[num]);
	}

	return ans;
}

3.队列中可以看到的人数
这道题目相对于前两道题更加复杂了,我们要找每个人右边能看到的人数。题目是这样描述的:一个人能 看到 他右边另一个人的条件是这两人之间的所有人都比他们两人 矮 。更正式的,第 i 个人能看到第 j 个人的条件是 i < j 且 min(heights[i], heights[j]) > max(heights[i+1], heights[i+2], …, heights[j-1]) 。
这样的话,我们还能用单调栈来处理吗?之前我们讲过,单调栈处理的是下一个更大元素,而本题似乎不是这个要求,把单调栈和本题联系起来,似乎有些困难。
我们认真分析一下,其实还是很有关系的,一个人能看到的人数最多截止到下一个更高的人。因为在这个人和他的下一个更高的人之间的元素都比他俩矮。这样的话我们可以很方便的使用单调栈统计中间的人数。但是这个人并不一定能看到和下一个更高的人中间所有人,为什么呢?看下面的图。小人1最多能看到小人5,因为后面的人会被小人5挡到。但是小人1却看不到小人4,因为小人3挡到了小人4,在小人1和4中间,有更高的元素出现。这就是本题的关键难点。 我们要想如果使用单调栈,我们应该在出栈入栈做什么样的操作,才能完成这个目标。
我们可以发现,当前的人能看到的人的身高是单调递增的,小人1可以看到2,3,5,他们身高逐渐递增。
在这里插入图片描述
先模拟一下入栈过程,就明白该如何写了。我们已经知道了10可以看到三个人,分别是6,8,11。所以如果按照以往的思路,如果只在10出栈时候再进行一个统计计算的话,是达不到目的的。我们的目的是去除5这类在小人中间却更小的元素。一个人可以看到的人数增加途径分为两方面,一方面是看到下一个更大元素,这时候属于检测完成了,可以弹出了,而另外一方面,当遇到6和8这些元素是,他们在栈中与10是相邻的,这说明他们中间不存在比6或者8更大的数,这时候10可以看到的元素就能增加了。你看下图的入栈过程,5与10 是隔开的,说明5和10中存在比5更大的数,这样的话,就不能增加10的结果了。
在这里插入图片描述
关键的代码在于,在push元素之前, 判断栈是否为空,如果不为空,说明栈顶元素能看到将要push来的元素,能看到的人数加一。

if(!st.empty()) ans[st.top()] += 1;

总体的代码如下:

vector<int> canSeePersonsCount(vector<int>& heights) {
	int n = heights.size();
	stack<int>st;
	vector<int>ans(n,0);
	for (int i = 0; i < n; i++)
	{
		//遇到下一个更大元素,正常处理
		while (!st.empty() && heights[i] > heights[st.top()]) {

			ans[st.top()] += 1;
			st.pop();
		}
		//在当前元素和下一个更大元素中间的处理
		if(!st.empty()) ans[st.top()] += 1;

		st.push(i);
	}
	return ans;
}

到这里还没有结束。我看了一下官方的题解,居然是从右往左遍历!这让我大吃一惊。之前我也说过,找右边下一个更大元素时候,是从左往右,如果找左边下一个更大元素时候,那就是从右往左。但这道题为什么能从右往左,而且代码也十分简单呢?接下来我们分析一下,遍历顺序不同造成的影响。先贴一个这道题的官方代码,大家找找不同。

vector<int> canSeePersonsCount(vector<int>& heights) {
        int n = heights.size();
        vector<int> ans(n);
        stack<int> st;
        for (int i = n - 1; i >= 0; i--) {
            while (!st.empty() && st.top() < heights[i]) {
                st.pop();
                ans[i]++;
            }
            if (!st.empty()) { // 还可以再看到一个人
                ans[i]++;
            }
            st.push(heights[i]);
        }
        return ans;
    }

3.2遍历顺序对单调栈的影响

单调栈的本质是储存已经遍历过的元素,并且剔除无用元素。假如我们要找右边下一个更大元素。那么从左往右遍历的话,会出现什么情况呢?我们先把第一个元素用栈储存了起来,但是如果他的下一个更大元素在数组末尾的话,那我们只能等到最后才能弹出第一个元素,得出他的结果。也就是我之前说的,结果数组不是按照顺序返回的,这时候一般使用。

ans[st.top()] = num;

我们转念一想,如果从右往左遍历的话,我们储存的元素是从后往前的,所以每次我们当前元素的下一个更大元素我们总会储存过该目标,那么我们按照顺序,n-1,n-2,…1,0得到结果数组,是不是也可以了呢?这时候我们使用

ans[i] = num;

上面我讲的是两个遍历顺序的本质区别,就是是否按照顺序得出结果! 其中由于出栈入栈的元素改变,导致代码的逻辑也需要修改,尤其是获取栈顶元素与出栈的顺序也需要考虑。这里,我在列出找下一个更大元素的两个遍历顺序的代码,就不过多解释啦。

vector<int> left_to_right(vector<int>& nums) {
	int n = nums.size();
	stack<int>st;
	vector<int>ans(n, -1);
	for (int i = 0; i < n; i++)
	//for (int i = n-1; i >=0; i--)
	{
		while (!st.empty() && nums[i] > nums[st.top()])
		{
			ans[st.top()] = nums[i];
			st.pop();

		}
		//if (!st.empty()) { ans[i] = nums[st.top()]; }
		st.push(i);
	}
	return ans;
}
vector<int> right_to_left(vector<int>& nums) {
	int n = nums.size();
	stack<int>st;
	vector<int>ans(n, -1);
	for (int i = n-1; i >=0; i--)
	{
		while (!st.empty() && nums[i] > nums[st.top()])
		{
			ans[i] = nums[i];
			st.pop();

		}
		if (!st.empty()) { ans[i] = nums[st.top()]; }
		st.push(i);
	}
	return ans;
}

3.3 左右开弓的单调栈!

1.接雨水
这个题目其实已经很难了,他的难点在于接到的雨水是一个抽象的概念,我们要进行一个转化,才能进行正确的解答。

接的雨水两等于每个柱子能接雨水的总和。每个柱子能接雨水的量等于其左边最高柱子和右边最高柱子积累的量,也就是min(左边最大,右边最大)-当前柱子的高度。 如果当前柱子比左边或者右边柱子更大的元素,则该柱子不能接水。

上面的分析就是解题的关键,知道了每个元素两边最大元素的位置或者大小,那找积水量就是信手拈来了。但是和之前不同的是,我们找的不是下一个更大元素,而是找两边的最大元素。为什么呢?看下图,其他的不说,我们就分析柱子5能接多少水。如果按照找两边下一个更大元素的话,那他只能接1格水,而我们想要的结果是两个,所以要找两边最大元素才可以。
在这里插入图片描述
之前单调栈都是找下一个更大元素,找最大元素的题目好像没做过哎。其实,,这个很简单,设两个数组储存结果,一个for循环就简单实现了。

	vector<int> leftMax(n); // 存储每个位置左侧的最大高度
    vector<int> rightMax(n); // 存储每个位置右侧的最大高度
    // 计算左侧最大高度
    leftMax[0] = height[0];
    for (int i = 1; i < n; ++i) {
        leftMax[i] = max(leftMax[i - 1], height[i]);
    }

    // 计算右侧最大高度
    rightMax[n - 1] = height[n - 1];
    for (int i = n - 2; i >= 0; --i) {
        rightMax[i] = max(rightMax[i + 1], height[i]);
    }

然后有了这俩数组已经无敌了,直接求和得到答案。代码如下。

int trap(vector<int>& height) {
    int n = height.size();
    if (n == 0) return 0;

    vector<int> leftMax(n); // 存储每个位置左侧的最大高度
    vector<int> rightMax(n); // 存储每个位置右侧的最大高度

    // 计算左侧最大高度
    leftMax[0] = height[0];
    for (int i = 1; i < n; ++i) {
        leftMax[i] = max(leftMax[i - 1], height[i]);
    }

    // 计算右侧最大高度
    rightMax[n - 1] = height[n - 1];
    for (int i = n - 2; i >= 0; --i) {
        rightMax[i] = max(rightMax[i + 1], height[i]);
    }

    int water = 0;
    // 计算每个位置上的积水量
    for (int i = 0; i < n; ++i) {
        int minHeight = min(leftMax[i], rightMax[i]);
        water += max(minHeight - height[i], 0);
    }

    return water;
}

代码写完了,也过了。但是我有一个疑问,这尼玛好像和单调栈没一点关系啊!上面的解法还真没用到单调栈。主要原因还是找最大元素并不是单调栈适用的范围,普通方法就能很简单的找到。不过这可不行,咱这篇文章作为单调栈的总结,必须得用到单调栈。如何才能不求最大元素,而是求更大元素?
上面的代码是按照列求解得,我们只需要改变一下思考方式,按照行求解,那么就只需找下一个更大元素了!因为行只关注比自己更大的元素,只要是更大,就会出现凹槽,而且这个宽度不确定。如下图所示,按照行就可以使用单调栈了。

在这里插入图片描述
使用单调栈还有一个非常神奇的一点,一个栈就能找到当前元素左边和右边的下一个更大值,为何能这么奇妙呢?咱们分析一下。由于我们建立的是单调递增栈,也就是栈底元素大于栈顶元素。假设栈中存在两个元素,那么此时栈顶元素的左边更大元素不就是栈底的元素嘛!以此类推,反正栈顶下面一个元素就是左边更大。找右边更大和以往一样操作。我们需要注意的是,如果右边没有更大,那么找左边的也毫无意义,所以只有当右边存在更大,也就是while循环中,我们才要进行操作。同时如果栈中只有一个元素,那说明他左边没有最大。下面的代码就容易理解了。

int trap(vector<int>& height) {
	stack<int> st;
	int sum = 0;
	int n = height.size();
	for (int i = 0; i < n; i++)
	{
		while (!st.empty() && height[i] > height[st.top()]) {
			int mid = st.top();
			st.pop();
			if (!st.empty()) {
				int h = min(height[i], height[st.top()]) - height[mid];
				int w = i - st.top() - 1;
				sum += h * w;
			}
		}
		st.push(i);
	}
	return sum;
}

2.柱状图中最大的矩形

这道题和接雨水太像了,但是首先要看明白题意,学会进行转化。一个是求两边更大元素,一个求两边更小元素。但是需要注意的是:首尾都要加上0,这样的话,每个元素两边都有更小元素了。

int largestRectangleArea(vector<int>& heights) {

	stack<int>s;
	int sum = 0;
	//首尾都加上0的话,每个元素两边都有更小元素了

	heights.insert(heights.begin(), 0); // 数组头部加入元素0
	heights.push_back(0); // 数组尾部加入元素0
	int n = heights.size();
	for (int i = 0; i < n; i++)
	{
		while (!s.empty() && heights[i] < heights[s.top()]) {
			int mid = s.top();
			s.pop();
			sum = max(sum, (heights[mid] * (i - s.top() - 1)));
		}
		s.push(i);
	}
	return sum;
}

四、单调队列

单调栈的相关题目很多,也很常考。这队列也模仿起了单调栈,变成了单调队列,相对来说,单调队列的只适用于特定的场景。咱们就看着一道经典题目。

滑动窗口最大值

我们看着题目,很容易想到队列,因为他就很像一个滑动窗口。但是如果我们只按照滑动窗口遍历,而不进行其他处理,是没办法在移动的同时得到每个窗口的最大值的。

于是我们假设有这样一个队列,一直把最大值放到队首,这样我们每次只需要queue.front()就能得到每次的最大值了。此时的问题是,我们要求的是滑动窗口中的最大值,这个过程需要不断添加和删除元素,应该如何维护这个过程呢?

首先,为了保证队首能返回当前最大值,就需要满足队列中的元素是有序的,而且是从队首到队尾从大到小。我们的思路是,每次遍历到的元素比队尾元素大时,那么队尾元素要弹出;如果当前遍历的元素小于队尾元素时,那就进入队列。这样的话,我们就维护了一个单调递减的队列。

同时,我们要删除不在当前窗口范围内的元素的索引。也就是说,我们检查队列的前端下标是否位于当前窗口范围之外,如果是,则将其从队列中删除。删除过期元素。最后,我们只需要把每次的队首元素放入结果中即可。
这样的话,我们需要维护一个双端队列(deque),它将存储当前窗口内的元素的索引。队列的前端将始终保存当前窗口中的最大元素的索引。代码如下:

ector<int> maxSlidingWindow(std::vector<int>& nums, int k) {
	vector<int> result;
	deque<int> dq;

	for (int i = 0; i < nums.size(); ++i) {
		// 删除队列中不在当前窗口范围内的元素
		while (!dq.empty() && dq.front() < i - k + 1)
			dq.pop_front();

		// 删除队列中小于当前元素的元素,因为它们不可能是窗口最大值
		while (!dq.empty() && nums[dq.back()] < nums[i])
			dq.pop_back();

		dq.push_back(i);

		// 将窗口最大值加入结果集
		if (i >= k - 1)
			result.push_back(nums[dq.front()]);
	}

	return result;
}
  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值