栈的经典应用:有效括号与最小栈
栈作为一种 “先进后出”(LIFO)的数据结构,在处理具有 “匹配关系” 或 “最值追踪” 需求的问题时展现出极高的效率。本文将聚焦两道 LeetCode 高频题目 ——“有效括号” 和 “最小栈”,通过拆解解题思路、剖析代码细节,带你掌握栈在实际场景中的核心用法。
155. 最小栈
解题思路
为什么不能每次遍历找最小值?
若在 getMin() 中遍历整个栈,时间复杂度为 O (n),不满足题目要求。因此,必须在插入和删除时同步维护最小值信息。
基础实现(O (n) 时间复杂度 getMin)
// es5 构造函数
// 函数表达式
const MiniStack = function() {
this.stack = []; //数组
}
MiniStack.prototype.push = function(x) {
this.stack.push(x);
}
MiniStack.prototype.pop = function(){
return this.stack.pop()
}
MiniStack.prototype.top = function() {
if(!this.stack || this.stack.length === 0) {
return undefined;
}
return this.stack[this.stack.length-1];
}
// O(n)
MiniStack.prototype.getMin = function (){
// 遍历一遍
// Infinity 无穷大
let minValue = Infinity; //无穷大
const { stack } = this;
for(let i = 0; i<stack.length; i++) {
if(stack[i] < minValue) {
minValue = stack[i];
}
}
return minValue;
}
优化实现:辅助栈(O (1) 时间复杂度 getMin)
核心策略:辅助栈
引入一个辅助栈 minStack,用于记录 “到当前为止” 的最小值。其维护规则如下:
- 入栈时:如果新值
val小于或等于minStack的栈顶(即当前最小值),则也将val压入minStack; - 出栈时:如果主栈弹出的值等于
minStack的栈顶,则同步弹出minStack的栈顶; - 查询最小值:直接返回
minStack的栈顶。
注意:使用 小于等于(≤) 而非小于(<),是为了正确处理重复的最小值。例如连续压入两个
-2,若只记录一次,第一次pop后最小值就会丢失。
优化代码
var MinStack = function() {
this.stack = [];
this.stack2 = [];
};
/**
* @param {number} val
* @return {void}
*/
MinStack.prototype.push = function(val) {
this.stack.push(val);
if(this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= val){
this.stack2.push(val);
}
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
const pop = this.stack.pop();
if(this.stack2[this.stack2.length - 1] === pop){
this.stack2.pop();
}
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
};
/**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.stack2[this.stack2.length - 1];
};
/**
* Your MinStack object will be instantiated and called as such:
* var obj = new MinStack()
* obj.push(val)
* obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.getMin()
*/
20. 有效的括号
解题代码
const leftToRight = {
"(" : ")",
"{" : "}",
"[" : "]"
};
const isValid = function(s){
if(!s) return true;
const stack = [];
const len = s.length;
if(len % 2 != 0){
return false;
}
for(let i = 0; i < len; i++){
const ch = s[i];
if (ch ==="(" || ch === "{" || ch ==="["){
stack.push(leftToRight[ch]); // 关键技巧:存入预期的右括号
} else {
if(!stack.length || stack.pop() !== ch){
return false;
}
}
}
return !stack.length;
}
内存中的括号追踪器
让我们走进代码执行的微观世界,看每个字符如何在内存中触发一场精准的 “括号追捕行动”。
const leftToRight = { '(': ')', '{': '}', '[': ']' };
→ 先建立一张 “通缉画像表”:每个左括号的 “同伙” 是谁,一目了然。
if (!s) return true;
→ 空字符串?直接返回 true!没有括号,自然无需匹配,视为有效。
if (len % 2 !== 0) return false;
→ 字符数为奇数?直接判定无效!括号必须成对出现,单数肯定配不齐。
const stack = []; // 创建一个“待验证嫌疑人名单”
现在,开始逐字扫描字符串,指针如探照灯扫过每一个字符:
-
遇到左括号(
(、{、[):- 不存它自己,而是把它的 “通缉目标”(对应右括号)推入
stack栈顶。 - 比如看到
[,立刻push(']')—— 记住:我们等的是],不是[!
- 不存它自己,而是把它的 “通缉目标”(对应右括号)推入
-
遇到右括号(
)、}、]):-
第一问:
stack是否为空?若空,说明没人预告你要来 → 非法入侵! -
第二问:
stack.pop()弹出的期待值是否等于当前字符?- 相等?✅ 匹配成功,继续;
- 不等?❌ 身份不符,立即终止!
-
循环结束后:
return !stack.length;
→ 最终审判:如果 “嫌疑人名单” 清空,说明所有期待都被满足;若还有残留,说明有左括号 “悬案未破”。
💡 关键洞察:栈中存储的不是历史,而是未来预期。这是本算法高效的核心!
从 “匹配” 到 “追踪”:栈的另一核心用法 —— 最小栈
如果说 “有效括号” 利用栈的 “后进先出” 特性解决了 “成对匹配” 问题,那么 “最小栈” 则需要在此基础上,额外实现 “快速查询最小值” 的功能,这就需要我们对栈的结构进行优化设计。
总结
通过两道经典题目,我们见证了栈的核心应用场景:
- 匹配问题(有效括号):利用栈 “后进先出” 的特性,存储 “未来预期的匹配项”,实现高效的成对校验,时间复杂度 O (n)、空间复杂度 O (n);
- 最值追踪问题(最小栈):通过 “主栈 + 辅助栈” 的设计,在不影响栈基本操作的前提下,将最小值查询优化至 O (1) 时间复杂度,核心是 “同步维护最值信息”。
栈的本质是对 “顺序依赖” 问题的高效解决 —— 无论是括号的 “先开后闭”,还是最小值的 “动态更新”,都可以通过栈的结构特性简化逻辑。掌握这两道题的解题思路,你将能快速应对各类栈相关的算法题目,理解 “用数据结构优化时间复杂度” 的核心思想。