02【JS数据结构与算法】栈

写在前面

上一节我们介绍了JS中的第一个数据结构——数组,包括它里面的一些自带的方法、还有我们自己手动实现的方法、还有使用场景等。JS中的数组跟其他语言不太一样,它是动态增长的,所以它给我们编程带来了极大的灵活性。今天我们再来介绍另一种数据结构——栈,它其实可以看作是JS中的一种特殊的数组。

接下来的内容,我们按下图所示给大家介绍:

栈的描述

栈的功能其实跟数组类似,就是用来存储一些数据的,不过它跟数组不同的是:栈是一种后进先出(LIFO)的数据结构。怎么理解呢?就像是现实生活中我们在桌子上摞的一摞书,你如果想拿中间的某一本书的话,你只能从上面开始将压盖在这本书上方的书全部拿掉,才能取出你想要的这本书(这里不讨论直接从旁边抽出来的情况,纯属杠精,哈哈)。所以我们的栈也是这种数据结构,我们存进去的数据就像是这些书一样,也可以理解成是“摞”起来的,最先存入的数据它位于栈的最底部,最后存入的数据位于栈的最顶部。如果后期想从里面取数据的话,最后存入的数据也就是栈顶的数据是最先拿到的,然后依次往下取,最先存入的数据应该是最后拿到的数据,就像下面这张图一样:

创建一个基于数组的栈

栈在JS中实例化和创建方式可以有两种:基于数组和基于对象。我们先介绍第一种方式,本文后续部分再继续介绍基于对象的方式创建栈。

1、创建一个基于数组的栈

我们先创建一个类来表示栈这个数据结构,然后栈里面所要保存的数据元素我们将它保存到一个数组里,这就是基于数组的一个栈,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
            }

由上代码,我们创建了一个基于数组的栈,但是现在直接往里面存数据或者取数据的话,跟直接将数据存放在数组中是没有任何区别的,在这里体现不出LIFO的栈特性,所以我们为其定义一些方法,让它符合我们后进先出的特性。

2、往栈中添加元素

往栈中添加元素就是要将元素添加到栈顶,也就是我们数组的末尾,所以我们直接用push()方法就可以,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
                push(value) {
                    //往栈中添加元素
                    this.items.push(value);
                }
            }

3、从栈移除元素

如果要删除栈中的元素,也是从栈顶开始,所以我们直接用pop()就可以实现,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
                push(value) {
                    //往栈中添加元素
                    this.items.push(value);
                }
                pop() {
                    //从栈中移除元素
                    this.items.pop();
                }
            }

4、查看栈顶元素

获取栈顶的元素,其实就是获取我们创建的栈中数组的最后一位元素,因为栈是后进先出的数据结构,所以我们直接返回最后一个元素即可,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
                push(value) {
                    //往栈中添加元素
                    this.items.push(value);
                }
                pop() {
                    //从栈中移除元素
                    this.items.pop();
                }
                peek() {
                    //获取栈顶元素
                    return this.items[this.items.length - 1];
                }
            }

5、检查栈是否为空

检查一个栈是否为空的话,我们直接去判断这个栈的数组长度是否为0即可,同样的,通过返回栈的数组长度,我们也可以间接地得到栈的大小,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
                push(value) {
                    //往栈中添加元素
                    this.items.push(value);
                }
                pop() {
                    //从栈中移除元素
                    this.items.pop();
                }
                peek() {
                    //获取栈顶元素
                    return this.items[this.items.length - 1];
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.items.length === 0 ? true : false;
                }
                size() {
                    //获取栈的大小
                    return this.items.length;
                }
            }

6、清空栈元素

如果想清空一个栈的话,我们直接将栈内的数组置空就行,代码如下:

            //基于数组的栈
            class Stack {
                constructor() {
                    this.items = []; //用来保存栈中的数据元素
                }
                push(value) {
                    //往栈中添加元素
                    this.items.push(value);
                }
                pop() {
                    //从栈中移除元素
                    this.items.pop();
                }
                peek() {
                    //获取栈顶元素
                    return this.items[this.items.length - 1];
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.items.length === 0 ? true : false;
                }
                size() {
                    return this.items.length;
                }
                clear() {
                    //清空栈
                    this.items = [];
                }
            }

