栈和队列

简介: 大家好,我王有志又回来啦。今天我们来学习线性表中最后两种数据结构:栈和队列。
大家好,我是[王有志],欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:[共同富裕的Java人]
最近被全链路优化搞得焦头烂额,等抽出时间来和大家分享下我司正在做的“全面提速工程”。

今天我们来学习线性表中最后两种数据结构:栈和队列。它们是两种比较特殊的线性表,相较于数组和链表而言,可以认为它们是“高级”的线性表。

栈和队列的特点

前几天和媳妇聊到大学校门口的美食广场,每到夜晚便人声鼎沸,撸肉串的,喝扎啤的.....

Hold on!Hold on!(最近沉迷《中国说唱巅峰对决》hhh)。

羊肉串和扎啤不就是栈和队列吗?你品,你细品。

串肉串的时候,从签子的尖头(栈顶)开始串(入栈),最先串进去的被压倒签子的底部(栈底),而撸串(出栈)也是从尖头开始,最后串进去的却最先被吃掉(后进先出,LIFO,last in first out)。

灌扎啤的时候,从灌口(队尾)灌进去(入队)的扎啤停留在罐底(队首),当你从罐底的水龙头打开的时候,最先流出来(出队)的停留在罐底的那部分(先进先出,FIFO,first in first out)。

我们再来画张图,来看看栈和队列到底“长什么样”。

图1:形象的栈和队列.png

看起来和队队列就是很普通的线性表啊,没什么特别的。

实际上就是这样的,如果限定数组或链表只能在尾部插入和尾部删除,那就是栈。如果只能在头部删除尾部插入,那就是队列。

也正是如此,我们常常称栈和队列为受限线性表,数组和链表为非受限线性表。而栈和队列所允许的操作也是数组和链表所允许操作的子集

栈和队列的ADT

了解了栈和队列后,我们来看看它们都允许哪些操作,这里我们“抄袭”Java中方法的命名习惯给出对应数据结构的ADT,首先是栈:

public interface Stack<E> {

  /**
   * 大小
   */
  int size();

  /**
   * 是否为空
   */
  boolean isEmpty();

  /**
   * 入栈
   */
  void push(E e);

  /**
   * 出栈
   */
  E pop();

  /**
   * 查看栈顶数据
   * 与{@link Stack#pop()}的区别是:
   * pop会移除栈顶数据
   * peek不会移除栈顶数据
   */
  E peek();
}

接着是队列:

public interface Queue<E> {

  /**
   * 大小
   */
  int size();

  /**
   * 是否为空
   */
  boolean isEmpty();

  /**
   * 入队
   */
  void add(E element);

  /**
   * 出队
   */
  E poll();

  /**
   * 查看队首数据
   * 与{@link Queue#poll()}的区别是:
   * poll会移除队首数据
   * peek不会移除队首数据
   */
  E peek();
}

相较于动态数组和链表,栈和队列的操作就简单很多了,实现起来也非常容易,在这里就不水字数了。你可以基于已经实现过的动态数组和链表去实现,也可以自行实现顺序结构或者链式结构。

附上我实现的队列,欢迎大家一起讨论。

栈的应用

栈这种结构虽然简单,但是在计算机世界中却有着极其重要的作用,其中最耳熟能详的就是调用栈了。

如果对调用栈不熟悉的小伙伴,推荐阅读汤洋老师的文章《Call Stack(调用栈)是什么?》

调用栈

如果你是Java程序员,相信在面试中你一定被问到过虚拟机栈。没错,Java中的虚拟机栈就是最典型的调用栈之一。

还记得我们在编程技巧:从斐波那契数列到递归中说到尾递归消除时的那张图吗?

图2:方法压栈.png

main中调用functionAfunctionA调用functionBfunctionB调用functionC,每次调用后都压入到栈中,按照代码的执行逻辑,functionC最先执行完毕,可以从栈中取出,然后functionBfunctionAmain

现在来看,栈的特性是不是完美契合计算机中的调用顺序?

撤销栈

