栈(Stack)是一种操作受限的线性表数据结构,它遵循后进先出(LIFO)的原则。想象一下自助餐厅的餐盘堆叠方式——最后放上去的餐盘总是最先被取走,这就是栈的典型应用场景。
栈的核心操作只有两个:
与数组和链表不同,栈不提供遍历功能,也没有迭代器(iterator)。这意味着你不能像使用set或map那样直接访问栈中间的元素。这种设计看似限制了功能,实则保证了数据操作的严格顺序性。
在C++的STL实现中,默认使用deque(双端队列)作为栈的底层容器。选择deque是因为它在头部和尾部插入/删除操作的时间复杂度都是O(1),能很好地满足栈的操作需求。
注意:虽然STL栈默认使用deque实现,但也可以通过模板参数指定其他容器,如vector或list。例如:
stack<int, vector<int>> myStack;
栈在计算机科学中应用广泛,主要包括:
给定一个只包含 '(', ')', '{', '}', '[' 和 ']' 的字符串,判断字符串是否有效。有效字符串需满足:
栈特别适合解决这类对称匹配问题,因为它的后进先出特性正好可以处理嵌套关系。我们可以利用栈来保存遇到的左括号,当遇到右括号时检查栈顶元素是否匹配。
cpp复制class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 快速失败:奇数长度必定无效
stack<char> st;
for (char c : s) {
if (c == '(') st.push(')');
else if (c == '{') st.push('}');
else if (c == '[') st.push(']');
else if (st.empty() || st.top() != c) return false;
else st.pop();
}
return st.empty();
}
};
关键优化点:
实际编码中常见错误是忘记检查栈空状态就直接调用top(),这会导致运行时错误。正确的顺序应该是
if(st.empty() || st.top() != c)。
设计一个支持push、pop、top操作,并能在常数时间内检索到最小元素的栈。关键在于如何在O(1)时间内获取当前栈中的最小值,而不是通过遍历查找。
使用两个栈:
cpp复制class MinStack {
stack<int> mainStack;
stack<int> minStack;
public:
MinStack() {
minStack.push(INT_MAX); // 哨兵值
}
void push(int val) {
mainStack.push(val);
minStack.push(min(minStack.top(), val));
}
void pop() {
mainStack.pop();
minStack.pop();
}
int top() {
return mainStack.top();
}
int getMin() {
return minStack.top();
}
};
设计要点:
时间复杂度:
空间复杂度:
替代方案:差值存储法
可以只使用一个栈,存储元素与当前最小值的差值。这种方法能减少空间使用,但实现更复杂,且可能面临整数溢出问题。
给定一个编码字符串如"3[a2[c]]",需要解码为"accaccacc"。主要挑战在于处理嵌套结构和多位数字。
使用两个栈:
cpp复制class Solution {
public:
string decodeString(string s) {
stack<int> countStack;
stack<string> stringStack;
string currentString;
int currentNumber = 0;
for (char c : s) {
if (isdigit(c)) {
currentNumber = currentNumber * 10 + (c - '0');
} else if (c == '[') {
countStack.push(currentNumber);
stringStack.push(currentString);
currentNumber = 0;
currentString.clear();
} else if (c == ']') {
string decodedString = stringStack.top();
stringStack.pop();
int repeatTimes = countStack.top();
countStack.pop();
for (int i = 0; i < repeatTimes; i++) {
decodedString += currentString;
}
currentString = decodedString;
} else {
currentString += c;
}
}
return currentString;
}
};
处理流程:
这个问题也可以使用递归解决,每次遇到'['就进入新的一层递归。递归解法代码更简洁,但栈深度受限于嵌套层数,可能引发栈溢出。迭代的栈解法在空间使用上更可控。
给定每日温度列表,要求返回一个列表,表示对于每一天需要等待多少天才能遇到更高的温度。暴力解法是对每个元素向后遍历,时间复杂度O(n^2)。
使用单调递减栈存储日期索引,当遇到更高温度时计算等待天数。
cpp复制class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
vector<int> result(T.size(), 0);
stack<int> st;
for (int i = 0; i < T.size(); i++) {
while (!st.empty() && T[i] > T[st.top()]) {
int prev = st.top();
st.pop();
result[prev] = i - prev;
}
st.push(i);
}
return result;
}
};
关键点:
单调栈适用于求解"下一个更大/更小元素"类问题。判断使用递增还是递减栈的简单记忆法:
时间复杂度从暴力解法的O(n^2)降低到O(n),因为每个元素最多入栈出栈各一次。
给定n个非负整数表示柱状图高度,求能勾勒出的最大矩形面积。暴力解法是对于每个柱子,向左右扩展直到遇到更矮的柱子,时间复杂度O(n^2)。
使用单调递增栈,在保证栈内高度递增的同时计算可能的矩形面积。
cpp复制class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.insert(heights.begin(), 0);
heights.push_back(0);
stack<int> st;
st.push(0);
int result = 0;
for (int i = 1; i < heights.size(); i++) {
while (heights[i] < heights[st.top()]) {
int h = heights[st.top()];
st.pop();
int w = i - st.top() - 1;
result = max(result, h * w);
}
st.push(i);
}
return result;
}
};
关键技巧:
时间复杂度: O(n),每个柱子最多入栈出栈各一次
空间复杂度: O(n),最坏情况下需要存储所有柱子
类似问题包括:
遇到以下特征的问题可考虑使用栈:
单调栈使用要点:
调试时可以:
掌握栈的关键在于理解其"后进先出"的本质特性,并能在实际问题中识别出适合栈处理的模式。通过大量练习培养对栈应用的直觉,是提高算法问题解决能力的有效途径。