数据结构与算法——栈

一、🍊 栈的理论基础

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)
相关推荐
点燃银河尽头的篝火(●'◡'●)9 分钟前
【BurpSuite】Cross-site scripting (XSS 学徒部分:1-9)
前端·web安全·网络安全·xss
Jiaberrr36 分钟前
手把手教你:微信小程序实现语音留言功能
前端·微信小程序·小程序·语音·录音
熊猫在哪36 分钟前
安装nuxt3
前端·nuxt.js
明志刘明41 分钟前
昇思量子计算系列教程-龙算法
深度学习·算法·量子计算
დ旧言~44 分钟前
刷题训练之栈
算法
fieldsss1 小时前
动态规划part 06
算法·动态规划
kuilaurence1 小时前
C语言数组学习
c语言·学习·算法
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_036】5.6 Grid 网格布局中与对齐相关的属性 + 5.7本章小结
前端·css·css3·html5·网格布局·grid·css网格
郭源潮11 小时前
【C++二叉树】二叉树的前序遍历、中序遍历、后序遍历递归与非递归实现
开发语言·c++·算法
请揣满RMB1 小时前
BFS 解决多源最短路问题
算法·宽度优先