栈,是一种基本的数据结构,就像我们放盘子一样,一个一个往上放,用的时候,从上一个一个往下拿,不能从中间抽。典型的特点:只允许在一端插入和删除数据,后进者先出,先进者后出。当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,优先联想“栈”这种数据结构。最近在看数据结构,所以简单总结了栈的一些常用场景:括号匹配
假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。
比如,{[{}]}
或 [{()}([])]
等都为合法格式,而{[}()]
或 [({)]
为不合法的格式。
现在给一个包含三种括号的表达式字符串,如何检查它是否合法呢?从左到右扫描字符串,遇到左边的括号,进栈;遇到右边的括号,看看和栈顶的括号是不是配对的,是则出栈,不是则肯定非法字符串。最后,栈空表示合法字符串,否则非法字符串。function validString(string) {
const resultStack = [];
const markMap = new Map([
["(", ")"],
["[", "]"],
["{", "}"]
]);
const top = arr => arr[arr.length - 1];
for (let i = 0; i < string.split("").length; i++) {
const str = string[i];
const isLeft = markMap.get(str);
// 遇到左边的括号,进栈
if (isLeft) {
resultStack.push(str);
continue;
}
// 遇到右边的括号,看看和栈顶的括号是不是配对的,是则出栈,不是则肯定非法字符串
const isRelative = str === markMap.get(top(resultStack));
if (!isRelative) {
return false;
}
resultStack.pop();
}
// 最后,栈空表示合法字符串,否则非法字符串
return !resultStack.length;
}
// true
console.log(validString("[[({}{})]]({})"));
表达式中求值
比如:3+5*8-6
对于这个四则运算,我们可以很快求解出答案。
但是对于计算机来说,怎么实现这样一个表达式求值的功能呢?其实,利用两个栈,一个保存数,一个保存符号。从左向右遍历表达式,当遇到数字,直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
栈顶的优先级低,入栈。栈顶的优先级高,则数栈栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。这边用js实现代码:输入单纯考虑10以内的加减乘除的表达式,得到计算结果/** -- 准备工作开始 - **/
// 符号的优先级
const priorityMap = { "+": 0, "-": 0, "*": 1, "/": 1 };
// 符号的运算公式
const computeMap = {
"+": (a, b) => a + b,
"-": (a, b) => a - b,
"*": (a, b) => a * b,
"/": (a, b) => a / b,
};
// 栈顶元素
const top = (arr) => arr[arr.length - 1];
// 次栈顶元素
const top2 = (arr) => arr[arr.length - 2];
// 栈是不是空的
const isEmpty = (arr) => !arr.length;
// 每次计算的时候,根据符号栈的栈顶符号,计算数字栈前两个,然后符号栈执行出栈,将计算结果推到数字栈
const compute = (numberStack, markStack) => {
const computeFn = computeMap[markStack[markStack.length - 1]];
const result = computeFn(top2(numberStack), top(numberStack));
markStack.pop();
numberStack.pop();
numberStack.pop();
numberStack.push(result - 0);
};
/** -- 准备工作结束 -- **/
// 正餐:
function computeExpression(expressionString) {
const markStack = [];
const numberStack = [];
expressionString.split("").forEach((str) => {
const isMark = priorityMap.hasOwnProperty(str);
// 是数字的话,直接推进数字栈
if (!isMark) {
numberStack.push(str - 0);
return;
}
// 是符号的话,如果符号栈不为空且当前优先级不大于栈顶符号的优先级,则计算
// 直到符号栈为空或者当前优先级大于栈顶符号的优先级,符号栈才执行进栈
while (
!isEmpty(markStack) &&
priorityMap[str] <= priorityMap[top(markStack)]
) {
compute(numberStack, markStack);
}
markStack.push(str);
});
// 遍历完之后,挨个计算,直到符号栈为空
while (markStack.length) {
compute(numberStack, markStack);
}
// 数字栈只剩下一个
const result = top(numberStack);
numberStack.pop();
return result;
}
// 18
console.log(computeExpression("1+2*5*1+4/2+5"));
下一个更大元素
给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。比如给一个数组 [2,1,2,4,3],则需要返回数组 [4,2,4,-1,-1]。因为2的下一个更大的元素是4,1的下一个更大的元素是2,以类类推。当然暴力想法很容易,每次都来个遍历,但时间复杂度会变成o(n^2)。把数组的元素想象成一个队伍,元素大小想象成人的身高。你现在是长官,面对他们站在前面。
如何求元素「2」的 Next Greater Number 呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的 Next Greater Number,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。其实想想,下一个更大的元素,遍历的时候,如果从前往后遍历,是不知道后面的。所以遍历最好从后往前遍历。
就是倒着进栈,用栈存放更小的值
只要栈不为空且当前的值不小于栈顶的值,就将栈顶的值出栈(结合图,矮个子的就起开)
直到栈为空或者当前的值小于栈顶的值
对应的答案就是栈顶的值,栈为空的话就是-1,并且当前值进栈const top = arr => arr[arr.length - 1]
const isEmpty = arr => !arr.length
var nextGreaterElement = function(numbers) {
const stack = [];
let ans = [];
for (let i = numbers.length - 1; i >= 0; i--) {
const cur = numbers[i];
while (!isEmpty(stack) && cur >= top(stack) ) {
stack.pop();
}
ans[i] = isEmpty(stack) ? -1 : top(stack);
stack.push(cur);
}
return ans;
};
这个算法的复杂度只有 O(n),总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。给一个数组,存放近几天的天气气温,需要返回一个数组,计算:对于每一天,还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0
举例:天气数组 T = [23, 24, 25, 21, 19, 22, 26, 23]
,返回[1, 1, 4, 2, 1, 1, 0, 0]
细品品,其实思路很接近了,只是这里因为计算天数,所以栈里存放的是索引。var dailyTemperatures = function(numbers) {
const stack = [];
// 存放的是索引,这样方便计算天数
let ans = [];
for (let i = numbers.length - 1; i >= 0; i--) {
const cur = numbers[i];
// 注意比较的时候,根据栈顶的索引拿到相应的温度进行比较
while (!isEmpty(stack) && cur >= numbers[top(stack)]) {
stack.pop();
}
// 存答案的时候,是计算差值
ans[i] = isEmpty(stack) ? 0 : top(stack) - i;
stack.push(i);
}
return ans;
};
再拓展下!
如何处理「循环数组」?同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理?给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,4]。拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4 。这里就很有技巧了,通过 %
运算符求模(余数),数组索引%整数倍数的数组长度 === 数组索引
,让数组有”环形“的效果,但实际长度没有增加。var nextGreaterElement = function(numbers) {
const stack = [];
let ans = [];
// 假装数组长度翻倍了
for (let i = numbers.length * 2 - 1; i >= 0; i--) {
// 这里要用实际索引
const cur = numbers[i % numbers.length];
while (!isEmpty(stack) && cur >= top(stack)) {
stack.pop();
}
// 只需要真实数组长度这里存答案即可
if (i < numbers.length) {
ans[i] = isEmpty(stack) ? -1 : top(stack);
}
stack.push(cur);
}
return ans;
};
浏览器的前进后退
浏览器里,依次看了a、b、c三个页面,此时后退看到b,再后退看到a,前进又看到b,但是从b跳转到d之后,此时不管是前进还是后退都无法看到c页面了。新建两个栈:x栈负责存储打开过的网址和后退的网址,y栈负责存储点击前进时浏览的网址。- 点击后退,c出x栈,进y栈,此时x栈栈顶是b,所以看到b页面
- 再点击后退,b出x栈,进y栈,此时x栈栈顶是a,所以看到a页面
- 点击前进,b出y栈,进x栈,此时x栈栈顶是b,所以看到b页面
- 跳转到新页面d,清空y栈,d进x栈,此时x栈栈顶是d,所以看到b页面,y栈被清空了所以不论前进或后退都访问不到
执行上下文栈
执行Javascript代码,更是栈的应用场景之一,JS是单线程,所有代码排队执行。- 浏览器执行JS代码的时候,首先创建全局的执行上下文,压入执行栈的顶部
- 执行函数的时候,创建函数的执行上下文,压入执行栈的顶部
- 浏览器的JS引擎总是访问,位于执行栈的顶部的执行上下文
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
- 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
- 调用
changeColor
函数时,此时changeColor
函数内部代码还未执行,js执行引擎立即创建一个changeColor
的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECStack)中。 - 执行
changeColor
函数过程中,调用swapColors
函数,同样地,swapColors
函数执行之前也创建了一个swapColors
的执行上下文,并压入到执行栈中。 swapColors
函数执行完成,swapColors
函数的执行上下文出栈,并且被销毁。changeColor
函数执行完成,changeColor
函数的执行上下文出栈,并且被销毁。
这里注意,执行函数,是先创建函数的执行上下文,然后再是执行函数内部的代码。
换言之,函数内部代码执行前:参数已被初始化、作用域链已被创建、this已被确定。引用
- https://segmentfault.com/a/1190000018550118
- https://time.geekbang.org/column/intro/126
- 单调栈解决 Next Greater Number 一类问题https://leetcode-cn.com/problems/next-greater-element-i/solution/dan-diao-zhan-jie-jue-next-greater-number-yi-lei-w/