如何使用JavaScript实现栈与队列


Posted in Javascript onJune 24, 2019

前言

栈和队列是web开发中最常用的两种数据结构。绝大多数用户,甚至包括web开发人员,都不知道这个惊人的事实。如果你是一个程序员,那么请听我讲两个启发性的例子:使用堆栈来组织数据,来实现文本编辑器的“撤消”操作;使用队列处理数据,实现web浏览器的事件循环处理事件(单击click、悬停hoover等)。

等等,先想象一下我们作为用户和程序员,每天使用栈和队列的次数,这太惊人了吧!由于它们在设计上有普遍性和相似性,我决定从这里开始为大家介绍数据结构。


在计算机科学中,栈是一种线性数据结构。如果你理解起来有困难,就像最初非常困惑的我一样,不妨这样认为:一个栈可以对数据按照顺序进行组织和管理。

要理解这种顺序,我们可以把栈这种结构想象为自助餐厅的一堆盘子,当一个盘子被叠加到一堆盘子上时,原有的盘子保留了它们原来的顺序;同时,当一个新盘子被添加时,它会朝栈的底部方向堆积。每当我们添加一个新盘子时,被称作入栈,这个新盘子处于栈的顶部,也被称作栈顶。

这个添加盘子的过程会保留每个盘子被添加到栈中的顺序,每次从栈中取出一个盘子时也是一样的。我可能用了太多的篇幅来描述自助餐厅中的盘子是怎样被添加和删除的过程。

为了是大家理解栈更多的技术细节,让我们回顾一下前面关于文本编辑器的“撤消”操作。每次将文本添加到文本编辑器事,该文本被压入栈中。其中第一次添加的文本代表栈的底部(栈底);最后一次的修改表示栈的顶部(栈顶)。如果用户希望撤销最后一次修改,则删除处于栈的顶部的那段文本,这个过程可以不断重复,一直到栈中没有更多内容,这时我们会得到一个空白文件。

栈的操作

现在我们对栈的模型有了基本概念,下一步就要定义栈的两个操作:

  • push(data) 添加数据
  • pop() 删除最后添加的数据

栈的实现

现在让我们开始为栈编写代码吧!

栈的属性

为了实现栈结构,我们将会创建一个名为 Stack 的构造函数。栈的每个实例都有两个属性:_size 和 _storage。

function Stack() {
this._size = 0;
this._storage = {};
}

this._storage 属性使栈的每一个实例都具有自己的用来存储数据的容器; this._size 属性反映了当前栈中数据的个数。如果创建了一个新的栈的实例,并且有一个数据被存入栈中,那么 this._size 的值将被增加到1。如果又有数据入栈,this._size 的值将增加到2。如果一个数据从栈中被取出,this._size 的值将会减少为1。

栈的方法(操作)

我们需要定义可以向栈中添加(入栈)和从栈中取出(出栈)数据的方法。让我们从添加数据开始。

方法1/2: push(data)

(每一个栈的实例都具有这个方法,所以我们把它添加到栈结构的原型中)
我们对这个方法有两个要求:

1.每当添加数据时, 我们希望能够增加栈的大小。

2.每当添加数据时,我们希望能够保留它的添加顺序。

Stack.prototype.push = function(data) {
// increases the size of our storage
var size = this._size++;
// assigns size as a key of storage
// assigns data as the value of this key
this._storage[size] = data;
};

我们实现push(data)方法时要包含以下逻辑:声明一个变量 size 并赋值为 this._size++。指定 size 为 this._storage 的键;并将数据赋给相应键的值。

如果我们调用push(data)方法5次,那么栈的大小将是5。第一次入栈时,将会把数据存入this._storage 中键名为1对应的空间,当第5次入栈时,将会把数据存入this._storage 中键名为5对应的空间。现在我们的数据有了顺序!

方法2/2: pop()

我们已经实现了把数据送入栈中,下一步我们要从栈中弹出(删除)数据。从栈中弹出数据并不是简单的删除数据,它只删除最后一次添加的数据。

以下是这个方法的要点:

  1. 使用栈当前的大小获得最后一次添加的数据。
  2. 删除最后一次添加的数据。
  3. 使 _this._size 计数减一。
  4. 返回刚刚删除的数据。
Stack.prototype.pop = function() {
var size = this._size,
deletedData;
deletedData = this._storage[size];
delete this._storage[size];
this.size--;
return deletedData;
};

pop()方法满足以上四个要点。首先,我们声明了两个变量:size 用来初始化栈的大小;deletedData 用来保存栈中最后一次添加的数据。第二,我们删除了最后一次添加的数据的键值对。第三,我们把栈的大小减少了1.第四,返回从栈中删除的数据。

