一、🍊 栈的理论基础
1、概念
栈是后进先出,先进后出。
2、操作特性
栈是一种"操作受限"的线性表,它只允许在一端插入和删除数据。
3、使用场景
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,这时我们就应该首选"栈"这种数据结构。
二、🍊 如何实现一个栈
栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。类似于js中数组的push和pop。js代码如下:
kotlin
class ArrayStack {
constructor(n) {
this.items = new Array(n); // 数组
this.count = 0; // 栈中元素个数
this.n = n; // 栈的大小
}
// 入栈操作
push(item) {
// 数组空间不够了,直接返回false,入栈失败。
if (this.count === this.n) return false;
// 将item放到下标为count的位置,并且count加一
this.items[this.count] = item;
this.count++;
return true;
}
// 出栈操作
pop() {
// 栈为空,则直接返回null
if (this.count === 0) return null;
// 返回下标为count-1的数组元素,并且栈中元素个数count减一
const tmp = this.items[this.count - 1];
this.count--;
return tmp;
}
}
// 使用示例
const stack = new ArrayStack(5);
console.log(stack.push('A')); // true
console.log(stack.push('B')); // true
console.log(stack.push('C')); // true
console.log(stack.pop()); // 'C'
console.log(stack.pop()); // 'B'
- 上面的代码我们使用 class 关键字来定义一个类,constructor方法用于初始化实例的属性和方法
- push时检查栈是否已满(即count是否等于n),如果已满则返回false,否则将元素添加到数组中并增加count
-
- 在pop方法中,我们检查栈是否为空(即count是否为0),如果为空则返回null,否则返回栈顶元素并减少count。
注意:JavaScript中的数组是动态数组,这意味着数组大小会根据需要自动调整。然而,在这个顺序栈的实现中,我们预先设定了一个固定大小的数组来模拟栈的空间限制,这有助于更好地理解栈的基本工作原理。在实际应用中,可能不需要预先设定数组大小,而是让JavaScript自动管理数组大小
。
三、🍊 动态扩容顺序栈
当数组空间不够时,我们就重新申请一块更大的内存,将原来数组中数据统统拷贝过去。这样就实现了一个支持动态扩容的数组。如图:
1. 出栈
出栈不会涉及内存重新申请和数据的移动,所以出栈的时间复杂度和空间复杂度为O (1)
2. 入栈
入栈分为2种情况,
- 当栈中有空闲空间时,入栈操作的时间复杂度为 O(1)。
- 但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了 O(n)。也就是说,对于入栈操作来说,最好情况时间复杂度是 O(1),最坏情况时间复杂度是 O(n)。平均时间复杂度可以用之前文章中说到的均摊分析法。
分析:如果当前栈大小为 K,并且已满,当再有新的数据要入栈时,就需要重新申请 2 倍大小的内存,并且做 K 个数据的搬移操作,时间复杂度为 O(n),然后再入栈。但是,接下来的 K-1 次入栈操作,我们都不需要再重新申请内存和搬移数据,所以这 K-1 次入栈操作的时间复杂度为 O(1), 所以O(n)均摊到后面的K-1次,平均情况下的耗时就接近 O(1)。
四、🍊 相关应用
1、栈在函数调用中的应用
操作系统给每个线程分配一个独立的内存空间(栈这种结构)用来存储函数调用时的临时变量。每当执行一个函数时,就会将这个函数作为一个栈帧入栈(栈帧里面是该函数的临时变量),当被调用的函数执行完成返回时将这个函数对应的栈帧出栈。如图:
2、栈在表达式求值中的应用
- 假如只包含常见的加减乘除四则运算。
- 编译器就是通过两个栈来实现的。一个栈存储数据,另一个栈存储操作符。
- 我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈。当遇到运算符,就与运算符栈的栈顶元素进行比较。
- 果当前操作符的优先级比栈顶的要高,说明这一操作符需要先执行,但是数据还没有准备好,因此先将操作符保存起来,等数据准备好了再执行。
- 如果当前操作符的优先级比栈顶的要低,说明栈中的操作符需要先执行,那么先从数据栈中弹出两个数据,从操作符栈中弹出一个操作符,将这个结果先计算,并将计算出来的结果再入栈。
3、栈在括号匹配中的应用
- 假如表达式中只包含()、[]、{}。如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。
- 用栈来保存未匹配的左括号。
- 从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中。
- 当扫描到右括号时,从栈顶取出一个左括号。
- 如果能够匹配,比如"("跟")"匹配,"["跟"]"匹配,"{"跟"}"匹配,则继续扫描剩下的字符串。
- 如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
- 最后如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。
4、栈在浏览器前进后退功能中的应用
- 们使用两个栈,X 和 Y。
- 们把首次浏览的页面依次压入栈 X。
- 当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。
- 点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。
- 当 X 中没有数据时,则不能再后退,Y中没数据,则不能再前进。
注意:当前进后退之后,点击一个新的页面,则 X 中压入这个新页面,Y 清空。
五、🍊 总结
- 栈是一种操作受限的数据结构,只支持入栈和出栈操作。
- 栈的特点是后进先出,先进后出。
- 栈既可以通过数组实现,也可以通过链表来实现,用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。
- 不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1)