在计算机编程中,栈是一种很常见的数据结构,它遵从后进先出(LIFO——Last In First Out)原则,新添加或待删除的元素保存在栈的同一端,称作栈顶,另一端称作栈底。在栈中,新元素总是靠近栈顶,而旧元素总是接近栈底。

  让我们来看看在JavaScript中如何实现栈这种数据结构。

function Stack() {
let items = [];
// 向栈添加新元素
this.push = function (element) {
items.push(element);
}; // 从栈内弹出一个元素
this.pop = function () {
return items.pop();
}; // 返回栈顶的元素
this.peek = function () {
return items[items.length - 1];
}; // 判断栈是否为空
this.isEmpty = function () {
return items.length === 0;
}; // 返回栈的长度
this.size = function () {
return items.length;
}; // 清空栈
this.clear = function () {
items = [];
}; // 打印栈内的所有元素
this.print = function () {
console.log(items.toString());
};
}

  我们用最简单的方式定义了一个Stack类。在JavaScript中,我们用function来表示一个类。然后我们在这个类中定义了一些方法,用来模拟栈的操作,以及一些辅助方法。代码很简单,看起来一目了然,接下来我们尝试写一些测试用例来看看这个类的一些用法。

let stack = new Stack();
console.log(stack.isEmpty()); // true stack.push(5);
stack.push(8);
console.log(stack.peek()); // stack.push(11);
console.log(stack.size()); //
console.log(stack.isEmpty()); // false stack.push(15);
stack.pop();
stack.pop();
console.log(stack.size()); //
stack.print(); // 5,8 stack.clear();
stack.print(); //

  返回结果也和预期的一样!我们成功地用JavaScript模拟了栈的实现。但是这里有个小问题,由于我们用JavaScript的function来模拟类的行为,并且在其中声明了一个私有变量items,因此这个类的每个实例都会创建一个items变量的副本,如果有多个Stack类的实例的话,这显然不是最佳方案。我们尝试用ES6(ECMAScript 6)的语法重写Stack类。

class Stack {
constructor () {
this.items = [];
} push(element) {
this.items.push(element);
} pop() {
return this.items.pop();
} peek() {
return this.items[this.items.length - 1];
} isEmpty() {
return this.items.length === 0;
} size() {
return this.items.length;
} clear() {
this.items = [];
} print() {
console.log(this.items.toString());
}
}

  没有太大的改变,我们只是用ES6的简化语法将上面的Stack函数转换成了Stack类。类的成员变量只能放到constructor构造函数中来声明。虽然代码看起来更像类了,但是成员变量items仍然是公有的,我们不希望在类的外部访问items变量而对其中的元素进行操作,因为这样会破坏栈这种数据结构的基本特性。我们可以借用ES6的Symbol来限定变量的作用域。

let _items = Symbol();

class Stack {
constructor () {
this[_items] = [];
} push(element) {
this[_items].push(element);
} pop() {
return this[_items].pop();
} peek() {
return this[_items][this[_items].length - 1];
} isEmpty() {
return this[_items].length === 0;
} size() {
return this[_items].length;
} clear() {
this[_items] = [];
} print() {
console.log(this[_items].toString());
}
}

  这样,我们就不能再通过Stack类的实例来访问其内部成员变量_items了。但是仍然可以有变通的方法来访问_items:

let stack = new Stack();
let objectSymbols = Object.getOwenPropertySymbols(stack);

  通过Object.getOwenPropertySymbols()方法,我们可以获取到类的实例中的所有Symbols属性,然后就可以对其进行操作了,如此说来,这个方法仍然不能完美实现我们想要的效果。我们可以使用ES6的WeakMap类来确保Stack类的属性是私有的:

const items = new WeakMap();

class Stack {
constructor () {
items.set(this, []);
} push(element) {
let s = items.get(this);
s.push(element);
} pop() {
let s = items.get(this);
return s.pop();
} peek() {
let s = items.get(this);
return s[s.length - 1];
} isEmpty() {
return items.get(this).length === 0;
} size() {
return items.get(this).length;
} clear() {
items.set(this, []);
} print() {
console.log(items.get(this).toString());
}
}

  现在,items在Stack类里是真正的私有属性了,但是,它是在Stack类的外部声明的,这就意味着谁都可以对它进行操作,虽然我们可以将Stack类和items变量的声明放到闭包中,但是这样却又失去了类本身的一些特性(如扩展类无法继承私有属性)。所以,尽管我们可以用ES6的新语法来简化一个类的实现,但是毕竟不能像其它强类型语言一样声明类的私有属性和方法。有许多方法都可以达到相同的效果,但无论是语法还是性能,都会有各自的优缺点。