如果我们测试当前实现的pop()方法,会发现它适用下面的案例:如果向栈内push数据,栈的大小会增加1,如果从栈中pop()数据,栈的大小会减少1!

为了处理这个用例,我们将向pop()中添加if语句。

Stack.prototype.pop = function() {
var size = this._size,
deletedData;
if (size) {
deletedData = this._storage[size];
delete this._storage[size];
this._size--;
return deletedData;
}
};

通过添加if语句,可以使代码在存储中有数据时才被执行。

栈的完整实现

我们已经实现了完整的栈结构。不管以怎样的顺序调用任何一个方法,代码都可以工作!下面使代码的最终版本:

function Stack() {
this._size = 0;
this._storage = {};
}
Stack.prototype.push = function(data) {
var size = ++this._size;
this._storage[size] = data;
};
Stack.prototype.pop = function() {
var size = this._size,
deletedData;
if (size) {
deletedData = this._storage[size];
delete this._storage[size];
this._size--;
return deletedData;
}
};

从栈到队列

当我们想要按顺序添加数据或删除数据时,可以使用栈结构。根据它的定义,栈可以只删除最近添加的数据。如果想要删除最早的数据该怎么办呢?这时我们希望使用名为queue的数据结构。

队列

与栈类似,队列也是一个线性数据结构。与栈不同的是,队列只删除最先添加的数据。

为了帮助你明白队列是如何工作的,让我们花点时间举个例子。我们可以把队列想象成为熟食店的售票系统。每个顾客拿一张票,当他们的号码被呼叫时接受服务。持第一张票的顾客首先接受服务。

再进一步想象一下,这张票上有一个数字“1”。下一张票上有数字“2”。得到二张票的顾客将会第二个接受服务。(如果我们的售票系统像栈一样运行,最先进入堆栈的客户将会最后一个接受服务!)

队列的一个更实际的例子是Web浏览器的事件循环。当触发不同事件时,例如单击某个按钮,点击事件将被添加到事件循环队列中,并按照它们进入队列的顺序进行处理。

现在我们具有了队列的概念,接下来就要定义它的操作。你会注意到,队列的操作和栈非常相似。区别就在被删除的数据在什么地方。

  • enqueue(data) 将数据添加到队列中。
  • dequeue 删除最早加入队列的数据。

队列的实现

现在让我们开始写队列的代码吧!

队列的属性

在实现队列的代码中,我们将会创建一个名为 Queue 的构造方法。接下来添加三个属性:_oldestIndex, _newestIndex, 和 _storage。在下一小节中,_oldestIndex 和 _newestIndex 的作用将变得更加清晰。

function Queue() {
this._oldestIndex = 1;
this._newestIndex = 1;
this._storage = {};
}

队列的方法

现在我们将创建队列会用到的三个方法:size(), enqueue(data), 和 dequeue(data)。我将描述每个方法的作用,写出每个方法的代码,然后解释这些代码。

方法1/3:size( )

这个方法有两个作用:

  • 返回当前队列的长度。
  • 保持队列中键的正确范围。
Queue.prototype.size = function() {
return this._newestIndex - this._oldestIndex;
};

实现 size() 可能显得微不足道,但你会很快发现并不是这样的。为了理解其原因,我们必须快速重新审视 size() 在栈结构中的实现。

回想一下栈的概念模型,假设我们把5个盘子添加到一个栈上。栈的大小是5,每个盘子都有一个数字,从1(第一个添加的盘子)到5(最后一个添加的盘子)。如果我们取走三个盘子,就只剩下两个盘子。我们可以简单地用5减去3,得到正确的大小,也就是2。这是关于栈大小最重要的一点:当前大小相当于从栈顶部的盘子(2)到栈中其他盘子(1)的计数。换句话说,键的范围总是从当前大小到1之间。

现在,让我们将栈大小的实现应用到队列中。假设有五个顾客从我们的售票系统中取到了票。第一个顾客有一张显示数字1的票,第五个客户有一张显示数字5的票。现在有了一个队列,拿着第一张票的第一位顾客。

假设第一个客户接受了服务,这张票会从队列中被移除。与栈类似,我们可以通过从5减去1来获得队列的正确大小。那么服务队列中还有4张票。现在出现了一个问题:队列的大小不能对应正确的票号。如果我们从五减去一个,得到大小是4,但是不能使用4来确定当前队列中剩余票的编号范围。我们并不能确定队列中票号的顺序到底是1到4还是2到5。

