有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列。

第三章 栈

栈数据结构

栈是一种遵循后进先出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一端就叫做栈底。在栈里, 新元素都靠近栈顶,旧元素都接近栈底。

栈也被用在编程语言的编译器和内存中保存变量、方法调用等。

创建栈

  1. 先声明这个类
  1. function Stack(){
  2. // 各种属性和方法的声明
  3. }
  1. 选择数组这种数据结构来保存栈里的元素
  1. let items = [];
  1. 为栈声明一些方法

    • push(element(s)): 添加一个(或者几个)新元素到栈顶
    • pop():移除栈顶的元素,同时返回被移除的元素
    • peek():返回栈顶的元素,不会对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)
    • isEmpty():如果栈里没有任何元素的就返回true,否则就返回false.
    • clear():移除栈里的所有元素
    • size():返回栈里的元素个数,这个方法和数组的length属性很类似。

向栈添加元素

我们要实现的第一个方法是 push,这个方法负责向栈里添加新元素,该方法只添加元素到栈顶,也就是栈的末尾。

  1. this.push = function(element){
  2. return items.push(element);
  3. }

只能用 push 和 pop 方法添加和删除栈中元素,这样一来,我们的栈就自然遵从了 LIFO 原则。

向栈移除元素

我们要实现的第一个方法是 pop,这个方法主要用来移除栈里的元素。栈遵从 LIFO 原则,因此移出的是最后添加进去的元素。栈的 pop 方法可以这么写

  1. this.pop = function(){
  2. return items.pop();
  3. }

只能用 push 和 pop 方法添加和删除栈中元素,这样一来,我们的栈就自然遵从了 LIFO 原则。

查看栈顶元素

现在为类实现一些额外的辅助方法,如果想知道栈里最后添加的元素是什么,可以用 peek 方法,这个方法将返回栈顶的元素。

  1. this.peek = function(){
  2. return items[items.length-1];
  3. }

因为类内部是用数组保存元素的,所以访问数组的最后一个元素可以用 length - 1

检查栈是否为空

isEmpty ,如果栈为空的话就返回true,否则就返回false

  1. this.isEmpty = function(){
  2. return items.length == 0;
  3. }

类似于数组的 length 属性,我们也能实现栈的 length,对于集合,最好用 size 代替 length。因为栈的内部使用数组保存元素,所以能简单地返回栈的长度。

  1. this.size = function(){
  2. return items.length;
  3. }

清空和打印栈元素

实现 clear 方法。clear 方法用来移除栈里所有的元素,把栈清空。实现这个方法最简单的方式是

  1. this.clear = function(){
  2. items = [];
  3. return null;
  4. }

打印出来栈里面的内容,通过实现辅助方法 print 来实现。

  1. this.print = function(){
  2. console.log(items.toString());
  3. }

实例

  1. function Stack(){
  2. let items = [];
  3. this.push = function(element){
  4. return items.push(element);
  5. }
  6. this.pop = function(){
  7. return items.pop();
  8. }
  9. this.peek = function(){
  10. return items[items.length-1];
  11. }
  12. this.isEmpty = function(){
  13. return items.length == 0;
  14. }
  15. this.size = function(){
  16. return items.length;
  17. }
  18. this.clear = function(){
  19. items = [];
  20. }
  21. this.print = function(){
  22. console.log(items.toString());
  23. }
  24. }
  25. let stack = new Stack();
  26. console.log(stack.isEmpty()); // true 判断是否为空
  27. stack.push(5); // 往栈里添加元素 5
  28. stack.push(8); // 往栈里添加元素 8
  29. console.log(stack.peek()); // 查看最后一个元素 8
  30. stack.push(11); // 往栈里添加元素 11
  31. console.log(stack.size()); // 3 输出栈的元素个数
  32. console.log(stack.isEmpty()); // false 判断是否为空
  33. stack.push(15); // 往栈里添加元素 15
  34. stack.print(); // 5,8,11,15 输出栈里的元素

下面是流程图

ECMAScript6 和 Stack 类

创建了一个可以当做类来使用的 Stack 函数。JavaScript 函数都有构造函数,可以用来模拟类的行为。我们声明一个私有的 items变量,它只能被 Stack 函数/类访问。然而,这个方法为每个类的实例都创建了一个 items 变量的副本。因此如果要创建多个 Stack实例,就不太适合。我们可以尝试用 ES6语法来声明 Stack 类。

