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

第三章 栈

栈数据结构

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

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

创建栈

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

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

向栈添加元素

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

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

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

向栈移除元素

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

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

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

查看栈顶元素

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

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

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

检查栈是否为空

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

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

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

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

清空和打印栈元素

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

this.clear = function(){
    items = [];
    return null;
}

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

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

实例

function Stack(){
        let items = [];
        this.push = function(element){
            return 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());
        }
    }
let stack = new Stack();
console.log(stack.isEmpty()); // true 判断是否为空
stack.push(5); // 往栈里添加元素 5
stack.push(8); // 往栈里添加元素 8
console.log(stack.peek()); // 查看最后一个元素 8
stack.push(11); // 往栈里添加元素 11
console.log(stack.size()); // 3 输出栈的元素个数
console.log(stack.isEmpty()); // false 判断是否为空
stack.push(15); // 往栈里添加元素 15
stack.print(); // 5,8,11,15 输出栈里的元素

下面是流程图

ECMAScript6 和 Stack 类

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

用 ES6 声明 Stack 类

class Stack{
    constructor(){
        this.items = []; // {1}
    }
    push(elememt){
        this.items.push(element);
    }
    // 其他方法
}

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

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

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

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

let _items = Symbol(); // 声明了 Symbol 类型的变量
class Stack{
    constructor(){
        this[_items] = [] // 要访问 _items,只需把所有的 this.items都换成 this.[_items]
    }
    push(element){
        return 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 新增的Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbols 属性。下面是一个破坏 Stack 类的例子

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

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

用ES6类的 WeakMap 实现类

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

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

const items = new WeakMap(); // 声明了一个 WeakMap 类型的变量 items
class Stack{
    constructor(){
        items.set(this, []) // 在 constructor 中,以this(Stack类自己引用)为键,把代表栈的数组存入 items
    }
    push(element){
        let s = items.get(this);
        s.push(element);
    }
    pop (){
        let s = items.get(this);
        let r = s.pop();
        return r;
    }
    peek (){
        let s = items.get(this);
        return s[s.length-1];
    }
    isEmpty (){
        let s = items.get(this);
        return s.length == 0;
    }
    size (){
        let s = items.get(this);
        let r = s.length
        return r;
    }
    clear (){
        items.set(this, [])
    }
    print (){
        let s = items.get(this);
        console.log(s.toString());
    }
}

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

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

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

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

用栈解决问题

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

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

从十进制到二进制

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

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

function divideBy2(decNumber){
    var remStack = new Stack(),
    rem,
    binaryString = '';
    while(decNumber > 0){
        rem = Math.floor(decNumber % 2); // 拿到被2整除的余数
        remStack.push(rem);
        decNumber = Math.floor(decNumber / 2) // 拿到被2整除的整数
    }
    while (! remStack.isEmpty()){
        binaryString += remStack.pop().toString();
    }
    return binaryString;
}

console.log(divideBy2(10)); // 1010
console.log(divideBy2(233)); // 11101001
console.log(divideBy2(100)); // 11101001

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

进制转换算法

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

function baseConverter(decNumber, base){
    var remStack = new Stack(),
    rem,
    baseString = '',
    digits = '0123456789ABCDEF';
    while(decNumber > 0){
        rem = Math.floor(decNumber % base); // 拿到被base整除的余数
        remStack.push(rem);
        decNumber = Math.floor(decNumber / base) // 拿到被base整除的整数
    }
    while (! remStack.isEmpty()){
        baseString += digits[remStack.pop()];
    }
    return baseString;
}
console.log(baseConverter(100345,2)); // 11000011111111001
console.log(baseConverter(100345, 8)); // 303771
console.log(baseConverter(100345, 16)); // 187F9

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

小结

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  9. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  10. 为什么我要放弃javaScript数据结构与算法(第四章)—— 队列

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第四章 队列 队列数据结构 队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序 ...

随机推荐

  1. Linux 查找进程运行位置

    1.通过ps或者top命令查看运行的进程的pid ps -aux|grep php-fpm #或者 top 2. 获取进程的pid后,然后使用命令ls -l /proc/${pid},这个命令可以列出 ...

  2. [Android]使用Kotlin+Anko开发Android(一)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4800656.html  Kotlin是由JetBrains开发 ...

  3. phpcms使用细节

    1.在模板中使用php语句 <?php for ($i=0; $i < 10; $i++) {     echo $i."#######<br>"; }?& ...

  4. mysql 指定端口

    mysql -P3307 -uemove -h180. -p #-P是指定端口

  5. 【转载】linux内核笔记之进程地址空间

    原文:linux内核笔记之进程地址空间 进程的地址空间由允许进程使用的全部线性地址组成,在32位系统中为0~3GB,每个进程看到的线性地址集合是不同的. 内核通过线性区的资源(数据结构)来表示线性地址 ...

  6. Java——关于String(字符串)

     String s = "abc";//创建一个字符串对象在常量池中. String s2 = new String("abc");//创建两个对象   一 ...

  7. 【实习记】2014-09-24万事达卡bin查询项目总结

            8月28号,接到这个问题:现有前缀查询速度较慢,改进此知值求区间问题. 一开始没想到用二分法,更没有想到这个项目用了一个月,这一个月里,我学习并使用了middle框架写出了server ...

  8. Java位运算在程序设计中的使用:位掩码(BitMask)

    在Java中,位运算符有很多,例如与(&).非(~).或(|).异或(^).移位(<<和>>)等.这些运算符在日常编码中很少会用到. 在下面的一个例子中,会用到位掩码( ...

  9. 新手之VM下安装centos版本Linux系统完整版!

    一.安装必备软件 1:下载好VM workstations虚拟机 2:下载好你要安装的centos版本. 如果没有,请自己先百度下载好~或者找我要. 二.开始安装 VM workstation部分 1 ...

  10. STM32 基于定时器的PWM发生器

    脉冲宽度调制(PWM),是英文"Pulse Width Modulation" 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术.简单一点,就 ...