7、使用上述创建的基于数组的栈

            //使用栈
            let arrayStack = new Stack();

            console.log(arrayStack.isEmpty()); //输出true

            arrayStack.push(1);
            arrayStack.push(2);
            arrayStack.push(3);
            console.log(arrayStack.size()); //输出3
            console.log(arrayStack.peek()); //输出3

            arrayStack.pop();
            console.log(arrayStack.peek()); //输出2

创建一个基于对象的栈

在上面我们介绍栈的时候,保存栈中的元素时是将元素保存在了一个JS数组中,但是这样其实有个问题。如果我们栈中存放的数据很多的话,用数组来存放元素其实性能是很低的,所以我们想一想其它的方法。这部分我们开始介绍一下用基于JS对象的方式来创建栈,并让它遵循LIFO原则。

1、创建一个基于JS对象的栈

我们在此处创建一个基于JS对象的栈,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
            }

以上代码中我们使用count变量来表示栈的大小,后期也可以用它来操作栈中的元素。在以上代码中我们没有像之前一样使用数组来存放栈中的元素,而是定义了一个对象items,后期我们用这个对象来存放栈中的元素。

2、往栈中插入元素

往栈中添加元素的话,其实就是往items这个对象中添加元素。对象中的元素其实就是一系列键值对的集合,我们将count作为它的键,要添加的元素作为相应的值,就在items对象中插入元素,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
            }

3、获取栈的大小和检查栈是否为空

我们之前说过,用count变量来表示栈的大小,所以获取栈的大小的时候我们只需要返回count变量即可。检查栈是否为空的话,我们直接去判断count变量是否等于0即可,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
                size() {
                    //获取栈的大小
                    return this.count;
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.count === 0 ? true : false;
                }
            }

4、删除栈中元素

删除栈中元素的话,我们直接使用删除对象值的方法delete来实现,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
                size() {
                    //获取栈的大小
                    return this.count;
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.count === 0 ? true : false;
                }
                pop() {
                    //删除栈中元素
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    this.count--;
                    const resultData = this.items[this.count];
                    delete this.items[this.count];
                    return resultData;
                }
            }

5、查看栈顶值

获取栈顶元素的话,我们直接获取items对象的count-1的值就可以,如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
                size() {
                    //获取栈的大小
                    return this.count;
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.count === 0 ? true : false;
                }
                pop() {
                    //删除栈中元素
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    this.count--;
                    const resultData = this.items[this.count];
                    delete this.items[this.count];
                    return resultData;
                }
                peek() {
                    //查看栈顶值
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    return this.items[this.count - 1];
                }
            }

6、清空栈

清空栈的方法有两种,最简单的就是将count值设为0,items对象置空即可。你也可以使用循环来调用pop()方法依次删除元素,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
                size() {
                    //获取栈的大小
                    return this.count;
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.count === 0 ? true : false;
                }
                pop() {
                    //删除栈中元素
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    this.count--;
                    const resultData = this.items[this.count];
                    delete this.items[this.count];
                    return resultData;
                }
                peek() {
                    //查看栈顶值
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    return this.items[this.count - 1];
                }
                clear() {
                    //清空栈
                    this.count = 0;
                    this.items = {};
                }
            }

7、toString()方法

toString()方法用于打印出栈中的元素,对于数组创建的栈,我们直接调用数组提供的toString()方法即可,对于基于对象创建的栈,我们要自己手动去实现这方法,代码如下:

            //基于对象的栈
            class Stack {
                constructor() {
                    this.count = 0;
                    this.items = {};
                }
                push(value) {
                    //往栈中添加元素
                    this.items[this.count] = value;
                    this.count++;
                }
                size() {
                    //获取栈的大小
                    return this.count;
                }
                isEmpty() {
                    //检查栈是否为空
                    return this.count === 0 ? true : false;
                }
                pop() {
                    //删除栈中元素
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    this.count--;
                    const resultData = this.items[this.count];
                    delete this.items[this.count];
                    return resultData;
                }
                peek() {
                    //查看栈顶值
                    if (this.isEmpty()) {
                        return undefined;
                    }

                    return this.items[this.count - 1];
                }
                clear() {
                    //清空栈
                    this.count = 0;
                    this.items = {};
                }
                toString() {
                    //打印栈中元素
                    if (this.isEmpty()) {
                        return '';
                    }

                    let resultStr = `${this.items[0]}`;
                    for (let i = 1; i < this.count; i++) {
                        resultStr = `${resultStr},${this.items[i]}`;
                    }
                    return resultStr;
                }
            }

