翻译:疯狂的技术宅javascript
说明:本专栏文章首发于公众号:jingchengyideng 。程序员
栈和队列是web开发中最经常使用的两种数据结构。绝大多数用户,甚至包括web开发人员,都不知道这个惊人的事实。若是你是一个程序员,那么请听我讲两个启发性的例子:使用堆栈来组织数据,来实现文本编辑器的“撤消”操做;使用队列处理数据,实现web浏览器的事件循环处理事件(单击click、悬停hoover等)。web
等等,先想象一下咱们做为用户和程序员,天天使用栈和队列的次数,这太惊人了吧!因为它们在设计上有广泛性和类似性,我决定从这里开始为你们介绍数据结构。浏览器
在计算机科学中,栈是一种线性数据结构。若是你理解起来有困难,就像最初很是困惑的我同样,不妨这样认为:一个栈能够对数据按照顺序进行组织和管理。bash
要理解这种顺序,咱们能够把栈这种结构想象为自助餐厅的一堆盘子,当一个盘子被叠加到一堆盘子上时,原有的盘子保留了它们原来的顺序;同时,当一个新盘子被添加时,它会朝栈的底部方向堆积。每当咱们添加一个新盘子时,被称做入栈,这个新盘子处于栈的顶部,也被称做栈顶。数据结构
这个添加盘子的过程会保留每一个盘子被添加到栈中的顺序,每次从栈中取出一个盘子时也是同样的。我可能用了太多的篇幅来描述自助餐厅中的盘子是怎样被添加和删除的过程。编辑器
为了是你们理解栈更多的技术细节,让咱们回顾一下前面关于文本编辑器的“撤消”操做。每次将文本添加到文本编辑器事,该文本被压入栈中。其中第一次添加的文本表明栈的底部(栈底);最后一次的修改表示栈的顶部(栈顶)。若是用户但愿撤销最后一次修改,则删除处于栈的顶部的那段文本,这个过程能够不断重复,一直到栈中没有更多内容,这时咱们会获得一个空白文件。ide
如今咱们对栈的模型有了基本概念,下一步就要定义栈的两个操做:函数
如今让咱们开始为栈编写代码吧!
为了实现栈结构,咱们将会建立一个名为 Stack 的构造函数。栈的每一个实例都有两个属性:_size 和 _storage。
function Stack() {
this._size = 0;
this._storage = {};
}
复制代码
this._storage 属性使栈的每个实例都具备本身的用来存储数据的容器; this._size 属性反映了当前栈中数据的个数。若是建立了一个新的栈的实例,而且有一个数据被存入栈中,那么 this._size 的值将被增长到1。若是又有数据入栈,this._size 的值将增长到2。若是一个数据从栈中被取出,this._size 的值将会减小为1。
咱们须要定义能够向栈中添加(入栈)和从栈中取出(出栈)数据的方法。让咱们从添加数据开始。
(每个栈的实例都具备这个方法,因此咱们把它添加到栈结构的原型中)
咱们对这个方法有两个要求:
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对应的空间。如今咱们的数据有了顺序!
咱们已经实现了把数据送入栈中,下一步咱们要从栈中弹出(删除)数据。从栈中弹出数据并非简单的删除数据,它只删除最后一次添加的数据。
如下是这个方法的要点:
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浏览器的事件循环。当触发不一样事件时,例如单击某个按钮,点击事件将被添加到事件循环队列中,并按照它们进入队列的顺序进行处理。
如今咱们具备了队列的概念,接下来就要定义它的操做。你会注意到,队列的操做和栈很是类似。区别就在被删除的数据在什么地方。
如今让咱们开始写队列的代码吧!
在实现队列的代码中,咱们将会建立一个名为 Queue 的构造方法。接下来添加三个属性:_oldestIndex, _newestIndex, 和 _storage。在下一小节中,_oldestIndex 和 _newestIndex 的做用将变得更加清晰。
function Queue() {
this._oldestIndex = 1;
this._newestIndex = 1;
this._storage = {};
}
复制代码
如今咱们将建立队列会用到的三个方法:size(), enqueue(data), 和 dequeue(data)。我将描述每一个方法的做用,写出每一个方法的代码,而后解释这些代码。
这个方法有两个做用:
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 这两个属性 在队列中的用途。全部这一切彷佛使人困惑——到如今我仍然会偶尔以为困惑。下面的例子能够帮助我门理顺全部的逻辑。
假设咱们的熟食店有两个售票系统:
对于两个售票系统来讲,这是最难掌握的概念:当两个系统中的数字相同时,队列中的每一个客户都被处理了,队列是空的。咱们将使用下面的场景来增强这种逻辑:
如今属性 _newestindex能够告诉咱们被分配在队列中票号的最大值(键),属性 _oldestindex 能够告诉咱们最早进入队列中票号(键)。
探讨完了size(),接下来看enqueue(data)方法。
对于 enqueue 方法,有两个功能:
基于这两个功能,咱们将编写 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( )。
如下是此方法的两个功能点:
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;
}
};
复制代码
在本文中,咱们探讨了两个线性数据结构:栈和队列。栈按照顺序存储数据,并删除最后添加的数据;队列按顺序存储数据,但删除最早的添加数据。
若是这些数据结构的实现看起来微不足道,请提醒本身数据结构的用途。它们并无被设计得过于复杂,它们是用来帮助咱们组织数据的。在这种状况下,若是您发现有须要按顺序组织数据的场合,请考虑使用栈或队列。