这就是 oldestIndex 和 newestIndex 这两个属性 在队列中的用途。所有这一切似乎令人困惑——到现在我仍然会偶尔觉得困惑。下面的例子可以帮助我门理顺所有的逻辑。
假设我们的熟食店有两个售票系统:

  1. _newestindex 代表顾客售票系统的票。
  2. _oldestindex 代表员工售票系统的票。

对于两个售票系统来说,这是最难掌握的概念:当两个系统中的数字相同时,队列中的每个客户都被处理了,队列是空的。我们将使用下面的场景来加强这种逻辑:

  1. 当顾客买票时,顾客的票号从_newestIndex 得到,票的编号是1。顾客售票系统的下一张票号码是2。
  2. 员工不买票,员工售票系统中当前票的编号是1。
  3. 我们在顾客系统中得到当前的票号2,减去员工系统中的号码1,得到的结果是1。这个数字1表示仍然在队列中没有被删除的票的数量
  4. 员工从它们的售票系统中取票,这张票代表正在被服务的顾客的票号,从_oldestIndex中得到,数字为1。
  5. 重复第4步,现在差为0,队列中没有其他的票了。

现在属性 _newestindex可以告诉我们被分配在队列中票号的最大值(键),属性 _oldestindex 可以告诉我们最先进入队列中票号(键)。

探讨完了size(),接下来看enqueue(data)方法。

方法2/3:enqueue(data)

对于 enqueue 方法,有两个功能:

  • 使用_newestIndex 的值作为 this._storage 的键,并使用要添加的数据作为该键的值。
  • 将_newestIndex 的值增加1。

基于这两个功能,我们将编写 enqueue(data) 方法的代码:

Queue.prototype.enqueue = function(data) {
this._storage[this._newestIndex] = data;
this._newestIndex++;
};

该方法的主体只有两行代码。 在第一行,用 this._newestIndex 为this._storage 创建一个新的键,并为其分配数据。 this._newestIndex 始终从1开始。在第二行代码中,我们将 this._newestIndex 的值增加1,将其更新为2。
以上是方法 enqueue(data) 的所有代码。下面我们来实现方法 dequeue( )。

方法2/3:dequeue( )

以下是此方法的两个功能点:

  • 删除队列中最旧的数据。
  • 属性 _oldestIndex 加1。
Queue.prototype.dequeue = function() {
var oldestIndex = this._oldestIndex,
deletedData = this._storage[oldestIndex];
delete this._storage[oldestIndex];
this._oldestIndex++;
return deletedData;
};

在 dequeue( )的代码中,我们声明两个变量。 第一个变量 oldestIndex 给 this._oldestIndex 赋值。第二个变量 deletedData 被赋予 this._storage[oldestIndex] 的值。

下一步,删除队列中最早的索引。之后将 this._oldestIndex 的值加1。最后返回刚刚被删除的数据。

与栈的 pop() 方法第一次实现中出现的问题类似,dequeue() 在队列中没有数据的情况下不应该被执行。我们需要一些代码来处理这种情况。

Queue.prototype.dequeue = function() {
var oldestIndex = this._oldestIndex,
newestIndex = this._newestIndex,
deletedData;
if (oldestIndex !== newestIndex) {
deletedData = this._storage[oldestIndex];
delete this._storage[oldestIndex];
this._oldestIndex++;
return deletedData;
}
};

每当 oldestIndex 和 newestIndex 的值不相等时,我们就执行前面的逻辑。

队列的完整实现代码

到此为止,我们实现了一个完整的队列结构的逻辑。下面是全部代码。

function Queue() {
this._oldestIndex = 1;
this._newestIndex = 1;
this._storage = {};
}
Queue.prototype.size = function() {
return this._newestIndex - this._oldestIndex;
};
Queue.prototype.enqueue = function(data) {
this._storage[this._newestIndex] = data;
this._newestIndex++;
};
Queue.prototype.dequeue = function() {
var oldestIndex = this._oldestIndex,
newestIndex = this._newestIndex,
deletedData;
if (oldestIndex !== newestIndex) {
deletedData = this._storage[oldestIndex];
delete this._storage[oldestIndex];
this._oldestIndex++;
return deletedData;
}
};

结束语

在本文中,我们探讨了两个线性数据结构:栈和队列。栈按照顺序存储数据,并删除最后添加的数据;队列按顺序存储数据,但删除最先的添加数据。

