如何使用JavaScript实现栈与队列

前言

栈和队列是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;
}
};

结束语

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

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

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

(0)

相关推荐

  • JavaScript数据结构学习之数组、栈与队列

    前言 数据结构就是关系,没错,就是数据元素相互之间存在的一种或多种特定关系的集合. 常用的数据结构有: 数组,队列(queue),堆(heap),栈(stack),链表(linked list ),树(tree),图(graph)和散列表(hash) 本文主要介绍的是数组.栈与队列,下面来一起看看详细的介绍吧. 一.数组 数组是平时使用最常用的数据结构,在JavaScript中数组是动态的分配大小,在这里我不会介绍JavaScript里面数组的所有的方法,而是针对数据结构这个方向谈谈所用到的方法

  • JavaScript数组的栈方法与队列方法详解

    数组(Array)和对象(Object)应该是JavaScript中使用最多也是最频繁的两种类型了,Array提供了很多常用的方法:栈方法.队列方法.重排序方法.操作方法.位置方法.迭代方法等等. 1.Array的栈方法 栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除.栈中项的插入(push)和移除,只发生在一个位置--栈的顶部.ECMAScript为数组提供了push()和pop()方法,可以实现类似栈的行为.下面两图分别演示了入栈与出

  • JS实现利用两个队列表示一个栈的方法

    本文实例讲述了JS实现利用两个队列表示一个栈的方法.分享给大家供大家参考,具体如下: 先看原理图: 理清楚思路,再动笔写: <!DOCTYPE html> <html> <head> <title>2 Queue</title> <meta charset="utf-8"/> <script type="text/javascript"> var arr1 = []; var arr

  • JavaScript栈和队列相关操作与实现方法详解

    本文实例讲述了JavaScript栈和队列相关操作与实现方法.分享给大家供大家参考,具体如下: 一.栈的介绍 栈就是和列表类似的一种数据结构,数据只能在栈顶添加或者删除.栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,成为栈顶.栈具有后进先出的特点,所以任何不在栈顶的元素都无法访问. 后进先出(LIFO,last-in-first-out)的数据结构. 对栈的操作 1.对栈的两种主要操作为将一个元素压入栈和将一个元素弹出栈. 入栈:push(); 出栈:pop(); 2.预览栈顶的元素pe

  • JavaScript基于数组实现的栈与队列操作示例

    本文实例讲述了JavaScript基于数组实现的栈与队列操作.分享给大家供大家参考,具体如下: 栈数据结构: 1. 后进先出 队列在列表的尾端添加项,从列表的尾端移除项 队列图: 实现代码: var colors = ["red","blue"]; colors.push("brown"); //从队列尾部添加一项 console.log(colors);//[ 'red', 'blue', 'brown' ] var item =colors.

  • JavaScript中栈和队列应用详情

    目录 什么是栈和队列 什么时候用到栈 目录的计算 什么是栈和队列 栈如果用数组模拟的话是类似于一个U形桶状堆栈空间,地下是封口的,只能从顶部一个地方进出,它的进出都是有顺序的,看下图:如果是进入,则是最下是最先进入的,如果要出,则是从最顶部先出 和队列来对比,只是数据结构相同,队列是一侧进一侧出,做任务队列调度的时候都是先入先出 什么时候用到栈 从编辑器开发写代码的时候,如果代码的中的括号写错了,则很容易判定出那个地方少了括号,在JavaScript语法中有可以设定大括号{}.中括号:[].小括

  • 如何使用JavaScript实现栈与队列

    前言 栈和队列是web开发中最常用的两种数据结构.绝大多数用户,甚至包括web开发人员,都不知道这个惊人的事实.如果你是一个程序员,那么请听我讲两个启发性的例子:使用堆栈来组织数据,来实现文本编辑器的"撤消"操作;使用队列处理数据,实现web浏览器的事件循环处理事件(单击click.悬停hoover等). 等等,先想象一下我们作为用户和程序员,每天使用栈和队列的次数,这太惊人了吧!由于它们在设计上有普遍性和相似性,我决定从这里开始为大家介绍数据结构. 栈 在计算机科学中,栈是一种线性数

  • Javascript数据结构之栈和队列详解

    目录 前言 栈(stack) 栈实现 解决实际问题 栈的另外应用 简单队列(Queue) 队列实现 队列应用 - 树的广度优先搜索(breadth-first search,BFS) 优先队列 优先队列实现 线性数据结构实现优先队列 Heap(堆)数据结构实现优先队列 代码实现一个二叉堆 小顶堆在 React Scheduler 事务调度的包应用 最后 前言 我们实际开发中,比较熟悉的数据结构是数组.一般情况下够用了.但如果遇到复杂的问题,数组就捉襟见肘了.在解决一个复杂的实际问题的时候,选择一

  • JavaScript实现栈结构Stack过程详解

    JavaScript实现栈结构(Stack) 一.前言 1.1. 什么是数据结构? 数据结构就是在计算机中,存储和组织数据的方式. 例如:图书管理,怎样摆放图书才能既能放很多书,也方便取? 主要需要考虑两个问题: 操作一:新书怎么插入?操作二:怎么找到某本指定的书? 常见的数据结构: 数组(Aarray)栈(Stack)链表(Linked List)图(Graph)散列表(Hash)队列(Queue)树(Tree)堆(Heap) 注意:数据结构与算法与语言无关,常见的编程语言都有直接或间接的使用

  • JavaScript实现栈结构详细过程

    目录 一.认识栈结构 二.栈结构封装 三.十进制转化为二进制 一.认识栈结构 我们知道数组是一种常见的数据结构,并且可以在数组的任意位置插入和删除数据,但是有时候,我们为了实现某些功能,必须对这种任意性加以限制,而栈和队列就是比较常见的受限的数据结构,我们先来看看栈. 栈(stack),它是一种受限的线性表,后进先出(LIFO) 其限制性是允许在表的一端进行插入和删除运算.这一端被称为栈顶,相对的,把另一端,称为栈底. LIFO(last in first out)表示就是后进入的元素,第一个弹

  • JavaScript实现一个Promise队列小工具

    目录 摘要 思考 实现 总结 摘要 在百度的解释中,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表.进行插入操作的端称为队尾,进行删除操作的端称为队头. 受这个队列结构的启发,在前端不同的业务场景中,由于一次性发起的异步请求过多,并且这些服务位于不同组件或者不同兄弟功能之间,我们无法直接对这些方法进行同步,所以需要引入队列的概念,对这些并发性的问题进行顺序处理. 思考 为什么要写一个类似于队列的功能? 我们知道

  • 栈和队列数据结构的基本概念及其相关的Python实现

    先来回顾一下栈和队列的基本概念: 相同点:从"数据结构"的角度看,它们都是线性结构,即数据元素之间的关系相同. 不同点:栈(Stack)是限定只能在表的一端进行插入和删除操作的线性表. 队列(Queue)是限定只能在表的一端进行插入和在另一端进行删除操作的线性表.它们是完全不同的数据类型.除了它们各自的基本操作集不同外,主要区别是对插入和删除操作的"限定". 栈必须按"后进先出"的规则进行操作:比如说,小学老师批改学生的作业,如果不打乱作业本的顺

随机推荐