学习数据结构和算法十分重要。首要缘由是数据结构和算法能够很高效地解决常见问题,这对从此的代码质量相当重要(也包括性能,要是用了不恰当的数据结构或算法,极可能会产生性能问题)。其次,对于计算机科学,算法是最基础的概念。数组是计算机科学中最经常使用的数据结构,咱们知道,能够在数组的任意位置上删除或添加元素。然而,有时候还须要一种在添加或删除元素时有更多控制的数据结构。有两种数据结构相似于数组,但在添加和删除元素时更为可控。它们就是栈和队列。本文将详细介绍栈git
栈是一种听从后进先出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称做栈顶,另外一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。算法
在现实生活中也能发现不少栈的例子。例如,下图里的一摞书或者餐厅里堆放的盘子编程
栈也被用在编程语言的编译器和内存中保存变量、方法调用等数组
下面将建立一个类来表示栈,先声明这个类:数据结构
function Stack() { //各类属性和方法的声明 }
使用一种数据结构来保存栈里的元素。能够选择数组:闭包
let items = [];
接下来,为栈声明一些方法数据结构和算法
push(element(s)):添加一个(或几个)新元素到栈顶
pop():移除栈顶的元素,同时返回被移除的元素
peek():返回栈顶的元素,不对栈作任何修改(这个方法不会移除栈顶的元素,仅仅返回它)
isEmpty():若是栈里没有任何元素就返回true,不然返回false
clear():移除栈里的全部元素
size():返回栈里的元素个数。这个方法和数组的length属性很相似
【push】编程语言
push方法负责往栈里添加新元素,有一点很重要:该方法只添加元素到栈顶,也就是栈的末尾ide
由于使用了数组来保存栈里的元素,因此能够数组的push方法来实现函数
this.push = function(element){ items.push(element); };
【pop】
接着来实现pop方法。这个方法主要用来移除栈里的元素。栈听从LIFO原则,所以移出的是最后添加进去的元素。所以,能够用数组的pop方法
this.pop = function(){ return items.pop(); };
只能用push和pop方法添加和删除栈中元素,这样一来,栈天然就听从了LIFO原则
【peek】
如今,为类实现一些额外的辅助方法。若是想知道栈里最后添加的元素是什么,能够用peek方法。这个方法将返回栈顶的元素:
this.peek = function(){ return items[items.length-1]; };
由于类内部是用数组保存元素的,因此访问数组的最后一个元素能够用 length - 1
在上图中,有一个包含三个元素的栈,所以内部数组的长度就是3。数组中最后一项的位置是2,length - 1(3 -1)正好是2
【isEmpty】
下面要实现的方法是 isEmpty,若是栈为空的话将返回true,不然就返回false:
this.isEmpty = function(){ return items.length == 0; };
使用isEmpty方法,能简单地判断内部数组的长度是否为0
【size】
相似于数组的length属性,也能实现栈的length。对于集合,最好用size代替length。由于栈的内部使用数组保存元素,因此能简单地返回栈的长度:
this.size = function(){ return items.length; };
【clear】
最后来实现clear方法。clear方法用来移除栈里全部的元素,把栈清空。实现这个方法最简单的方式是:
this.clear = function(){ items = []; };
另外也能够屡次调用pop方法,把数组中的元素所有移除,这样也能实现clear方法
栈已经实现。经过一个例子来应用它,为了检查栈里的内容,咱们来实现一个辅助方法,叫print。它会把栈里的元素都输出到控制台:
this.print = function(){ console.log(items.toString()); };
这样,咱们就完整建立了栈!
栈的完整代码以下
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()); }; this.toString = function(){ return items.toString(); }; }
下面来学习如何使用Stack类。 首先,须要初始化Stack类。而后,验证一下栈是否为空(输出是true,由于尚未往栈里添加元素)
var stack = new Stack(); console.log(stack.isEmpty()); //输出为true
接下来,往栈里添加一些元素(能够添加任意类型的元素)
stack.push(5); stack.push(8);
若是调用peek方法,将会输出8,由于它是往栈里添加的最后一个元素:
console.log(stack.peek());//输出8
再添加一个元素:
stack.push(11); console.log(stack.size()); //输出3 console.log(stack.isEmpty()); //输出false
咱们往栈里添加了11。若是调用size方法,输出为3,由于栈里有三个元素(五、8和11)。 若是调用isEmpty方法,会看到输出了false(由于栈里有三个元素,不是空栈)。最后, 咱们再添加一个元素:
stack.push(15);
下图描绘了目前为止咱们对栈的操做,以及栈的当前状态:
而后,调用两次pop方法从栈里移除2个元素:
stack.pop(); stack.pop(); console.log(stack.size()); //输出2 stack.print(); //输出[5, 8]
在两次调用pop方法前,咱们的栈里有四个元素。调用两次后,如今栈里仅剩下5和8了。下图描绘这个过程的执行:
下面来花点时间分析一下代码,看看是否能用ES6的新功能来改进
咱们建立了一个能够看成类来使用的Stack函数。JS函数都有构造函数,能够用来模拟类的行为。咱们声明了一个私有的items变量,它只能被Stack函数/类访问。然而,这个方法为每一个类的实例都建立一个items变量的副本。所以,若是要建立多个Stack实例,它就不太适合了
下面用ES6新语法来声明Stack类
class Stack { constructor () { this.items = []; } push(element){ this.items.push(element); } //其余方法 }
咱们只是用ES6的简化语法把Stack函数转换成Stack类。这种方法不能像其余语言(Java、C++、C#)同样直接在类里面声明变量,只能在类的构造函数constructor里声明,在类的其余函数里用this.items就能够引用这个变量
尽管代码看起来更简洁、更漂亮,变量items倒是公共的。ES6的类是基于原型的,虽然基于原型的类比基于函数的类更节省内存,也更适合建立多个实例,却不能声明私有属性(变量)或方法。并且,在这种状况下,咱们但愿Stack类的用户只能访问暴露给类的方法。不然,就有可能从栈的中间移除元素(由于咱们用数组来存储其值),这不是咱们但愿看到的
ES6语法有没有其余方法来建立私有属性呢?
【Symbol】
ES6新增了一种叫做Symbol的基本类型,它是不可变的,能够用做对象的属性。看看怎么用它来在Stack类中声明items属性
let _items = Symbol(); //{1} class Stack { constructor () { this[_items] = []; //{2} } //Stack方法 }
在上面的代码中,咱们声明了Symbol类型的变量_items(行{1}),在类的constructor函数中初始化它的值(行{2})。要访问_items,只需把全部的this.items都换成this[_items]
这种方法建立了一个假的私有属性,由于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[objectSymbols[0]]是能够获得_items的。而且,_items属性是一个数组,能够进行任意的数组操做,好比从中间删除或添加元素。咱们操做的是栈,不该该出现这种行为
【WeakMap】
有一种数据类型能够确保属性是私有的,这就是WeakMap。WeakMap能够存储键值对,其中键是对象,值能够是任意数据类型。
若是用WeakMap来存储items变量,Stack类就是这样的:
const items = new WeakMap(); //{1} class Stack { constructor () { items.set(this, []); //{2} } push(element) { let s = items.get(this); //{3} s.push(element); } pop() { let s = items.get(this); let r = s.pop(); return r; } //其余方法 }
行{1},声明一个WeakMap类型的变量items。行{2},在constructor中,以this(Stack类本身的引用)为键,把表明栈的数组存入items。行{3},从WeakMap中取出值,即以this为键(行{2}设置的)从items中取值
如今知道,items在Stack类里是真正的私有属性了,但还有一件事要作。items如今仍然是在Stack类之外声明的,所以谁均可以改动它。要用一个闭包(外层函数)把Stack类包起来,这样就只能在这个函数里访问WeakMap:
let Stack = (function () { const items = new WeakMap(); class Stack { constructor () { items.set(this, []); } //其余方法 } return Stack; //{5} })();
当Stack函数里的构造函数被调用时,会返回Stack类的一个实例(行{5})
如今,Stack类有一个名为items的私有属性。虽然它很丑陋,但毕竟实现了私有属性。然而,用这种方法的话,扩展类没法继承私有属性。鱼与熊掌不可兼得
栈的完整代码以下
let Stack3 = (function () { const items = new WeakMap(); class Stack3 { constructor () { items.set(this, []); } 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(){ return items.get(this).length == 0; } size(){ let s = items.get(this); return s.length; } clear(){ items.set(this, []); } print(){ console.log(this.toString()); } toString(){ return items.get(this).toString(); } } return Stack3; })();
把上面的代码跟最初实现的Stack类作个比较,咱们会发现有一些类似之处:
function Stack() { let items = []; //其余方法 }
事实上,尽管ES6引入了类的语法,仍然不能像在其余编程语言中同样声明私有属性或方法。有不少种方法均可以达到相同的效果,但不管是语法仍是性能,这些方法都有各自的优势和缺点
哪一种方法更好?这取决于在实际项目中如何使用算法,要处理的数据量,要建立的实例个数,以及其余约束条件
栈的实际应用很是普遍。在回溯问题中,它能够存储访问过的任务或路径、撤销的操做。Java和C#用栈来存储变量和方法调用,特别是处理递归算法时,有可能抛出一个栈溢出异常
下面将学习使用栈的三个最著名的算法示例。首先是十进制转二进制问题,以及任意进制转换的算法;而后是平衡圆括号问题;最后,学习如何用栈解决汉诺塔问题
【十进制转二进制】
现实生活中,咱们主要使用十进制。但在计算科学中,二进制很是重要,由于计算机里的全部内容都是用二进制数字表示的(0和1)。没有十进制和二进制相互转化的能力,与计算机交流就很困难
要把十进制转化成二进制,咱们能够将该十进制数字和2整除(二进制是满二进一),直到结果是0为止。举个例子,把十进制的数字10转化成二进制的数字,过程大概是这样
下面是对应的算法描述:
function divideBy2(decNumber){ var remStack = new Stack(), rem, binaryString = ''; while (decNumber > 0){ //{1} rem = Math.floor(decNumber % 2); //{2} remStack.push(rem); //{3} decNumber = Math.floor(decNumber / 2); //{4} } while (!remStack.isEmpty()){ //{5} binaryString += remStack.pop().toString(); } return binaryString; }
在这段代码里,当结果知足和2作整除的条件时(行{1}),咱们会得到当前结果和2的余数,放到栈里(行{2}、{3})。而后让结果和2作整除(行{4})。另外请注意:JavaScript有数字类型,可是它不会区分到底是整数仍是浮点数。所以,要使用Math.floor函数让除法的操做仅返回整数部分。最后,用pop方法把栈中的元素都移除,把出栈的元素变成链接成字符串(行{5})。
用刚才写的算法作一些测试,使用如下代码把结果输出到控制台里:
console.log(divideBy2(233)); //输出11101001 console.log(divideBy2(10)); //输出1010 console.log(divideBy2(1000)); //输出1111101000
【进制转换算法】
咱们很容易修改以前的算法,使之能把十进制转换成任何进制。除了让十进制数字和2整除 转成二进制数,还能够传入其余任意进制的基数为参数,就像下面算法这样:
function baseConverter(decNumber, base){ var remStack = new Stack(), rem, baseString = '', digits = '0123456789ABCDEF'; //{6} while (decNumber > 0){ rem = Math.floor(decNumber % base); remStack.push(rem); decNumber = Math.floor(decNumber / base); } while (!remStack.isEmpty()){ baseString += digits[remStack.pop()]; //{7} } return baseString; }
咱们只须要改变一个地方。在将十进制转成二进制时,余数是0或1;在将十进制转成八进制时,余数是0到7之间的数;可是将十进制转成16进制时,余数是0到9之间的数字加上A、B、C、D、E和F(对应十、十一、十二、1三、14和15)。所以,咱们须要对栈中的数字作个转化才能够(行{6}和行{7})
可使用以前的算法,输出结果以下:
console.log(baseConverter(100345, 2)); //输出11000011111111001 console.log(baseConverter(100345, 8)); //输出303771 console.log(baseConverter(100345, 16)); //输出187F9
【平衡圆括号】
function parenthesesChecker(symbols){ let stack = new Stack(), balanced = true, index = 0, symbol, top, opens = "([{", closers = ")]}"; while (index < symbols.length && balanced){ symbol = symbols.charAt(index); if (opens.indexOf(symbol) >= 0){ stack.push(symbol); console.log(`open symbol - stacking ${symbol}`); } else { console.log(`close symbol ${symbol}`); if (stack.isEmpty()){ balanced = false; console.log('Stack is empty, no more symbols to pop and compare'); } else { top = stack.pop(); //if (!matches(top, symbol)){ if (!(opens.indexOf(top) === closers.indexOf(symbol))) { balanced = false; console.log(`poping symbol ${top} - is not a match compared to ${symbol}`); } else { console.log(`poping symbol ${top} - is is a match compared to ${symbol}`); } } } index++; } if (balanced && stack.isEmpty()){ return true; } return false; } console.log(parenthesesChecker('{([])}')); //true console.log(parenthesesChecker('{{([][])}()}')); //true console.log(parenthesesChecker('[{()]')); //false
【汉诺塔】
function towerOfHanoi(n, from, to, helper){ if (n > 0){ towerOfHanoi(n-1, from, helper, to); to.push(from.pop()); console.log('-----'); console.log('Source: ' + from.toString()); console.log('Dest: ' + to.toString()); console.log('Helper: ' + helper.toString()); towerOfHanoi(n-1, helper, to, from); } } var source = new Stack(); source.push(3); source.push(2); source.push(1); var dest = new Stack(); var helper = new Stack(); towerOfHanoi(source.size(), source, dest, helper); source.print(); helper.print(); dest.print();