1. 最小栈
思路:
一、整体架构
- 使用两个栈
st
和min_st
分别实现不同的功能。
st
用于存放插入的数据,即主要的数据存储栈,模拟常规的数据存储结构。min_st
用于存放最小的元素,通过特定的插入和弹出规则,始终保持栈顶为当前st
中的最小元素。二、插入操作(push)
- 对于
st
:
- 正常插入新元素,无论该元素的大小如何,都直接将其压入
st
。这保证了数据的完整存储,不进行任何筛选或特殊处理。- 对于
min_st
:
- 当
min_st
第一次为空时,先插入一个元素。这是初始化操作,确保min_st
有一个起始元素,以便后续进行比较和更新。- 如果插入的元素比
min_st
的栈顶元素小,就将该元素插入min_st
。这样做的目的是当有新的更小元素出现时,更新min_st
的栈顶,使其始终保持当前st
中的最小元素在栈顶。例如,如果当前min_st
的栈顶是 5,新插入的元素是 4,那么将 4 压入min_st
,此时min_st
的栈顶变为 4,代表当前最小元素为 4。三、弹出操作(pop)
- 对于
st
:
- 正常弹出栈顶元素。按照栈的先进后出原则,删除最后插入
st
的元素。- 对于
min_st
:
- 在弹出
st
的元素后,需要检查该元素是否与min_st
的栈顶元素相等。如果相等,说明当前要弹出的元素是最小元素,那么也需要将min_st
的栈顶元素弹出。这是为了保持两个栈的同步,确保min_st
始终反映st
中的最小元素。例如,如果st
和min_st
的栈顶都是 4,当从st
中弹出 4 时,也需要从min_st
中弹出 4,以保证min_st
的栈顶仍然是st
中剩余元素的最小元素。
class MinStack {
public:
stack<int> st;
stack<int> min_st;
MinStack()
{
}
void push(int val)
{
st.push(val);
if(min_st.empty() || val <= min_st.top())
{
min_st.push(val);
}
}
void pop()
{
if(st.top() == min_st.top())
{
min_st.pop();
}
st.pop();
}
int top()
{
return st.top();
}
int getMin()
{
return min_st.top();
}
};
2. 栈的弹出压入序列
思路:
首先遍历压入顺序的序列。在遍历过程中,将压入顺序中的元素依次压入辅助栈,同时不断检查辅助栈的栈顶元素是否与弹出顺序的当前元素相等。如果相等,就将辅助栈的栈顶元素弹出,并移动弹出顺序的索引。继续这个过程,直到压入顺序的所有元素都被处理完。最终,如果辅助栈为空,说明给定的压入顺序和弹出顺序是合法的栈的操作顺序;如果辅助栈不为空,则说明不是合法的弹出顺序。
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
int pushi = 0;
int popi = 0;
stack<int> st;
while (pushi < pushV.size())
{
st.push(pushV[pushi]);
++pushi;
while (!st.empty() && st.top() == popV[popi])
{
st.pop();
++popi;
}
}
return st.empty();
}
};
3. 逆波兰表达式求值
思路:
一、定义一个栈
- 使用一个栈来存储操作数。逆波兰表达式(后缀表达式)的特点是不需要考虑运算符的优先级,按照从左到右的顺序进行计算。栈这种数据结构非常适合这种计算方式,因为它可以方便地进行后进先出的操作。
二、遍历表达式
- 逐个字符地遍历逆波兰表达式。
- 如果遇到数字字符,将其转换为数字并压入栈中。这是因为数字是操作数,在计算过程中需要先存储起来,等待运算符出现时进行计算。
- 如果遇到运算符,说明需要进行计算。从栈中弹出两个操作数,注意弹出的顺序是先弹出的作为运算符的右操作数,后弹出的作为运算符的左操作数。这是因为栈是后进先出的数据结构,与通常的数学运算顺序相反。
三、进行计算
- 根据遇到的运算符进行相应的计算。
- 例如,如果遇到加号(+),则将弹出的两个操作数相加;如果遇到减号(-),则进行减法运算;遇到乘号(*)进行乘法运算;遇到除号(/)进行除法运算。
- 注意在进行除法运算时,需要考虑除数不能为零的情况,以避免出现错误。
四、结果处理
- 将计算得到的结果压入栈中。
这样,栈中始终存储着尚未处理的操作数和中间结果。继续遍历表达式,重复上述步 骤,直到表达式遍历完毕。
五、最终结果
- 当表达式遍历结束后,栈中应该只剩下一个元素,这个元素就是逆波兰表达式的求值结果。
例如,对于逆波兰表达式 “3 4 + 5 *”,具体的计算过程如下:
- 首先遇到数字 3,压入栈中。此时栈为 [3]。
- 遇到数字 4,压入栈中。此时栈为 [3, 4]。
- 遇到加号(+),从栈中弹出两个数字 3 和 4,进行加法运算,得到结果 7。将 7 压入栈中。此时栈为 [7]。
- 遇到数字 5,压入栈中。此时栈为 [7, 5]。
- 遇到乘号(*),从栈中弹出两个数字 7 和 5,进行乘法运算,得到结果 35。将 35 压入栈中。此时栈为 [35]。
表达式遍历完毕,栈中只剩下一个元素 35,这就是逆波兰表达式的求值结果。
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
for(auto& str : tokens)
{
if(str == "+" || str == "-" || str == "*" || str == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
st.push(stoi(str)); // stoi 把字符类型转整型
}
}
return st.top();
}
};
4. 用栈实现队列
思路:
一、使用两个栈
- 使用两个栈,一个用于入队操作(命名为 _pushST),另一个用于出队操作(命名为 _popST)。
二、入队操作(push)
- 当进行入队操作时,将元素直接压入 _pushST。
- 由于栈是后进先出的数据结构,入队操作相当于在栈顶添加元素,这是很自然的操作。
三、出队操作(pop)
- 首先检查 _popST 是否为空。
- 如果 _popST 不为空,说明之前已经有一些元素从 _pushST 转移到了 _popST,此时可以直接从 _popST 的栈顶弹出元素,这就实现了队列的先进先出特性。
- 如果 _popST 为空,将 _pushST 中的所有元素依次弹出并压入 _popST。这样就将 _pushST 中后进先出的顺序反转成了先进先出的顺序,然后再从 _popST 的栈顶弹出元素。
四、查看队首元素(peek)
- 与出队操作类似,先检查 _popST 是否为空。
- 如果 _popST 不为空,直接返回 _popST 的栈顶元素,这就是队首元素。
- 如果 _popST 为空,将 _pushST 中的所有元素依次弹出并压入 _popST,然后返回 _popST 的栈顶元素。
五、判断队列是否为空(empty)
- 当 _pushST 和 _popST 都为空时,队列被认为是空的。
- 因为只有这两个栈都没有元素时,才能确定整个队列中没有任何元素。
class MyQueue
{
private:
stack<int> _PushST;
stack<int> _PopST;
public:
MyQueue() {}
void push(int x)
{
_PushST.push(x);
}
int pop()
{
int front = peek(); //返回队列的 前一个位置
_PopST.pop();
return front;
}
int peek()
{
if (!_PopST.empty())
{
return _PopST.top();
}
else
{
while (!_PushST.empty())
{
_PopST.push(_PushST.top());
_PushST.pop();
}
return _PopST.top();
}
}
bool empty()
{
return _PushST.empty() && _PopST.empty();
}
};
5. 用队列实现栈
一、利用两个队列
- 有两个队列,分别命名为
q1
和q2
。二、入栈操作(push)
- 检查哪个队列非空。
- 如果
q1
非空,就将新元素加入q1
。- 如果
q2
非空,就将新元素加入q2
。- 如果两个队列都为空,可以任意选择一个队列加入新元素。
三、出栈操作(pop)
- 确定哪个队列为非空,哪个队列为空。假设
q1
非空,q2
为空。- 将非空队列(这里是
q1
)中的元素逐个出队并加入空队列(q2
),除了最后一个元素。这样做是为了将除了最后进入的元素(即栈顶元素)之外的所有元素转移到另一个队列中。- 最后,将非空队列中剩下的那个元素出队并返回,这个元素就是栈顶元素。
四、查看栈顶元素(top)
- 与出栈操作类似,确定非空队列和空队列。
- 将非空队列中的元素逐个出队并加入空队列,除了最后一个元素。
- 记录这个最后一个元素的值,但不要出队。
- 再将这些元素逐个从空队列出队并加入原来的非空队列,恢复队列的状态。
- 返回之前记录的元素值,即栈顶元素。
- 但是这里可以直接使用 back。
class MyStack {
public:
queue<int> q1;
queue<int> q2;
MyStack() {}
void push(int x)
{
if (!q1.empty())
q1.push(x);
else
q2.push(x);
}
int pop() {
queue<int>* empty = &q1;
queue<int>* noempty = &q2;
if (!q1.empty()) {
noempty = &q1;
empty = &q2;
}
while (noempty->size() > 1)
{
empty->push(noempty->front());
noempty->pop();
}
int top = noempty->front();
noempty->pop();
return top;
}
int top()
{
if (!q1.empty())
return q1.back();
else
return q2.back();
}
bool empty()
{
return q1.empty() && q2.empty();
}
};
6. 数组中第K个大的元素
方法一:优先级队列求解
把数组的数据入队,然后pop前 k-1 个数据,栈顶就是第 K 大的数。
时间复杂度:堆排序的时间复杂度是 N*logN,取最坏,时间复杂度是 N*logN。
空间复杂:构建了堆,空间复杂度是O(N)。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> pq;
for(auto& e : nums)
{
pq.push(e);
}
while(--k)
{
pq.pop();
}
return pq.top();
}
};
方法二:排序
排序的底层是快排,快排使用三数取中法,避免了最坏的情况。
时间复杂度是 O(N*logN)
空间复杂度是 O(1)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
sort(nums.begin(), nums.end());
return nums[nums.size() - k];
}
};
方法三:topK问题。
当要求前第 K 个最大的数的时候,建立小堆,当要求前第 K 个最小的数的时候,建立大堆,这是二叉树部分的堆的基础。 我们构建一个小堆,小堆是 K个数据,时间复杂度是 K+logK,即 logK, 第一个for循环时间复杂度是 K*logK,第二个for循环时间复杂度是 N*logK,需要调整 K 个数据的大小 。
所以时间复杂度: O(K*logK) 空间复杂度:O(K)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int, vector<int>, greater<int>> minHeap;
// 1,建立 K个数的小堆
size_t i = 0;
for (; i < k; ++i)
{
minHeap.push(nums[i]);
}
for (; i < nums.size(); ++i)
{
if (nums[i] > minHeap.top())
{
minHeap.pop();
minHeap.push(nums[i]);
}
}
return minHeap.top();
}
};
7. 有效的括号
思路:
一、整体方法
这个思路是通过使用栈来判断给定字符串中的括号是否匹配正确。对于包含不同类型括号的字符串,如小括号
()
、中括号[]
、大括号{}
等,该方法可以有效地检查它们的匹配情况。二、三 种情况分析
情况①():
- 当遇到左括号
(
时,将其入栈。- 后续如果遇到右括号
)
,此时栈顶元素应为(
,如果匹配成功则弹出栈顶元素。如果整个字符串遍历完后栈为空,则说明括号完全匹配。情况②( [ { } ] ):
- 同样,遇到左括号时入栈。例如遇到
[
,将其入栈。- 当遇到右括号
]
时,检查栈顶元素是否为对应的左括号[
。如果是,则弹出栈顶元素;如果不是,则说明括号不匹配,直接返回false
。情况③( )[ ] { }:
- 对于这种混合括号的情况,也是遵循相同的逻辑。遇到左括号入栈,遇到右括号时与栈顶元素进行匹配。如果匹配成功则弹出栈顶元素,否则说明括号不匹配。
三、栈的作用
- 记录左括号:栈用于存储遍历过程中遇到的左括号。这样,当遇到右括号时,可以方便地与栈顶的左括号进行匹配。
- 确保顺序:通过入栈和出栈的操作,可以确保括号的匹配是按照正确的顺序进行的。只有当右括号与最近的未匹配的左括号相匹配时,才是正确的情况。
class Solution {
public:
stack<char> st;
bool isValid(string s)
{
for(int i = 0; i < s.size(); ++i)
{
if(s[i] == '(' || s[i] == '{' || s[i] == '[')
{
st.push(s[i]);
}
else
{
if(st.empty())
{
return false;
}
char top = st.top();
if(s[i] == ')' && top != '(')
{
return false;
}
if(s[i] == ']' && top != '[')
{
return false;
}
if(s[i] == '}' && top != '{')
{
return false;
}
st.pop();
}
}
return st.empty();
}
};
8. 字符串解码
9. 每日温度
思路:
① 一种很简单的做法就是暴力求解,两层for循环,一层遍历数组,一层找到比当前元素更大的元素在哪里,两个下标相减,求到对应的值,时间复杂度是 O(n²)。不太行。
②利用单调栈求解,时间复杂度是 O(n)。
单调栈:可以完成找到左边或者右边比它大或者比它小的元素。
什么是单调栈:
- 单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
- 单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大
那么这里求元素之间的距离,如果存放元素到栈里面,还要去数组中找元素对应的下标是多少,而且数组元素还会存在重复的,无法确定唯一下标,所以单调栈直接存放元素的下标,然后进行比较大小(数组通过下标映射元素进行比较)。
单调栈的作用?
存放遍历过的元素,我们怎么知道这个元素是不是第一个比遍历过的元素大的元素呢?我们需要一个数据结构记录我们之前遍历过的元素。
例如:
[73,74,75,71,69,69,72,76,73]。
76是第一个比75大的元素,然后用下标相减求差值,计算的 5。
综上所述:单调栈的作用是记录一下我们遍历过的元素,然后和当前遍历的元素做一个对比。
求解过程:
当前遍历元素和栈顶元素作比较,有三种情况,要么比当前元素大,要么小,要么等于。
根据这些情况,我们就可以来模拟单调栈的过程。
初始化:
- 创建一个空栈
st
。- 创建结果数组
result
,初始值全为 0,长度与输入数组相同。- 将索引 0(对应元素 73)入栈,即
st.push(0)
。处理元素 74:
i = 1
,当前元素为 74。- 74 大于栈顶元素 73(
T[i] > T[st.top()]
)。- 执行
while
循环:
result[st.top()] = i - st.top()
,即result[0] = 1 - 0 = 1
,表示第一个元素 73 升温天数为 1。st.pop()
,弹出栈顶元素。- 将索引 1(对应元素 74)入栈,即
st.push(1)
。处理元素 75:
i = 2
,当前元素为 75。- 75 大于栈顶元素 74(
T[i] > T[st.top()]
)。- 执行
while
循环:
result[st.top()] = i - st.top()
,即result[1] = 2 - 1 = 1
,表示第二个元素 74 升温天数为 1。st.pop()
,弹出栈顶元素。- 将索引 2(对应元素 75)入栈,即
st.push(2)
。处理元素 71:
i = 3
,当前元素为 71。- 71 小于栈顶元素 75(
T[i] <= T[st.top()]
)。- 将索引 3(对应元素 71)入栈,即
st.push(3)
。处理元素 69:
i = 4
,当前元素为 69。- 69 小于栈顶元素 71(
T[i] <= T[st.top()]
)。- 将索引 4(对应元素 69)入栈,即
st.push(4)
。处理元素 69(重复的 69):
i = 5
,当前元素为 69。- 69 小于栈顶元素 69(栈顶元素未变,还是对应索引 4),即
T[i] <= T[st.top()]
。- 将索引 5(对应元素 69)入栈,即
st.push(5)
。处理元素 72:
i = 6
,当前元素为 72。- 72 大于栈顶元素 69(
T[i] > T[st.top()]
)。- 执行
while
循环:
result[st.top()] = i - st.top()
,即result[5] = 6 - 5 = 1
,表示第六个元素 69 升温天数为 1。st.pop()
,弹出栈顶元素。- 栈顶元素变为对应索引 4 的 69,72 仍大于栈顶元素。
result[st.top()] = i - st.top()
,即result[4] = 6 - 4 = 2
,表示第五个元素 69 升温天数为 2。st.pop()
,弹出栈顶元素。- 栈顶元素变为对应索引 3 的 71,72 仍大于栈顶元素。
result[st.top()] = i - st.top()
,即result[3] = 6 - 3 = 3
,表示第四个元素 71 升温天数为 3。st.pop()
,弹出栈顶元素。- 将索引 6(对应元素 72)入栈,即
st.push(6)
。处理元素 76:
i = 7
,当前元素为 76。- 76 大于栈顶元素 72(
T[i] > T[st.top()]
)。- 执行
while
循环:
result[st.top()] = i - st.top()
,即result[6] = 7 - 6 = 1
,表示第七个元素 72 升温天数为 1。st.pop()
,弹出栈顶元素。- 栈顶元素变为对应索引 2 的 75,76 仍大于栈顶元素。
result[st.top()] = i - st.top()
,即result[2] = 7 - 2 = 5
,表示第三个个元素 75 升温天数为 5。st.pop()
,弹出栈顶元素。- 将索引 7(对应元素 76)入栈,即
st.push(7)
。处理元素 73:
i = 8
,当前元素为 73。- 73 小于栈顶元素 76(
T[i] <= T[st.top()]
)。- 将索引 8(对应元素 73)入栈,即
st.push(8)
。最终,结果数组
result
为[1, 1, 5, 3, 2, 1, 1, 0, 0]
,表示每个元素距离下一个更高温度的天数。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T)
{
stack<int> st;
vector<int> result(T.size(), 0);
st.push(0);
for(int i = 1; i < T.size(); ++i)
{
if(T[i] > T[st.top()])
{
while(!st.empty() && T[i] > T[st.top()])
{
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
else
{
st.push(i);
}
}
return result;
}
};