作为一名资深的“CV工程师”,除了Ctrl+CCtrl+V外,我还会经常使用Ctrl+Z,毕竟谁也不能保证绝对不出错嘛。

不过,你有没有思考过计算机是怎么知道撤销哪一步操作呢?

其实很简单,计算机使用栈这种结构记录每一步操作,假如我们分别输入“曾经”,“沧海”,“难为水”,当我们输入完成后,发现“难为水”写成了“难为谁”(可能是输入法不够智能)。

图3:撤销栈.png

当我们按下Ctrl+Z时,计算机只需要将撤销栈中栈顶数据对应的输入删除就可以实现撤销最近一次操作了。

逆波兰式

逆波兰式又称为后缀表达式,当然有后缀表达式就有前缀表达式(也称为波兰式),既然前后都有了,那也有中缀表达式

假如我们有一道算术题:$(1+3)×(4−2)$。

相信你可以很轻松的告诉我答案,但对于计算机来说,这太复杂了,既有括号,又有乘法,加法,优先级都搞不明白,还要怎么计算?不过也不是没有办法。

逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式形式, 在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法、后序表示法。逆波兰记法不需要括号来标识操作符的优先级

逆波兰结构由弗里德里希·鲍尔(Friedrich L. Bauer)和艾兹格·迪科斯彻在1960年代早期提议用于表达式求值,以利用堆栈结构减少计算机内存访问。逆波兰记法和相应的算法由澳大利亚哲学家、计算机学家查尔斯·汉布尔(Charles Hamblin)在1960年代中期扩充。

在1960和1970年代,逆波兰记法广泛地被用于台式计算器,因此也在普通公众(如工程、商业和金融等领域)中使用。

以上来自维基百科[逆波兰表示法]词条,能够科学上网的小伙伴可以自行阅读。

所以上面的题目在逆波兰表达式中为:$13+42−×$。

你发现没有,在逆波兰式和栈结合后,计算机不需要在考虑括号对运算优先级的提升了,这样计算机就可以快速计算了。

图4:逆波兰表达式.png

简单描述逆波兰式的算法过程为,数字按顺序压入栈,遇到符号时弹出栈顶的两个数字进行计算

大概原理是这么简单,不过实际处理起来还是比较复杂的,这里附上我实现的简易版[逆波兰式算法]

循环队列

使用数组实现队列时,出队不得不移动数据以保证下次依旧可以通过下标0取得数据,因此,出队操作的时间复杂度是O(n),显然是有点慢了。

如果我们换一个角度思考,为了保证下标0可持续使用,而移动数据代价是不是太大了?可不可以改变下标,而不移动数据呢?

答案是肯定的,我们可以不断改变队首的位置,而非移动数据来保证队首始终在下标为0的位置。

图5:循环队列.png

每次出队后指向队首的指针向后移动一位,入队后指向队尾的指针同样向后移动一位。这样,在不考虑超出数组容量的前提下,可以循环往复的使用数组中的内存,当然超过了就需要考虑扩容和重排队列数据了。

既然知道了基本原理,相信你可以很轻松的实现循环队列,提供我实现的[循环队列]作为参考,欢迎大家一起讨论。

结语

今天我们一起学习了栈和队列,它们是线性表中比较特殊的存在,如果要给线性表划分层级的话,基础的是数组和链表,在此之上我们可以构建出“更高级”的栈和队列。

在通过生活中的例子解释了栈和队列后我们学习了栈在计算机世界中丰富的应用,以及一种的队列-循环队列。

到今天为止线性表中比较基础的内容已经结束了,如果想要彻底的掌握它们还需要勤加练习。接下来,我们就要开始树的学习了,各位小伙伴们加油。

练习

本篇内容的代码仓库:[Gitee代码仓库]

好了,今天就到这里了,Bye~~

目录
相关文章
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
41 1
|
21小时前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
24 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
21小时前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
110 75
|
21小时前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
21 7
|
21小时前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
22 9
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
76 5
|
2月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
2月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
57 0
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
255 9
|
2月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
下一篇
开通oss服务