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

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

  1. function Stack() {
    let items = [];
  2. // 向栈添加新元素
  3. this.push = function (element) {
  4. items.push(element);
  5. };
  6.  
  7. // 从栈内弹出一个元素
  8. this.pop = function () {
  9. return items.pop();
  10. };
  11.  
  12. // 返回栈顶的元素
  13. this.peek = function () {
  14. return items[items.length - 1];
  15. };
  16.  
  17. // 判断栈是否为空
  18. this.isEmpty = function () {
  19. return items.length === 0;
  20. };
  21.  
  22. // 返回栈的长度
  23. this.size = function () {
  24. return items.length;
  25. };
  26.  
  27. // 清空栈
  28. this.clear = function () {
  29. items = [];
  30. };
  31.  
  32. // 打印栈内的所有元素
  33. this.print = function () {
  34. console.log(items.toString());
  35. };
  36. }

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

  1. let stack = new Stack();
  2. console.log(stack.isEmpty()); // true
  3.  
  4. stack.push(5);
  5. stack.push(8);
  6. console.log(stack.peek()); //
  7.  
  8. stack.push(11);
  9. console.log(stack.size()); //
  10. console.log(stack.isEmpty()); // false
  11.  
  12. stack.push(15);
  13. stack.pop();
  14. stack.pop();
  15. console.log(stack.size()); //
  16. stack.print(); // 5,8
  17.  
  18. stack.clear();
  19. stack.print(); //

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

  1. class Stack {
  2. constructor () {
  3. this.items = [];
  4. }
  5.  
  6. push(element) {
  7. this.items.push(element);
  8. }
  9.  
  10. pop() {
  11. return this.items.pop();
  12. }
  13.  
  14. peek() {
  15. return this.items[this.items.length - 1];
  16. }
  17.  
  18. isEmpty() {
  19. return this.items.length === 0;
  20. }
  21.  
  22. size() {
  23. return this.items.length;
  24. }
  25.  
  26. clear() {
  27. this.items = [];
  28. }
  29.  
  30. print() {
  31. console.log(this.items.toString());
  32. }
  33. }

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

  1. let _items = Symbol();
  2.  
  3. class Stack {
  4. constructor () {
  5. this[_items] = [];
  6. }
  7.  
  8. push(element) {
  9. this[_items].push(element);
  10. }
  11.  
  12. pop() {
  13. return this[_items].pop();
  14. }
  15.  
  16. peek() {
  17. return this[_items][this[_items].length - 1];
  18. }
  19.  
  20. isEmpty() {
  21. return this[_items].length === 0;
  22. }
  23.  
  24. size() {
  25. return this[_items].length;
  26. }
  27.  
  28. clear() {
  29. this[_items] = [];
  30. }
  31.  
  32. print() {
  33. console.log(this[_items].toString());
  34. }
  35. }

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

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

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

  1. const items = new WeakMap();
  2.  
  3. class Stack {
  4. constructor () {
  5. items.set(this, []);
  6. }
  7.  
  8. push(element) {
  9. let s = items.get(this);
  10. s.push(element);
  11. }
  12.  
  13. pop() {
  14. let s = items.get(this);
  15. return s.pop();
  16. }
  17.  
  18. peek() {
  19. let s = items.get(this);
  20. return s[s.length - 1];
  21. }
  22.  
  23. isEmpty() {
  24. return items.get(this).length === 0;
  25. }
  26.  
  27. size() {
  28. return items.get(this).length;
  29. }
  30.  
  31. clear() {
  32. items.set(this, []);
  33. }
  34.  
  35. print() {
  36. console.log(items.get(this).toString());
  37. }
  38. }

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

  1. let Stack = (function () {
  2. const items = new WeakMap();
  3. class Stack {
  4. constructor () {
  5. items.set(this, []);
  6. }
  7.  
  8. push(element) {
  9. let s = items.get(this);
  10. s.push(element);
  11. }
  12.  
  13. pop() {
  14. let s = items.get(this);
  15. return s.pop();
  16. }
  17.  
  18. peek() {
  19. let s = items.get(this);
  20. return s[s.length - 1];
  21. }
  22.  
  23. isEmpty() {
  24. return items.get(this).length === 0;
  25. }
  26.  
  27. size() {
  28. return items.get(this).length;
  29. }
  30.  
  31. clear() {
  32. items.set(this, []);
  33. }
  34.  
  35. print() {
  36. console.log(items.get(this).toString());
  37. }
  38. }
  39. return Stack;
  40. })();

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

