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

第三章 栈

栈数据结构

栈是一种遵循后进先出(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,转为八进制的时候,余数为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. 开源监控zabbix的搭建

    yum-nginx-php-mysql 1.依赖安装 1.依赖 yum -y install pcre* openssl* 2.php rpm -Uvh https://mirror.webtatic ...

  2. Linux--Smba服务搭建

    Samba文件共享服务 服务功能:跨平台文件共享 1.环境部署 ip=192.168.1.50 [root@localhost Packages]# rpm -ivh samba-3.6.9-164. ...

  3. 如何安全移除dataguard和如何安全移除备库并把备库变成一个单独的数据库

    参考MOS:How To Remove Standby Database And Convert It to Standalone Database (文档 ID 2074686.1) How to ...

  4. March 22 2017 Week 12 Wednesday

    Satisfaction doesn't come from the outside, but from the inside. 满足感并非来自外界,而是来自内心. Everything that e ...

  5. 如何查找Authorization object在哪些ABAP代码里使用到

    使用事务码SUIM: 双击where-Used List->Authorization Objects->In Programs: 输入要查找的Authorization Object名称 ...

  6. Asp.net网站优化【转】

    阅读目录 开始 配置OutputCache 启用内容过期 解决资源文件升级问题 启用压缩 删除无用的HttpModule 其它优化选项 本文将介绍一些方法用于优化ASP.NET网站性能,这些方法都是不 ...

  7. hdu 6243,6247

    题意:n只狗,n个笼子,每个笼子只能有一只,求不在自己笼子的狗的数量的期望. 分析:概率是相等的,可以直接用方案数代替,k 不在自己的笼子的方案数是 n!- (n-1)!,这样的k有n个,总的方案数n ...

  8. Android——Intent,Bundle

    Intent——切换activity intent.setClass(first.this,second.class); startActivity(intent); Bundle——切换时数据传递 ...

  9. 【luogu P3709 大爷的字符串题】 题解

    题目链接:https://www.luogu.org/problemnew/show/P3709 离散化+区间众数..? #include <iostream> #include < ...

  10. IP地址获取当前地理位置(省份)的接口

    腾讯的接口是 ,返回数组 http://fw.qq.com/ipaddress 返回值 var IPData = new Array("61.135.152.194"," ...