let Stack = (function () {
const items = new WeakMap();
class Stack {
constructor () {
items.set(this, []);
} push(element) {
let s = items.get(this);
s.push(element);
} pop() {
let s = items.get(this);
return s.pop();
} peek() {
let s = items.get(this);
return s[s.length - 1];
} isEmpty() {
return items.get(this).length === 0;
} size() {
return items.get(this).length;
} clear() {
items.set(this, []);
} print() {
console.log(items.get(this).toString());
}
}
return Stack;
})();

  下面我们来看看栈在实际编程中的应用。

进制转换算法

  将十进制数字10转换成二进制数字,过程大致如下:

  10 / 2 = 5,余数为0

  5 / 2 = 2,余数为1

  2 / 2 = 1,余数为0

  1 / 2 = 0, 余数为1

  我们将上述每一步的余数颠倒顺序排列起来,就得到转换之后的结果:1010。

  按照这个逻辑,我们实现下面的算法:

function divideBy2(decNumber) {
let remStack = new Stack();
let rem, binaryString = ''; while(decNumber > 0) {
rem = Math.floor(decNumber % 2);
remStack.push(rem);
decNumber = Math.floor(decNumber / 2);
} while(!remStack.isEmpty()) {
binaryString += remStack.pop().toString();
} return binaryString;
} console.log(divideBy2(233)); //
console.log(divideBy2(10)); //
console.log(divideBy2(1000)); //

  Stack类可以自行引用本文前面定义的任意一个版本。我们将这个函数再进一步抽象一下,使之可以实现任意进制之间的转换。

function baseConverter(decNumber, base) {
let remStack = new Stack();
let rem, baseString = '';
let digits = '0123456789ABCDEF'; while(decNumber > 0) {
rem = Math.floor(decNumber % base);
remStack.push(rem);
decNumber = Math.floor(decNumber / base);
} while(!remStack.isEmpty()) {
baseString += digits[remStack.pop()];
} return baseString;
} console.log(baseConverter(233, 2)); //
console.log(baseConverter(10, 2)); //
console.log(baseConverter(1000, 2)); // console.log(baseConverter(233, 8)); //
console.log(baseConverter(10, 8)); //
console.log(baseConverter(1000, 8)); // console.log(baseConverter(233, 16)); // E9
console.log(baseConverter(10, 16)); // A
console.log(baseConverter(1000, 16)); // 3E8

  我们定义了一个变量digits,用来存储各进制转换时每一步的余数所代表的符号。如:二进制转换时余数为0,对应的符号为digits[0],即0;八进制转换时余数为7,对应的符号为digits[7],即7;十六进制转换时余数为11,对应的符号为digits[11],即B。

汉诺塔

  有关汉诺塔的传说和由来,读者可以自行百度。这里有两个和汉诺塔相似的小故事,可以跟大家分享一下。

  1. 有一个古老的传说,印度的舍罕王(Shirham)打算重赏国际象棋的发明人和进贡者,宰相西萨·班·达依尔(Sissa Ben Dahir)。这位聪明的大臣的胃口看来并不大,他跪在国王面前说:“陛下,请您在这张棋盘的第一个小格内,赏给我一粒小麦;在第二个小格内给两粒,第三格内给四粒,照这样下去,每一小格内都比前一小格加一倍。陛下啊,把这样摆满棋盘上所有64格的麦粒,都赏给您的仆人吧!”。“爱卿。你所求的并不多啊。”国王说道,心里为自己对这样一件奇妙的发明所许下的慷慨赏诺不致破费太多而暗喜。“你当然会如愿以偿的。”说着,他令人把一袋麦子拿到宝座前。计数麦粒的工作开始了。第一格内放一粒,第二格内放两粒,第三格内放四粒,......还没到第二十格,袋子已经空了。一袋又一袋的麦子被扛到国王面前来。但是,麦粒数一格接以各地增长得那样迅速,很快就可以看出,即便拿来全印度的粮食,国王也兑现不了他对西萨·班·达依尔许下的诺言了,因为这需要有18 446 744 073 709 551 615颗麦粒呀!

  这个故事其实是一个数学级数问题,这位聪明的宰相所要求的麦粒数可以写成数学式子:1 + 2 + 22 + 23 + 24 + ...... 262 + 263 

  推算出来就是:

  

  其计算结果就是18 446 744 073 709 551 615,这是一个相当大的数!如果按照这位宰相的要求,需要全世界在2000年内所生产的全部小麦才能满足。

  2. 另外一个故事也是出自印度。在世界中心贝拿勒斯的圣庙里,安放着一个黄铜板,板上插着三根宝石针。每根针高约1腕尺,像韭菜叶那样粗细。梵天在创造世界的时候,在其中的一根针上从下到上放下了由大到小的64片金片。这就是所谓的梵塔。不论白天黑夜,都有一个值班的僧侣按照梵天不渝的法则,把这些金片在三根针上移来移去:一次只能移一片,并且要求不管在哪一根针上,小片永远在大片的上面。当所有64片都从梵天创造世界时所放的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。这其实就是我们要说的汉诺塔问题,和第一个故事一样,要把这座梵塔全部64片金片都移到另一根针上,所需要的时间按照数学级数公式计算出来:1 + 2 + 22 + 23 + 24 + ...... 262 + 263 = 264 - 1 = 18 446 744 073 709 551 615

  一年有31 558 000秒,假如僧侣们每一秒钟移动一次,日夜不停,节假日照常干,也需要将近5800亿年才能完成!

  好了,现在让我们来试着实现汉诺塔的算法。

  为了说明汉诺塔中每一个小块的移动过程,我们先考虑简单一点的情况。假设汉诺塔只有三层,借用百度百科的图,移动过程如下:

  一共需要七步。我们用代码描述如下:

