栈的奇妙世界:从冰棒到算法的华丽转身
「后进先出」这四个字,听起来像是在描述地铁早高峰的拥挤场景,但在编程世界里,它却是栈这个数据结构的精髓所在。今天我们就来聊聊这个看似简单却威力无穷的数据结构------栈。
什么是栈?一个冰棒的故事
想象一下,你有一个装冰棒的盒子:
arduino
let stack = []; // 主观上说它是一个栈,它就是一个栈。
stack.push('小布丁')
stack.push('老冰棒')
stack.push('东北大板')
stack.push('巧乐兹')
stack.push('可爱多')
当你想吃冰棒的时候,你只能从最上面开始拿,这就是**后进先出(LIFO - Last In First Out)**的原理:
arduino
// 栈的出栈
while(stack.length > 0) {
console.log(`我爱吃${stack.pop()}`);
}
// 输出:
// 我爱吃可爱多
// 我爱吃巧乐兹
// 我爱吃东北大板
// 我爱吃老冰棒
// 我爱吃小布丁
栈 vs 数组:阉割版的艺术
栈本质上就是一个「阉割版的数组」,它只保留了两个核心操作:
- push() : 入栈(把元素放到栈顶)
- pop() : 出栈(从栈顶取出元素)
为什么要「阉割」?因为约束产生美。通过限制操作,我们获得了一个行为可预测、逻辑清晰的数据结构。
实战一:有效的括号 - 栈的经典应用
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
- 每个右括号都有一个对应的相同类型的左括号
解题思路
这就像是在检查你的代码括号是否匹配一样。我们的策略是:
- 遇到左括号:「先别急,我记住你了」→ 入栈
- 遇到右括号:「让我看看你和谁配对」→ 出栈并检查
arduino
var isValid = function(s) {
if (s.length % 2 === 1)
return false; // 奇数长度直接false
let stack = [];
const map = {
'(': ')',
'[': ']',
'{': '}'
}
for (let char of s) {
if (map[char]) {
// 遇到左括号,入栈
stack.push(char);
} else {
// 遇到右括号,检查是否匹配
if (map[stack.pop()] !== char) {
return false;
}
}
}
// 栈空了说明全部匹配
return stack.length === 0;
};
收起代码
为什么用栈?
因为括号匹配天然符合「后进先出」的特性:
- "([{}])" ✅ - 最后进入的 { 最先被 } 匹配
- "([)]" ❌ - ( 和 ) 被 [ 和 ] 隔开了
实战二:每日温度 - 单调栈的威力
题目描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。
ini
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
暴力解法:双重循环的无奈
ini
var dailyTemperatures =
function(temperatures) {
for(let i = 0; i < temperatures.length; i++) {
for(let j = i + 1; j < temperatures.length; j++){
if(temperatures[j] > temperatures[i]){
temperatures[i] = j - i
break
}
if(j === temperatures.length - 1){
temperatures[i] = 0
}
}
}
}
时间复杂度:O(n²) 😱
这就像是每天都要从明天开始一直往后找,直到找到更热的天气。效率低下,面试官会皱眉头。
单调栈解法:优雅的O(n)
ini
var dailyTemperatures = function(temperatures) {
const n = temperatures.length;
const answer = new Array(n).fill(0);
const stack = []; // 存储温度的索引
for (let i = 0; i < n; i++) {
const currentTemp = temperatures[i];
// 当栈不为空,并且当前温度比栈顶索引对应的温度高时
while (stack.length > 0 && currentTemp > temperatures[stack[stack.length - 1]]){
// 弹出栈顶索引,因为它找到了它的"下一个更高温度"
const prevIndex = stack.pop();
// 计算天数差,并记录到结果数组中
answer[prevIndex] = i - prevIndex;
}
// 将当前温度的索引压入栈中
stack.push(i);
}
return answer;
};
收起代码
单调栈的精髓
单调栈就像是一个「等待队列」:
- 栈中存储的是索引,不是温度值
- 栈中索引对应的温度是单调递减的
- 当遇到更高温度时,栈中所有比它低的温度都找到了答案
让我们用 [73,74,75,71,69,72,76,73] 来演示:
ini
i=0: stack=[0], temp=73
i=1: temp=74>73, 栈顶0出栈,
answer[0]=1-0=1, stack=[1]
i=2: temp=75>74, 栈顶1出栈,
answer[1]=2-1=1, stack=[2]
i=3: temp=71<75, stack=[2,3]
i=4: temp=69<71, stack=[2,3,
4]
i=5: temp=72>69>71, 栈顶4,3出
栈, answer[4]=1, answer[3]
=2, stack=[2,5]
i=6: temp=76>72>75, 栈顶5,2出
栈, answer[5]=1, answer[2]
=4, stack=[6]
i=7: temp=73<76, stack=[6,7]
时间复杂度:O(n) ✨
每个元素最多入栈一次,出栈一次,所以总的时间复杂度是 O(n)。
栈的应用场景总结
1. 括号匹配类问题
- 有效的括号
- 最长有效括号
- 删除无效的括号
2. 单调栈问题
- 每日温度
- 柱状图中最大的矩形
- 下一个更大元素
3. 表达式求值
- 逆波兰表达式求值
- 基本计算器
4. 深度优先搜索(DFS)
- 二叉树的遍历
- 图的遍历
栈的哲学思考
栈教会我们一个道理:有时候,限制就是力量。
- 数组给了我们太多自由,反而让我们迷失方向
- 栈通过约束操作,让我们专注于解决特定类型的问题
- 「后进先出」这个简单的规则,却能解决复杂的算法问题
就像生活中的很多事情一样,专注比全能更有价值。
写在最后
栈虽然简单,但它的应用却无处不在。从函数调用栈到浏览器的前进后退,从编译器的语法分析到算法题的优雅解法,栈都在默默地发挥着作用。
下次当你看到「寻找下一个更大元素」、「括号匹配」这类问题时,不妨想想:这里是不是可以用栈来解决?
记住,栈不仅仅是一个数据结构,更是一种思维方式。
「代码如诗,栈如人生。有进有出,方得始终。」
相关题目推荐
- LeetCode 20. 有效的括号(入门)
- LeetCode 739. 每日温度(进阶)
- LeetCode 84. 柱状图中最大的矩形(困难)
- LeetCode 496. 下一个更大元素 I(中等)
如果这篇文章对你有帮助,别忘了点赞收藏!有问题欢迎在评论区讨论~ 🚀