8、使用上述创建的栈

在这一节我们通过下面的代码来使用一下上述基于JS对象创建的栈,代码如下:

            //使用栈
            let objectStack = new Stack();

            objectStack.push(22);
            objectStack.push(33);
            objectStack.push(44);
            objectStack.push(55);

            console.log(objectStack.size()); //输出4
            console.log(objectStack.peek()); //输出55
            console.log(objectStack.pop()); //输出55
            console.log(objectStack.peek()); //输出44
            console.log(objectStack.toString()); //输出22,33,44

保护数据结构内部元素

我们上述两种方式创建的栈数据结构,内部的变量都是可以随意进行更改和访问,但是我们的预期或愿景是它并不暴露给外部,如果想设置或者获取内部变量的话,只能通过我们暴露给外部的方法来实现。为了达到这样的目的,我们有以下几种方案:

1、下划线命名约定;

2、用ES6的限定作用域Symbol来实现类;

3、用ES6的WeakMap来实现类;

4、寄希望于ECMAScript的类属性提案。

以上四种方法的具体实现过程,大家自行百度即可,在这里就不展开叙述,其实前三种方法并不能真正达到将变量私有化的目的,我们只是尽可能的规避而已。第四种方法中,关于TypeScript或者ECMAScript最新的提案中有将类属性私有化的方式,目前看来这里面的方法是比较靠谱的。

栈数据结构的实际应用

前面介绍了那么多,最后我们用一道面试题来做一下结尾,顺便介绍下栈数据结构的实际应用。这道面试题其实很简单,就是一个十进制数字转为二进制或其他任意进制的问题。

这类进制转换问题用栈来做其实是很合适的,首先我们看一下十进制转二进制的计算过程和栈之间存在的联系:

由上图可看到,我们要将一个十进制的数字转为二进制的话,就需要将数字每次除以2后取余数,直到商为0截止,最后将这些余数倒叙排出来就是所需要的二进制结果。如果对应到栈中的话,我们每次将余数压入栈中,最后直接将栈中元素依次出栈,形成的数值就是最终的结果,代码如下:

            //十进制转二进制
            function decimalToBinary(deNumber) {
                let objectStack = new Stack(); //存放每次的取余
                let resultStr = ''; //存放结果,目前是一个字符串

                while (deNumber > 0) {
                    let rem = Math.floor(deNumber % 2); //每次取余,然后将余数取整,压入栈中
                    objectStack.push(rem);
                    deNumber = Math.floor(deNumber / 2); //每次取商,直至达到循环终止条件
                }

                while (!objectStack.isEmpty()) {
                    //如果栈不为空,栈中元素依次出栈
                    resultStr += objectStack.pop().toString();
                }

                return resultStr;
            }

            console.log(decimalToBinary(233)); //输出11101001
            console.log(decimalToBinary(10)); //输出1010

除了将十进制转换为二进制之外,我们还可以优化一下上述代码,将其改为十进制转2~36的任意进制,代码如下:

            //十进制转任意进制
            function decimalToConverter(deNumber, base) {
                let objectStack = new Stack();
                let resultStr = '';
                let digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

                while (deNumber > 0) {
                    let rem = Math.floor(deNumber % base);
                    objectStack.push(rem);
                    deNumber = Math.floor(deNumber / base);
                }

                while (!objectStack.isEmpty()) {
                    resultStr += digits[objectStack.pop()];
                }

                return resultStr;
            }

            console.log(decimalToConverter(100345, 2)); //输出11000011111111001
            console.log(decimalToConverter(100345, 8)); //输出303771
            console.log(decimalToConverter(100345, 16)); //输出187F9
            console.log(decimalToConverter(100345, 35)); //输出2BW0

总结

这篇文章我们介绍了一下栈这个数据结构,包括它的两种创建方式以及它的实际应用。其实栈在计算机领域的任何地方都有使用,大家今后的开发过程中需要自己再去做更加深入的研究和学习。下一篇文章我们介绍一下跟栈类似的另一个数据结构——队列。

猜你喜欢

转载自blog.csdn.net/qq_35117024/article/details/107043177