function hanoi(plates, source, helper, dest, moves = []) {
if (plates <= 0) {
return moves;
}
if (plates === 1) {
moves.push([source, dest]);
} else {
hanoi(plates - 1, source, dest, helper, moves);
moves.push([source, dest]);
hanoi(plates - 1, helper, source, dest, moves);
}
return moves;
}

  下面是执行结果:

console.log(hanoi(3, 'source', 'helper', 'dest'));
[
[ 'source', 'dest' ],
[ 'source', 'helper' ],
[ 'dest', 'helper' ],
[ 'source', 'dest' ],
[ 'helper', 'source' ],
[ 'helper', 'dest' ],
[ 'source', 'dest' ]
]

  可以试着将3改成大一点的数,例如14,你将会得到如下图一样的结果:

  如果我们将数改成64呢?就像上面第二个故事里所描述的一样。恐怕要令你失望了!这时候你会发现你的程序无法正确返回结果,甚至会由于超出递归调用的嵌套次数而报错。这是由于移动64层的汉诺塔所需要的步骤是一个很大的数字,我们在前面的故事中已经描述过了。如果真要实现这个过程,这个小程序恐怕很难做到了。

  搞清楚了汉诺塔的移动过程,我们可以将上面的代码进行扩充,把我们在前面定义的栈的数据结构应用进来,完整的代码如下:

function towerOfHanoi(plates, source, helper, dest, sourceName, helperName, destName, moves = []) {
if (plates <= 0) {
return moves;
}
if (plates === 1) {
dest.push(source.pop());
const move = {};
move[sourceName] = source.toString();
move[helperName] = helper.toString();
move[destName] = dest.toString();
moves.push(move);
} else {
towerOfHanoi(plates - 1, source, dest, helper, sourceName, destName, helperName, moves);
dest.push(source.pop());
const move = {};
move[sourceName] = source.toString();
move[helperName] = helper.toString();
move[destName] = dest.toString();
moves.push(move);
towerOfHanoi(plates - 1, helper, source, dest, helperName, sourceName, destName, moves);
}
return moves;
} function hanoiStack(plates) {
const source = new Stack();
const dest = new Stack();
const helper = new Stack(); for (let i = plates; i > 0; i--) {
source.push(i);
} return towerOfHanoi(plates, source, helper, dest, 'source', 'helper', 'dest');
}

  我们定义了三个栈,用来表示汉诺塔中的三个针塔,然后按照函数hanoi()中相同的逻辑来移动这三个栈中的元素。当plates的数量为3时,执行结果如下:

[
{
source: '[object Object]',
helper: '[object Object]',
dest: '[object Object]'
},
{
source: '[object Object]',
dest: '[object Object]',
helper: '[object Object]'
},
{
dest: '[object Object]',
source: '[object Object]',
helper: '[object Object]'
},
{
source: '[object Object]',
helper: '[object Object]',
dest: '[object Object]'
},
{
helper: '[object Object]',
dest: '[object Object]',
source: '[object Object]'
},
{
helper: '[object Object]',
source: '[object Object]',
dest: '[object Object]'
},
{
source: '[object Object]',
helper: '[object Object]',
dest: '[object Object]'
}
]

  栈的应用在实际编程中非常普遍,下一章我们来看看另一种数据结构:队列。