进制转换算法

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

  10 / 2 = 5,余数为0

  5 / 2 = 2,余数为1

  2 / 2 = 1,余数为0

  1 / 2 = 0, 余数为1

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

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

  1. function divideBy2(decNumber) {
  2. let remStack = new Stack();
  3. let rem, binaryString = '';
  4.  
  5. while(decNumber > 0) {
  6. rem = Math.floor(decNumber % 2);
  7. remStack.push(rem);
  8. decNumber = Math.floor(decNumber / 2);
  9. }
  10.  
  11. while(!remStack.isEmpty()) {
  12. binaryString += remStack.pop().toString();
  13. }
  14.  
  15. return binaryString;
  16. }
  17.  
  18. console.log(divideBy2(233)); //
  19. console.log(divideBy2(10)); //
  20. console.log(divideBy2(1000)); //

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

  1. function baseConverter(decNumber, base) {
  2. let remStack = new Stack();
  3. let rem, baseString = '';
  4. let digits = '0123456789ABCDEF';
  5.  
  6. while(decNumber > 0) {
  7. rem = Math.floor(decNumber % base);
  8. remStack.push(rem);
  9. decNumber = Math.floor(decNumber / base);
  10. }
  11.  
  12. while(!remStack.isEmpty()) {
  13. baseString += digits[remStack.pop()];
  14. }
  15.  
  16. return baseString;
  17. }
  18.  
  19. console.log(baseConverter(233, 2)); //
  20. console.log(baseConverter(10, 2)); //
  21. console.log(baseConverter(1000, 2)); //
  22.  
  23. console.log(baseConverter(233, 8)); //
  24. console.log(baseConverter(10, 8)); //
  25. console.log(baseConverter(1000, 8)); //
  26.  
  27. console.log(baseConverter(233, 16)); // E9
  28. console.log(baseConverter(10, 16)); // A
  29. 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亿年才能完成!

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

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

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

  1. function hanoi(plates, source, helper, dest, moves = []) {
  2. if (plates <= 0) {
  3. return moves;
  4. }
  5. if (plates === 1) {
  6. moves.push([source, dest]);
  7. } else {
  8. hanoi(plates - 1, source, dest, helper, moves);
  9. moves.push([source, dest]);
  10. hanoi(plates - 1, helper, source, dest, moves);
  11. }
  12. return moves;
  13. }

  下面是执行结果:

  1. console.log(hanoi(3, 'source', 'helper', 'dest'));
  1. [
  2. [ 'source', 'dest' ],
  3. [ 'source', 'helper' ],
  4. [ 'dest', 'helper' ],
  5. [ 'source', 'dest' ],
  6. [ 'helper', 'source' ],
  7. [ 'helper', 'dest' ],
  8. [ 'source', 'dest' ]
  9. ]

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

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

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

  1. function towerOfHanoi(plates, source, helper, dest, sourceName, helperName, destName, moves = []) {
  2. if (plates <= 0) {
  3. return moves;
  4. }
  5. if (plates === 1) {
  6. dest.push(source.pop());
  7. const move = {};
  8. move[sourceName] = source.toString();
  9. move[helperName] = helper.toString();
  10. move[destName] = dest.toString();
  11. moves.push(move);
  12. } else {
  13. towerOfHanoi(plates - 1, source, dest, helper, sourceName, destName, helperName, moves);
  14. dest.push(source.pop());
  15. const move = {};
  16. move[sourceName] = source.toString();
  17. move[helperName] = helper.toString();
  18. move[destName] = dest.toString();
  19. moves.push(move);
  20. towerOfHanoi(plates - 1, helper, source, dest, helperName, sourceName, destName, moves);
  21. }
  22. return moves;
  23. }
  24.  
  25. function hanoiStack(plates) {
  26. const source = new Stack();
  27. const dest = new Stack();
  28. const helper = new Stack();
  29.  
  30. for (let i = plates; i > 0; i--) {
  31. source.push(i);
  32. }
  33.  
  34. return towerOfHanoi(plates, source, helper, dest, 'source', 'helper', 'dest');
  35. }

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

  1. [
  2. {
  3. source: '[object Object]',
  4. helper: '[object Object]',
  5. dest: '[object Object]'
  6. },
  7. {
  8. source: '[object Object]',
  9. dest: '[object Object]',
  10. helper: '[object Object]'
  11. },
  12. {
  13. dest: '[object Object]',
  14. source: '[object Object]',
  15. helper: '[object Object]'
  16. },
  17. {
  18. source: '[object Object]',
  19. helper: '[object Object]',
  20. dest: '[object Object]'
  21. },
  22. {
  23. helper: '[object Object]',
  24. dest: '[object Object]',
  25. source: '[object Object]'
  26. },
  27. {
  28. helper: '[object Object]',
  29. source: '[object Object]',
  30. dest: '[object Object]'
  31. },
  32. {
  33. source: '[object Object]',
  34. helper: '[object Object]',
  35. dest: '[object Object]'
  36. }
  37. ]

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

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. python函数之enumerate()

    enumrate 语法: # enumerate(sequence, [start=]) 参数:# sequence -- 一个序列.迭代器或其他支持迭代对象.# start -- 下标起始位置. 使 ...

  2. K8s集群部署(三)------ Node节点部署

    之前的docker和etcd已经部署好了,现在node节点要部署二个服务:kubelet.kube-proxy. 部署kubelet(Master 节点操作) 1.二进制包准备 [root@k8s-m ...

  3. VUE-CLI3.0安装和使用echart方法

    在Vue中使用echarts的两种方式 npm webpack vue-cli echarts vue.js   准备:使用vue-cli脚手架 如果你已经有自己的项目,可以跳过这一步. npm下载v ...

  4. NOSQL—MongoDB之外的新选择

    MongoDB之外的新选择 MongoDB拥有灵活的文档型数据结构和方便的操作语法,在新兴的互联网应用中得到了广泛的部署,但对于其底层的存储引擎一直未对外开放,虽说开源却有失完整.Mongo版本3中开 ...

  5. Java SpringBoot 如何使用 IdentityServer4 作为验证学习笔记

    这边记录下如何使用IdentityServer4 作为 Java SpringBoot 的 认证服务器和令牌颁发服务器.本人也是新手,所以理解不足的地方请多多指教.另外由于真的很久没有写中文了,用词不 ...

  6. TLS示例开发-golang版本

    目录 前言 制作自签名证书 CA 服务器证书相关 客户端证书相关 证书如何验证 在浏览器中导入证书 导入证书 修改域名 golang服务端 目录 main.go 测试 参考 前言 在进行项目总结的时候 ...

  7. Java编程思想:文件读写实用工具

    import java.io.*; import java.util.ArrayList; import java.util.Arrays; public class Test { public st ...

  8. 个人永久性免费-Excel催化剂功能第76波-图表序列信息维护

    在之前开发过的图表小功能中,可以让普通用户瞬间拥有高级图表玩家所制作的精美图表,但若将这些示例数据的图表转换为自己实际所要的真实数据过程中,仍然有些困难,此篇推出后,再次拉低图表制作门槛,让真实的数据 ...

  9. 利用百度AI OCR图片识别,Java实现PDF中的图片转换成文字

    序言:我们在读一些PDF版书籍的时候,如果PDF中不是图片,做起读书笔记的还好:如果PDF中的是图片的话,根本无法编辑,做起笔记来,还是很痛苦的.我是遇到过了.我们搞技术的,当然得自己学着解决现在的痛 ...

  10. Android拨打电话权限总结

    android在6.0和6.0以上拨打电话的权限声明 /** * 打电话 * * @param phoneNumber */ protected void startCallPhone(String ...