用 ES6 声明 Stack 类

  1. class Stack{
  2. constructor(){
  3. this.items = []; // {1}
  4. }
  5. push(elememt){
  6. this.items.push(element);
  7. }
  8. // 其他方法
  9. }

只是用 ES6 的简化语法把 Stack 函数转换成 Stack 类。这种方法不能像其他语言(Java、C++、C#)一样直接在类里面声明变量,只能在类的构造函数 constructor 里声明,在类的其他函数里用 this.nameofVariable 就可以引用这个变量。

尽管代码看起来更加简洁、更漂亮,变量 items 却是公共的。ES6 类是基于原型的。虽然基于原型的类比基于函数的类更节省内存,也更适合创建多个实例,却不能够声明私有属性(变量)或方法。而且,在这种情况下,我们希望 Stack 类的用户只能访问暴露给类的方法。否则,就有可能从栈的中间移除元素(因为我们用数组来存储其值),这不是我们希望看到的。

用ES6的限定作用域 Symbol 实现类

ES6 新增了一种叫做 Symbol 的基本类型,它是不可变的,可以用作对象的属性。

  1. let _items = Symbol(); // 声明了 Symbol 类型的变量
  2. class Stack{
  3. constructor(){
  4. this[_items] = [] // 要访问 _items,只需把所有的 this.items都换成 this.[_items]
  5. }
  6. push(element){
  7. return this[_items].push(element);
  8. }
  9. pop (){
  10. return this[_items].pop();
  11. }
  12. peek (){
  13. return this[_items][this[_items].length-1];
  14. }
  15. isEmpty (){
  16. return this[_items].length == 0;
  17. }
  18. size (){
  19. return this[_items].length;
  20. }
  21. clear (){
  22. this[_items] = [];
  23. }
  24. print (){
  25. console.log(this[_items].toString());
  26. }
  27. }

这种方法创建了一个假的私有属性,因为ES6 新增的Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbols 属性。下面是一个破坏 Stack 类的例子

  1. let stack = new Stack();
  2. stack.push(5);
  3. stack.push(8);
  4. let objectSymbols = Object.getOwnPropertySymbols(stack);
  5. console.log(objectSymbols.length); // 1
  6. console.log(objectSymbols); // [Symbol()]
  7. console.log(objectSymbols[0]); // Symbol()
  8. stack[objectSymbols[0]].push(1);
  9. stack.print(); // 5,8,1

很明显可以通过访问 stack[objectSymbol[0]] 得到 _items。并且 _items属性是一个数组,可以进行任意的数组操作,比如从中间删除或者是添加元素。我们操作的是栈,不应该有这种行为出现。

用ES6类的 WeakMap 实现类

有一种数据类型可确保属性是私有的,这就是 WeakMap。后面会深入探讨 Map 这种数据结构,现在只需要知道 WeakMap 可以存储键值对,其中键是对象,值可以是任意数据类型。

如果使用 WeakMap 来存储 items 变量,那么 Stack 类是这样的

  1. const items = new WeakMap(); // 声明了一个 WeakMap 类型的变量 items
  2. class Stack{
  3. constructor(){
  4. items.set(this, []) // 在 constructor 中,以this(Stack类自己引用)为键,把代表栈的数组存入 items
  5. }
  6. push(element){
  7. let s = items.get(this);
  8. s.push(element);
  9. }
  10. pop (){
  11. let s = items.get(this);
  12. let r = s.pop();
  13. return r;
  14. }
  15. peek (){
  16. let s = items.get(this);
  17. return s[s.length-1];
  18. }
  19. isEmpty (){
  20. let s = items.get(this);
  21. return s.length == 0;
  22. }
  23. size (){
  24. let s = items.get(this);
  25. let r = s.length
  26. return r;
  27. }
  28. clear (){
  29. items.set(this, [])
  30. }
  31. print (){
  32. let s = items.get(this);
  33. console.log(s.toString());
  34. }
  35. }

现在 items 在 Stack 类里是真正的私有属性了,但是还有一件事要做, items 现在仍然是在 Stack 类以外声明的,因此任何谁都可以改动它。我们可以用一个闭包(外层函数)把 Stack 类包起来,这样就可以在这个函数里访问 WeakMap

  1. let stack = (function(){
  2. const items = new WeakMap();
  3. class Stack {
  4. constructor(){
  5. items.set(this, []);
  6. }
  7. // 其他方法
  8. }
  9. return Stack; // 当 Stack 函数里的构造函数被调用时,会返回 Stack 类的一个实例。
  10. })()

现在,Stack 类有一个名为 items 的私有属性。然后用这种方法的话,扩展类无法继承其属性。将其与最开始用 function 实现的 Stack 类来做个比较,我们会发现一些相似之处。

事实上,尽管 ES6 引入了类的语法,我们仍然不能像在其他编程语言中一样声明私有属性或方法。有很多种方法都可以达到相同的效果,但无论是语法还是性能,这些方法都有各自的缺点和优点。

用栈解决问题

栈的实际应用非常广泛。在回溯问题中,它可以存储访问过的任务或是路径、撤销的操作。Java 和 C# 用栈来存储变量和方法调用,特别是处理递归算法时,有可能抛出一个栈溢出异常(stack overflow)

下面,学习使用栈的三个最著名的算法实例。首先是十进制转二进制的问题,以及任意进制转换的算法,然后是平衡圆括号问题,最后,会学习栈解决汉诺塔的问题。

从十进制到二进制

计算科学中,二进制非常重要,因为计算机里的所有内容都是用二进制数字表示(0和1)。没有十进制和二进制相互转化的能力,与计算机交流就很困难。要把十进制化成十进制,将该十进制数字和2整除,直到结果为0为止。

实例:数字10转为二进制的数字。

  1. function divideBy2(decNumber){
  2. var remStack = new Stack(),
  3. rem,
  4. binaryString = '';
  5. while(decNumber > 0){
  6. rem = Math.floor(decNumber % 2); // 拿到被2整除的余数
  7. remStack.push(rem);
  8. decNumber = Math.floor(decNumber / 2) // 拿到被2整除的整数
  9. }
  10. while (! remStack.isEmpty()){
  11. binaryString += remStack.pop().toString();
  12. }
  13. return binaryString;
  14. }
  15. console.log(divideBy2(10)); // 1010
  16. console.log(divideBy2(233)); // 11101001
  17. console.log(divideBy2(100)); // 11101001

JavaScript有数字类型,但是不会区分究竟是整数还是浮点数,使用 Math.floor 让除法只返回整数部分。

进制转换算法

可以传入任意进制的基数作为参数

  1. function baseConverter(decNumber, base){
  2. var remStack = new Stack(),
  3. rem,
  4. baseString = '',
  5. digits = '0123456789ABCDEF';
  6. while(decNumber > 0){
  7. rem = Math.floor(decNumber % base); // 拿到被base整除的余数
  8. remStack.push(rem);
  9. decNumber = Math.floor(decNumber / base) // 拿到被base整除的整数
  10. }
  11. while (! remStack.isEmpty()){
  12. baseString += digits[remStack.pop()];
  13. }
  14. return baseString;
  15. }
  16. console.log(baseConverter(100345,2)); // 11000011111111001
  17. console.log(baseConverter(100345, 8)); // 303771
  18. console.log(baseConverter(100345, 16)); // 187F9

需要改动的地方:在将十进制转为二进制的时候,余数是0或者1,转为八进制的时候,余数为07,同理16进制是09加上A~F。所以要做个转换,通过定义 digits ,digits[remStack.pop()] 来实现转化。

小结

通过这一章,学习了栈这一数据结构的相关内容。可以用代码自己实现栈,还讲解了栈里面的相关方法。

书籍链接: 学习JavaScript数据结构与算法

为什么我要放弃javaScript数据结构与算法(第三章)—— 栈的更多相关文章

  1. 为什么我要放弃javaScript数据结构与算法(第九章)—— 图

    本章中,将学习另外一种非线性数据结构--图.这是学习的最后一种数据结构,后面将学习排序和搜索算法. 第九章 图 图的相关术语 图是网络结构的抽象模型.图是一组由边连接的节点(或顶点).学习图是重要的, ...

  2. 为什么我要放弃javaScript数据结构与算法(第二章)—— 数组

    第二章 数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构.JavaScript里也有数组类型,虽然它的第一个版本并没有支持数组.本章将深入学习数组数据结构和它的能力. 为什么 ...

  3. 为什么我要放弃javaScript数据结构与算法(第一章)—— JavaScript简介

    数据结构与算法一直是我算比较薄弱的地方,希望通过阅读<javaScript数据结构与算法>可以有所改变,我相信接下来的记录不单单对于我自己有帮助,也可以帮助到一些这方面的小白,接下来让我们 ...

  4. 重读《学习JavaScript数据结构与算法-第三版》- 第4章 栈

    定场诗 金山竹影几千秋,云索高飞水自流: 万里长江飘玉带,一轮银月滚金球. 远自湖北三千里,近到江南十六州: 美景一时观不透,天缘有分画中游. 前言 本章是重读<学习JavaScript数据结构 ...

  5. 重读《学习JavaScript数据结构与算法-第三版》- 第5章 队列

    定场诗 马瘦毛长蹄子肥,儿子偷爹不算贼,瞎大爷娶个瞎大奶奶,老两口过了多半辈,谁也没看见谁! 前言 本章为重读<学习JavaScript数据结构与算法-第三版>的系列文章,主要讲述队列数据 ...

  6. 为什么我要放弃javaScript数据结构与算法(第十一章)—— 算法模式

    本章将会学习递归.动态规划和贪心算法. 第十一章 算法模式 递归 递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题.递归通常涉及函数调用自身. 递归函数是像下面能够直接调用自身的 ...

  7. 为什么我要放弃javaScript数据结构与算法(第十章)—— 排序和搜索算法

    本章将会学习最常见的排序和搜索算法,如冒泡排序.选择排序.插入排序.归并排序.快速排序和堆排序,以及顺序排序和二叉搜索算法. 第十章 排序和搜索算法 排序算法 我们会从一个最慢的开始,接着是一些性能好 ...

  8. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  9. 为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表

    本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构. 集合.字典和散列表可以存储不重复的值.在集合中,我们感兴趣的是每个值本身,并把它作为主要元素.而字典和散列表中都是用 [键,值]的形式来 ...

  10. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

随机推荐

  1. jquery在IE8上使用find的问题

    有一个字符串,其中是一个XML文件的内容,但是使用find方法老是不正确(IE8,其他浏览器如Chrome.Firefox),代码如下: var xml="<ServiceResult ...

  2. ZT 困难是什么?困

    困难是什么?困难就是摆在我们面前的山峰,需要我们去翻越;困难就是摆阻碍我们前行的巨浪,需要我们扬帆劈刀斩浪航行:困难就是我们眼前所下的暴风雨,要坚信暴风雨过后会有阳光和彩虹. 其实困难并不可怕,怕的就 ...

  3. ZT onActivityResult在android中的用法

    onActivityResult在android中的用法 举例说我想要做的一个事情是,在一个主界面(主Activity)上能连接往许多不同子功能模块(子Activity上去),当子模块的事情做完之后就 ...

  4. DeepQA websocket 并发测试

    var client = new Array(); var W3CWebSocket = new Array(); var concurrent = 2; for (var i = 0; i < ...

  5. c++利用互斥锁实现读写锁

    很简单就是在读的时候把写的锁锁住就好了 class readwrite_lock { public: readwrite_lock() : read_cnt(0) { } void readLock( ...

  6. mxnet数据操作

    # coding: utf-8 # In[2]: from mxnet import nd # In[3]: x = nd.arange(12) x # In[4]: x.shape,x.size # ...

  7. Tinkoff Challenge - Final Round (ABC)

    A题:从两个保安中间那钞票 #include <bits/stdc++.h> using namespace std; int main() { int a,b,c; scanf(&quo ...

  8. Ubuntu不支持rpm安装软件解决方法

    Ubuntu不支持rpm安装软件解决方法 以前经常使用的是RedHat Linux,习惯使用rpm方法安装软件.最近发现Ubuntu系统居然不支持rpm方法安装软件,提示信息如下: root@root ...

  9. PHP设计模式——装饰器模式

    <?php /** * 装饰器模式 * 如果已有对象的部分内容或功能发生变化,但是不需要修改原始对象的结构,应使用装饰器模式 * * 为了在不修改对象结构的前提下对现有对象的内容或功能稍加修改, ...

  10. logback.xml常用配置

    一.logback的介绍 Logback是由log4j创始人设计的又一个开源日志组件.logback当前分成三个模块:logback-core,logback- classic和logback-acc ...