第三章 栈
作者: Loiane Groner
我们已经学了如何生成和使用计算机科学中最常用的数据结构--数组。我们已经知道如何给数组添加和删除元素。然而,我们一种可以更灵活的删除和添加成员的数结结构。有两种数据结构和数组有一定的相似,且给了我们更多在增加元素和删除元素的灵活性。这两个数据结构就是栈和队列。在这一章,我们将讲解栈数据结构。
栈是一种遵守后进先出原则的数据结构。在这一结构中,删除元素和添加元素所在的位置都是在栈的尾部。栈的尾部也被称为顶部,而栈的顶部则被称为基。最新的元素离顶部近,而最老的元素离基近。
在现实生活中,也有栈的实际例子。比如下图中的一堆书:
编程语言的编译器会使用栈结构,计算机内存器也会使用栈结构来存储变量和方法。
生成一个栈
我们将手写一个栈结构。让我们从最基础的声明开始:
function Stack() {
//此处为定义属性和方法的代码
}
首先,我们需要一个数据结构来存储栈结构的成员。我们可以使用数组来达到这一目的:
var items =[];
接下来,我们要声明栈的常用方法:
- push(element(s) ):它会在数组的顶部增加一个元素。
- pop( ):它会在数组的顶部删除一个元素。它也会返回被删除的值。
- peek( ):它会返回数组顶部的元素。它不会改变原数组。
- isEmpty( ):如果数组没有成员,它会返回true;反之则返回false。
- clear( ):它会清空栈的所有元素。
- size( ):它会返回栈成员的数量。这个方法和数组的length属性很像。
我们第一个要添加的是push方法。这个方法是用来给栈添加元素:我们只能在数组的顶部添加元素。push方法是这样写的:
this.push = function(element){
items.push(element);
};
我们使用数组来存储栈的元素,所以我们可以使用JavaScript给数组定义的push方法。
接下来,我们要添加的是pop方法。这个方法是用来给栈删除元素的。因为栈遵循LIFO原则,即最新被添加的元素也是最先被删除的元素。因此,我们可以使用JavaScript定义的数组的pop方法。pop方法可以这样写:
this.pop = function() {
return items.pop()
};
有了push 作为唯一的增加元素方法和pop作为唯一的删除元素的方法,栈元素则可以很好的遵守LIFO方法了。
现在让我们为栈结构添加帮手函数。如果我们想要查看最新添加到栈的成员,我们可以使用peek方法。这个方法将会返回栈的顶部:
this.peek = function() {
return items[items.length-1];
};
因为我们在函数内部使用数组来存储栈的成员,我们可以用length-1 来获取栈的最后一个元素:
如上图所示,我们有一个有三个元素的栈,因此内部数组的长度为3。内部数组最后一个元素的下标为2。所以数组最后一个元素的表达式为 length -1。
下一个方法为isEmpty方法。当栈为空时,isEmpty会返回true,反之则返回false:
this.isEmpty = function () {
return items.length === 0;
};
有了isEmpty方法,我们可以轻松的判断栈的内部数组的长度是否为0。
与数组类型的length属性相似,我们可以为栈结构添加length属性。当然,我们通常使用的是‘size’而非 ‘length’来表示栈的长度。因为我们用内部数组来存储栈的元素,我们用函数来返回内部数组的长度即可:
this.size = function () {
return items.length;
};
最后,我们要添加的是clear方法。clear方法会讲栈清空,删除栈的所有元素。最简单的添加方法如下:
this.clear = function () {
items = [];
};
实现clear函数的另一个方法是使用pop,直到stack为空为止。
我们现在已经完成了一个栈结构。为了更好的查看栈结构的成员。我们可以定义print方法如下:
this.print = function () {
console.log( items.toString() );
};
现在就真的完成了!
完成栈结构
然我们看看完整的栈数据结构的代码:
function Stack() {
var 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() );
};
}
使用栈结构
在我们深入栈结构的实例之前,我们需要知道如何使用栈结构。
首先我们要知道如何初始化一个栈结构,然后我们可以查看它是否为空:
var stack = new Stack();
console.log(stack.isEmpty()); //输出为true
接下来,我们可以加入一些元素进去:
stack.push(5);
stack.push(8);
如果我们调用call方法,返回值是8,因为8是最后一个被添加进去的成员。
console.log(stack.peek()); //输出为8
我们再加入其他成员:
stack.push(11);
console.log( stack.size() ); //输出为3
console.log( stack.isEmpty() ); //输出为false
我们把11push到栈里面。如果我们调用size方法,它将会返回3,因为栈结构里面有三个元素。如果我们调用isEmpty方法,它将会返回false。最后,让我再加入另一个元素吧:
stack.push(15);
下图生动地展示了使用push方法后对stack变量的影响:
现在,通过调用连词pop方法来删除stack的两个成员:
stack.pop();
stack.pop();
console.log( stack.size()); 输出为2
stack.print(); 输出为 [5,8]
在我们调用pop方法之前,stack变量是有成员的。调用了两次pop方法后,stack变量就只有两个成员了: 5 和 8。下图展示了调用pop方法后对stack的影响:
转十进制数为二进制数
我们现在已经知道如何使用栈结构了,我们一起用它来解决一些计算机科学的问题吧。
你应该知道什么是十进制数了。然而,二进制表示法在计算机科学中是非常重要的,因为所有事物在计算机中都是由0和1来表示的。如果不能讲十进制数和二进制数来回转换,操控计算机将会变得些许困难。
我们可以通过将一个十进制数一直除以二,直到除数为0止,以将十进制数转为二进制数。下面这个例子将为我们展示其详细过程:
这个例子是计算机科班同学们在大学里学到的第一个转化问题。
function divideBy2 (decNumber){
var remStack = new Stack(),
rem,
binaryString = ‘’;
while (decNumber > 0 ){ // {1}
rem = Math.floor( decNumber%2); //{2}
remStack.push(rem); //{3}
deNumber = Math.floor ( decNumber / 2); // {4}
}
while(!remStack.isEmpty() ){ //{5}
binaryString += remStack.pop().toString();
}
return binaryString;
}
在这段代码中,当decNumber不等于零时***(行 {1}),我们将会得到decNumber除以2的余数,并将之push进remStack中(行 {2} 和 {3}),之后,我们将会通过除以2的方式更新decNumber(行 {4})。有一个值得注意的地方是,虽然JavaScript有数值类型的变量,但是它不会区分整数和小数。为此,我们需要用Math.floor方法来获得商的整数部分。最后,我们使用pop方法,直到remStack为空为止(行 {5})***,把被删除的元素连接成一个字符串。
我们可以用几个实例来测试这个算法:
console.log(divideBy(233));
console.log(divideBy(10));
console.log(divideBy(1000));
我们可以通过一些简单的改动,讲刚刚的算法变成一个将十进制数转为任意进制的算法。我们需要将被除数以一个变量的形式传入,而非让被除数为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;将十进制数转为八进制数时,得到的余数是零到8中的一个数;但在转十进制数为十六进制数时,得到的余数将是 0到8以及 A到F(代表10到15)中的一个。为此,我们用一个变量来保存这些值***(行 {6} 和 {7})***。
我们可以用一些实例来测试这个算法:
console.log(baseConverter(100345, 2) );
console.log(baseConverter(100345, 8) );
console.log(baseConverter(100345, 16) );
小结
在这一章,我们已经学习了栈结构。我们学习了如何给栈价格添加pop和push方法。我们学习了一个非常有名的栈结构的使用案例。
在下一章,我们将会学习队列结构。队列结构与栈结构在很多方面是非常相似的,但队列所遵循的是LIFO(先进先出)原则。
注:本文翻译自Loiane Groner的《Learning JavaScript Data Structures and Algorithm》