如果这些数据结构的实现看起来微不足道,请提醒自己数据结构的用途。它们并没有被设计得过于复杂,它们是用来帮助我们组织数据的。在这种情况下,如果您发现有需要按顺序组织数据的场合,请考虑使用栈或队列。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
商城常用滚动的焦点图效果代码简单实用
Mar 28 Javascript
JavaScript创建类/对象的几种方式概述及实例
May 06 Javascript
使用GruntJS链接与压缩多个JavaScript文件过程详解
Aug 02 Javascript
ECHO.js 纯javascript轻量级延迟加载的实例代码
May 24 Javascript
jquery树形菜单效果的简单实例
Jun 06 Javascript
JavaScript实现反转字符串的方法详解
Apr 27 Javascript
AngularJS实现的JSONP跨域访问数据传输功能详解
Jul 20 Javascript
删除table表格行的实例讲解
Sep 21 Javascript
node使用promise替代回调函数
May 07 Javascript
Vuex 使用及简单实例(计数器)
Aug 29 Javascript
JS+HTML5 Canvas实现简单的写字板功能示例
Aug 30 Javascript
前端js中的事件循环eventloop机制详解
May 15 Javascript
简单了解JavaScript中的执行上下文和堆栈
Jun 24 #Javascript
一次让你了解全部JavaScript的作用域
Jun 24 #Javascript
通过循环优化 JavaScript 程序
Jun 24 #Javascript
在React中写一个Animation组件为组件进入和离开加上动画/过度效果
Jun 24 #Javascript
node中实现删除目录的几种方法
Jun 24 #Javascript
什么时候不能在 Node.js 中使用 Lock Files
Jun 24 #Javascript
vue-cli脚手架引入弹出层layer插件的几种方法
Jun 24 #Javascript
You might like
php实现文件编码批量转换
2014/03/10 PHP
php版微信数据统计接口用法示例
2016/10/12 PHP
TP5(thinkPHP框架)实现后台清除缓存功能示例
2019/05/29 PHP
用jquery实现等比例缩放图片效果插件
2010/07/24 Javascript
jQuery中click事件的定义和用法
2014/12/20 Javascript
AngularJS中的Directive自定义一个表格
2016/01/25 Javascript
bootstrap实现的自适应页面简单应用示例
2017/03/09 Javascript
Angularjs中数据绑定的实例详解
2017/08/25 Javascript
简单谈谈关于Angular Cli打包的事
2017/09/05 Javascript
防止页面url缓存中ajax中post请求的处理方法
2017/10/10 Javascript
AngularJS 中的数据源的循环输出
2017/10/12 Javascript
Node.js创建Web、TCP服务器
2017/12/05 Javascript
vue-cli 使用axios的操作方法及整合axios的多种方法
2018/09/12 Javascript
vue实现条件判断动态绑定样式的方法
2018/09/29 Javascript
利用原生的JavaScript实现简单拼图游戏
2018/11/18 Javascript
微信小程序登录态和检验注册过没的app.js写法
2019/05/22 Javascript
[01:35]辉夜杯战队访谈宣传片—iG.V
2015/12/25 DOTA
以windows service方式运行Python程序的方法
2015/06/03 Python
Java中重定向输出流实现用文件记录程序日志
2015/06/12 Python
Python爬虫辅助利器PyQuery模块的安装使用攻略
2016/04/24 Python
Python爬虫实例_城市公交网络站点数据的爬取方法
2018/01/10 Python
对Python中list的倒序索引和切片实例讲解
2018/11/15 Python
python引用(import)某个模块提示没找到对应模块的解决方法
2019/01/19 Python
对python_discover方法遍历所有执行的用例详解
2019/02/13 Python
Django 批量插入数据的实现方法
2020/01/12 Python
Tensorflow之MNIST CNN实现并保存、加载模型
2020/06/17 Python
python3爬虫GIL修改多线程实例讲解
2020/11/24 Python
css3实现简单的白云飘动背景特效
2020/10/28 HTML / CSS
美国最大的无人机经销商:DroneNerds
2018/03/20 全球购物
Fossil德国官网:化石手表、手袋、珠宝及配件
2019/12/07 全球购物
Erwin Müller穆勒家居瑞士官网:您整个家庭的邮购公司
2019/12/28 全球购物
绿里奇迹观后感
2015/06/15 职场文书
如何起草一份正确的合伙创业协议书?
2019/07/04 职场文书
go类型转换及与C的类型转换方式
2021/05/05 Golang
详解CSS玩转图片Base64编码
2021/05/25 HTML / CSS
python四个坐标点对图片区域最小外接矩形进行裁剪
2021/06/04 Python