JavaScript数据结构——栈的实现与应用的更多相关文章

  1. JavaScript数据结构——栈和队列

    栈:后进先出(LIFO)的有序集合 队列:先进先出(FIFO)的有序集合 --------------------------------------------------------------- ...

  2. JavaScript数据结构——栈的实现

    栈(stack)是一种运算受限的线性表.栈内的元素只允许通过列表的一端访问,这一端被称为栈顶,相对地,把另一端称为栈底.装羽毛球的盒子是现实中常见的栈例子.栈被称为一种后入先出(LIFO,last-i ...

  3. javascript数据结构——栈

    栈是一种高效的数据结构,数据只能在栈顶添加或删除,所以这样操作很快,也很容易实现.栈的使用遍布程序语言实现的方方面面,从表达式求值到处理函数调用.接下来,用JavaScript实现一个栈的数据结构. ...

  4. javascript数据结构-栈

    github博客地址 栈(stack)又名堆栈,它是一种运算受限的线性表.遵循后进先出原则,像垃圾桶似的.功能实现依然按照增删改查来进行,内部数据存储可以借用语言原生支持的数组. 栈类 functio ...

  5. JavaScript数据结构——队列的实现

    前面楼主简单介绍了JavaScript数据结构栈的实现,http://www.cnblogs.com/qq503665965/p/6537894.html,本次将介绍队列的实现. 队列是一种特殊的线性 ...

  6. JavaScript数据结构——字典和散列表的实现

    在前一篇文章中,我们介绍了如何在JavaScript中实现集合.字典和集合的主要区别就在于,集合中数据是以[值,值]的形式保存的,我们只关心值本身:而在字典和散列表中数据是以[键,值]的形式保存的,键 ...

  7. JavaScript数据结构——图的实现

    在计算机科学中,图是一种网络结构的抽象模型,它是一组由边连接的顶点组成.一个图G = (V, E)由以下元素组成: V:一组顶点 E:一组边,连接V中的顶点 下图表示了一个图的结构: 在介绍如何用Ja ...

  8. javascript数据结构

    学习数据结构非常重要.首要原因是数据结构和算法可以很高效的解决常见问题.作为前端,通过javascript学习数据结构和算法要比学习java和c版本容易的多. 在讲数据结构之前我们先了解一下ES6的一 ...

  9. 学习javascript数据结构(一)——栈和队列

    前言 只要你不计较得失,人生还有什么不能想法子克服的. 原文地址:学习javascript数据结构(一)--栈和队列 博主博客地址:Damonare的个人博客 几乎所有的编程语言都原生支持数组类型,因 ...

随机推荐

  1. JSP数据交互(二)

    1.application内置对象 application实现用户之间的数据共享 void setAttribute(String key,Object value) 以key/value的形式保存对 ...

  2. Requests方法 -- cookie绕过验证码登录操作

    前言有些登录的接口会有验证码:短信验证码,图形验证码等,这种登录的话验证码参数可以从后台获取的(或者查数据库最直接).获取不到也没关系,可以通过添加 cookie 的方式绕过验证码. 1.这里以登录博 ...

  3. Python 3网络爬虫开发实战中文 书籍软件包(原创)

    Python 3网络爬虫开发实战中文 书籍软件包(原创) 本书书籍软件包为本人原创,想学爬虫的朋友你们的福利来了.软件包包含了该书籍所需的所有软件. 因为软件导致这个文件比较大,所以百度网盘没有加速的 ...

  4. asyncio系列之sleep()实现

    先来看个例子,自己实现的模拟耗时操作 例1 import types import select import time import socket import functools class Fu ...

  5. shell 循环打印某一天后的n天的日期

    #!/bin/bash start_date="2017-10-26" for i in `seq 0 30` do num=$((${i}+1)) datatime=`date ...

  6. Flutter学习笔记(4)--Dart函数

    如需转载,请注明出处:Flutter学习笔记(4)--Dart函数 Dart是一个面向对象的语言,所以函数也是对象,函数属于Function对象,函数可以像参数一样传递给其他函数,这样便于做回调处理: ...

  7. Spring 注解编程之注解属性别名与覆盖

    前两篇文章咱聊了深入了解了 Spring 注解编程一些原理,这篇文章我们关注注解属性方法,聊聊 Spring 为注解的带来的功能,属性别名与覆盖. 注解属性方法 在进入了解 Spring 注解属性功能 ...

  8. kuangbin专题 专题一 简单搜索 Dungeon Master POJ - 2251

    题目链接:https://vjudge.net/problem/POJ-2251 题意:简单的三维地图 思路:直接上代码... #include <iostream> #include & ...

  9. 字符串翻转demo

    1.利用char数组 public class stringfanzhaun { public static void main(String[] args) { String str="1 ...

  10. redis分布式锁的问题和解决

    分布式锁 在分布式环境中,为了保证业务数据的正常访问,防止出现重复请求的问题,会使用分布式锁来阻拦后续请求.具体伪代码如下: public void doSomething(String userId ...