一、解题思维总结
1. 何时使用栈结构?
| 场景 | 解法 | 复杂度 |
|---|---|---|
| 括号匹配问题 | 使用栈存储左括号,遇到右括号时检查栈顶是否匹配 | O(n) |
| 需要追踪历史最小值 | 使用辅助栈存储当前最小值 | O(1) 获取最小值 |
| 后缀表达式求值 | 遇到数字入栈,遇到运算符弹出栈顶两个元素计算 | O(n) |
| 生成有效括号组合 | 回溯算法,维护左右括号计数 | O(4^n/√n) |
| 寻找下一个更大元素 | 单调栈,存储未找到更大元素的索引 | O(n) |
| 车队问题 | 按位置排序,计算到达时间,使用栈追踪车队 | O(n log n) |
2. 复杂度分析
- 栈操作:push/pop/top 操作均为 O(1)
- 空间复杂度:通常为 O(n),最坏情况下可能需要存储所有元素
- 回溯算法:时间复杂度通常较高,但能保证找到所有解
3. 常用技巧
- 括号映射:使用对象存储左右括号对应关系
- 辅助栈:用于追踪最小值、最大值等特殊需求
- 单调栈:保持栈内元素单调性,用于解决"下一个更大元素"类问题
- 回溯模板:递归 + 条件判断,注意字符串拼接不要修改原字符串
二、核心技巧详解
技巧一:括号匹配与栈结构
适用场景:字符串中的括号匹配、表达式有效性检查
核心要点:
- 左括号入栈,右括号检查栈顶是否匹配
- 使用映射对象提高代码可读性和可扩展性
- 遍历结束后栈必须为空才有效
典型例题:有效的括号
var isValid = function(s) {
const bracketMap = {
')': '(',
'}': '{',
']': '[',
};
const stack = [];
for (let v of s) {
if (Object.values(bracketMap).includes(v)) {
stack.push(v);
continue;
}
if (v in bracketMap) {
if (stack.length > 0 && stack.pop() === bracketMap[v]) {
continue;
}
}
return false;
}
return stack.length === 0;
};
技巧二:辅助栈追踪极值
适用场景:需要在常数时间内获取栈中最小值/最大值
核心要点:
- 主栈存储所有元素,辅助栈只存储小于等于栈顶的元素
- pop操作时需要检查是否弹出的是当前最小值
- 时间复杂度:所有操作均为 O(1)
典型例题:最小栈
var MinStack = function() {
this.stack = [];
this.minStack = [];
};
MinStack.prototype.push = function(val) {
this.stack.push(val);
if (this.minStack.length === 0 || val <= this.minStack[this.minStack.length - 1]) {
this.minStack.push(val);
}
};
MinStack.prototype.pop = function() {
const val = this.stack.pop();
if (val === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
};
技巧三:后缀表达式求值
适用场景:计算逆波兰表达式(后缀表达式)
核心要点:
- 数字直接入栈,运算符弹出栈顶两个元素计算
- 除法使用
Math.trunc()实现向零截断 - 注意操作数顺序:先弹出的是右操作数
典型例题:逆波兰表达式求值
function evalRPN(tokens) {
let stack = [];
for (let token of tokens) {
if (!isNaN(token)) {
stack.push(parseInt(token));
} else {
let num2 = stack.pop();
let num1 = stack.pop();
switch (token) {
case '+': stack.push(num1 + num2); break;
case '-': stack.push(num1 - num2); break;
case '*': stack.push(num1 * num2); break;
case '/': stack.push(Math.trunc(num1 / num2)); break;
}
}
}
return stack.pop();
}
技巧四:回溯算法生成括号
适用场景:生成所有可能的有效组合
核心要点:
- 任何时候右括号数量不能超过左括号
- 使用递归回溯,尝试所有可能路径
- 注意字符串拼接不要修改原字符串
典型例题:括号生成
var generateParenthesis = function(n) {
let result = [];
function backtrack(s = '', left = 0, right = 0) {
if (s.length === n * 2) {
result.push(s);
return;
}
if (left < n) {
backtrack(s + '(', left + 1, right);
}
if (right < left) {
backtrack(s + ')', left, right + 1);
}
}
backtrack();
return result;
};
技巧五:单调栈解决"下一个更大"问题
适用场景:寻找数组中每个元素的下一个更大元素
核心要点:
- 使用栈存储尚未找到更大元素的索引
- 当前元素大于栈顶索引对应元素时,计算天数差
- 时间复杂度 O(n),每个元素入栈出栈各一次
典型例题:每日温度
var dailyTemperatures = function(temperatures) {
const answer = new Array(temperatures.length).fill(0);
const stack = [];
for (let i = 0; i < temperatures.length; i++) {
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return answer;
};
技巧六:排序+栈解决车队问题
适用场景:计算车辆到达目的地时形成的车队数量
核心要点:
- 按位置从大到小排序,位置越大的车越先到达
- 计算每辆车到达目的地所需时间
- 如果当前车到达时间大于栈顶,说明追不上前车,形成新车队
典型例题:车队
var carFleet = function(target, position, speed) {
const arr = position
.map((p, index) => [p, speed[index]])
.sort((a, b) => b[0] - a[0]);
const stack = [];
for (let i = 0; i < arr.length; ++i) {
const time = (target - arr[i][0]) / arr[i][1];
if (stack.length === 0 || stack[stack.length - 1] < time) {
stack.push(time);
}
}
return stack.length;
};
三、JavaScript 数值处理技巧
向零截断方法比较
let num = -12.345;
// 推荐:最清晰的方式
Math.trunc(num) // -12
// 位运算技巧(有32位限制)
num | 0 // -12
~~num // -12
num << 0 // -12
// 其他方法
parseInt(num) // -12
Math.floor(num) // -13(注意负数差异)
Math.ceil(num) // -12
Math.round(num) // -12
注意事项:
- 位运算有32位整数限制(-2³¹ 到 2³¹-1)
parseInt会将数字先转为字符串,注意科学计数法陷阱- 推荐使用
Math.trunc()作为最清晰的解决方案
循环性能优化
// 传统 for 循环(性能最优)
for (let i = 0; i < arr.length; i++) {
const [pos, spd] = arr[i];
// 操作
}
// for...of 循环(可读性更好)
for (const [pos, spd] of arr) {
// 操作
}
开发准则:
- 优先可读性:先写出清晰、易维护的代码
- 测量后再优化:使用性能分析工具找到真正瓶颈
- 考虑使用场景:大多数情况下性能差异可以忽略
四、易错点提醒
- 字符串拼接陷阱:在回溯算法中,不要使用
s += '('会修改原字符串,应该使用s + '(' - 操作数顺序:在栈操作中,注意弹出顺序,后弹出的是右操作数
- 边界条件:栈操作前检查栈是否为空
- 数值转换:注意
parseInt的科学计数法陷阱和基数问题 - 性能优化:避免过早优化,优先保证代码可读性
- 单调栈理解:不要试图在遍历时立即计算结果,利用栈的延迟计算特性