数据结构与算法栈的深入学习(上)

Posted IT辰柒_测试指导

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法栈的深入学习(上)相关的知识,希望对你有一定的参考价值。

✨hello,进来的小伙伴们,你们好耶!✨

🍅🍅系列专栏:【数据结构与算法】

✈️✈️本篇内容:  栈的认识,栈的使用,栈的模拟实现!

⛵⛵作者简介:一名双非本科大三在读的科班Java编程小白,道阻且长,你我同行!

🍱🍱码云存放仓库gitee:Java数据结构代码存放!

一、栈(Stack)

一、栈的概念

栈,一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。

 二、栈的使用

 代码演示:

入栈push()

public class MyStack 
    public static void main(String[] args) 
        Stack<Integer> s1 = new Stack<>();
        s1.push(1);
        s1.push(2);
        s1.push(3);
        s1.push(4);
        s1.push(5);
        System.out.println(s1);
    

运行结果:

 出栈pop(),这里我们出栈两个元素,那么剩下的结果应该是1 2 3

public class MyStack 
    public static void main(String[] args) 
        Stack<Integer> s1 = new Stack<>();
        s1.push(1);
        s1.push(2);
        s1.push(3);
        s1.push(4);
        s1.push(5);
        System.out.println(s1);
        s1.pop();
        s1.pop();
        System.out.println(s1);
    

运行结果:

 获取栈顶元素peek():

public class MyStack 
    public static void main(String[] args) 
        Stack<Integer> s1 = new Stack<>();
        s1.push(1);
        s1.push(2);
        s1.push(3);
        s1.push(4);
        s1.push(5);
        System.out.println(s1.peek());
    

运行结果:

 获取栈中有效元素的个数

public class MyStack 
    public static void main(String[] args) 
        Stack<Integer> s1 = new Stack<>();
        s1.push(1);
        s1.push(2);
        s1.push(3);
        s1.push(4);
        s1.push(5);
        System.out.println(s1.size());
    

运行结果:

 判断栈是否为空

public class MyStack 
    public static void main(String[] args) 
        Stack<Integer> s1 = new Stack<>();
        s1.push(1);
        s1.push(2);
        s1.push(3);
        s1.push(4);
        s1.push(5);
        System.out.println(s1.empty());
    

运行结果:

 三、栈的应用场景

1. 若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是(C)
 A: 1,4,3,2  B: 2,3,4,1  C: 3,1,4,2  D: 3,4,2,1

问题分析:类似于这种栈的选择题,如果元素较少,我们直接心算就可以,元素较多的话我们可以画图来解决,本题c选项,先出的是3,那么就是1,2,3进栈,然后3出栈,第二个出栈选项给的是1,我们知道1是第一个进栈的,那么想出1,2必须先出,所以C选项错误!
 
2.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( B)。
A: 12345ABCDE  B: EDCBA54321  C: ABCDE12345  D: 54321EDCBA

问题分析:简单明了,栈的结构先进后出,直接选B。

 四、栈的模拟实现

import java.util.Arrays;
import java.util.Stack;

/**
 * 栈的模拟实现
 */

public class MyStack 

    public int[] elem;
    public int usedSize;
    public static final int DEFAULT_SIZE = 10;

    public MyStack() 
        this.elem = new int[DEFAULT_SIZE];
    


    /**
     * 压栈
     */

    public int push(int val) 
        if(isFull()) 
            elem = Arrays.copyOf(elem,2*elem.length);
        
        this.elem[usedSize] = val;
        usedSize++;
        return val;
    


    public boolean isFull() 
        return usedSize == elem.length;
    


    public int pop() 
        if(empty()) 
            throw new MyEmptyStackException("栈为空!");
        
        /*int ret = elem[usedSize-1];
        usedSize--;
        return ret;*/
        return elem[--usedSize];
    


    public boolean empty() 
        return usedSize == 0;
    


    public int peek() 
        if(empty()) 
            throw new MyEmptyStackException("栈为空!");
        
        return elem[usedSize-1];
    

🍎🍎栈的模拟实现我们使用的是数组来实现的,代码也非常的简单明了,易于看懂,博主准备下一篇博客更新栈的几个经典面试题,慢慢干货,期待你的一键三连喔!🤟🤟

《学习JavaScript数据结构与算法》 第四章笔记 栈


前言

食用方法:请着重看下创建数组栈的步骤, 由于创建方便, 下半篇的例子大都基于数组栈来说明.
三四章看不懂请回来看第二章
请至少在看完前三章后再去看第五章…

上一篇:第三章 14000字笔记


一、栈?

栈,英文Stack(这个洋名很重要),下文会多次提及;

是一种遵从先进后出原则(LIFO)的有序结合,新添加或者待删除的元素都保存在栈的同一端即栈顶,那另一端自然就是栈底。
栈也可以用于在编译器和内存中保存变量和方法,另外,浏览器的历史记录和路由也和栈有关。


二、构建两种栈的大致步骤

书上讲述了两种栈的创建方式,两者之间存在大量的相似之处。

  1. 都要先声明一个Stack类:
class Stack {
  constructor() {
  //constructor方法内有不同
  }
}
  1. 都要定义一系列方法来预备对栈进行的操作(如存入和取出),但针对数组和对象构建的栈, 定义的方法自然不能相同;

  2. 在使用之前都要初始化Stack类:

const 自定义名 = new Stack();

三、创建基于数组的栈

创建一个Stack类最简单的方式是使用数组.

创建class Stack

class Stack {
  constructor() {
    this.items = [];
    //需要一种数据结构来存储栈中的元素, 这里选择了数组items.
  }
}

定义用于操作栈的方法

既然是基于数组的栈, 那我们就使用与数组方法同名的自定义方法吧…
考虑到函数同名的问题,个人不推荐把这些自定义方法写在class Stack外面;
举例:写了push之后JS会马上判定为数组的push方法并且报错你没加分号而不是判定它为一个自定义方法:

但是写在类里面你就不用担心这种奇怪的问题, 那我就直接展示在类里面的写法:

        class Stack {
            constructor() {
                this.items = [];
            }
            
            push(element) {  
            //向栈顶添加一个元素
                this.items.push(element);
            }

            pop() {  
            //从栈顶移除一个元素
                return this.items.pop();
            }

            peek() {  
            //查看栈顶元素,由于使用数组存储,最新加入的处于栈顶的元素可使用length-1取到;
                return this.items[this.items.length - 1];
            }

            isEmpty() {
            //检测当前栈是否为空;
                return this.items.length === 0;
            }

            size() {
            //返回栈的长度;
                return this.items.length;
            }

            clear() {
            //清空栈, 也可以多次调用pop来解决;
                this.items = [];
            }

        }

使用栈

第二节说了我们需要先初始化Stack类才能使用它, 一个不错的习惯是先初始化然后再验证一下其是否为空:

const myStack = new Stack();  //初始化;
console.log(myStack.isEmpty());  //true;

我这里使用的myStack是基于上面的Stack类存在的, 这部是在初始化上面声明的Stack类, 来去掉Stack类看看:

Uncaught ReferenceError: Stack is not defined

嗯, 然后这个栈就建完了, 来试一下吧:

let arr = [3, 5, 6];
myStack.push(...arr); 
myStack.push("一个字符串");
console.log(myStack);

相当于myStack.push(3, 5, 6), 但是定义push方法时定义了只能传一个参;
故只有第一个参数3会被传入class Stack数组items里的第0位

另外在测试pop方法的时候发现栈似乎有些特性…
先push两个元素进去, 输出得到结果A.
然后pop移除栈顶的一个元素,刷新页面再次输出,两个结果都会是删除栈顶元素后的栈
即便第一次输出的代码排在pop之前:

console.log("初始栈是否为空:" + myStack.isEmpty()); 
let arr = [3, 5];
myStack.push(...arr);
myStack.push("一个字符串");
console.log(myStack);   //67行;
myStack.pop();
console.log(myStack); //70行;


四、创建基于对象的栈

与创建数组型栈的步骤很相似, 不做细说了.
即便十分方便, 用数组来创建栈依然存在诸多缺点, 若用n代表数组的长度,大部分方法访问数组时的时间复杂度是O(n), 这个公式的意思是我们需要迭代数组直到找到目标元素,甚至需要迭代整个数组。
此外,数组有序化的存储方式会占用更多的内存空间。

创建class Stack

        class Stack {
            constructor() {
                this.count = 0;
                this.items = {}
    //需要一种数据结构来存储栈中的元素, 这里选择了数组items.
            }

定义用于操作栈的方法

        class Stack {
            constructor() {
                this.count = 0;
                this.items = {}
            }

            push(element) {
                this.items[this.count] = element;
                this.count++;
            }

            pop(element) {
                if (this.isEmpty()) {
                    return undefined;
                }
                this.count--;
                const result = this.items[this.count];  
                //将被删除的元素赋值给result;
                delete this.items[this.count];
                //delete操作符用于删除对象中的某个属性;
                return result;  
                //返回被删除的元素;
            }

            peek(element) {
                if (this.isEmpty()) {
                    return undefined;
                }
                return this.items[this.count - 1]; 
                 //length-1取到栈顶值;
            }

            isEmpty(element) {
                return this.count === 0;
            }

            size() {
                return this.count;
            }

            toString () {
            //对象型栈无法使用toString数组方法,需要自定义方法来对栈内容进行打印;
                if(this.isEmpty()) {
                    return '';
                }
                let objString = `${this.items[0]}`;  
                //使用最底部字符串作为初始值
                for(let i = 1; i < this.count; i++) {
                //遍历整个栈内的键直到栈顶;
                    objString = `${objString}, ${this.items[i]}`;
                }
                return objString;
            }

            //这样下来除了toString方法, 其他几个方法的时间复杂度均为O(1), 也就是说都不用进行遍历.
        }

使用栈

const mystack = new Stack();
console.log(peek);
stack.push(5);
stack.push(8);

五、保护数据结构内部元素

“在创建别的开发者也可以使用的数据结构或对象时, 我们希望保护内部的元素,只有我们暴露出的方法才能修改其内部结构. 要确保元素只会被添加到栈顶, 而不是栈底或者其他什么地方, 不幸的是上面所举的栈并没有得到保护.”

“本章使用ES2015语法创建了Stack类, ES2015的类是基于原型的, 尽管基于原型的类可以节省内存空间而且在扩展方面优于基于函数的类, 但这种方式不能声明私有属性或方法.”

简而言之, 我们不希望栈内部的东西能被外面看到或者改写, 只允许人们从栈顶用我们定义好的方法进行操作, 将这一摞书外面套一层水泥管.

演示:暴露的栈模型

就常规的栈而言, 使用Object.getOwnPropertyNames()方法可以get到指定对象的所有属性名组成的数组, keys()也可, 就算直接输出都可以:

class Stack {
  constructor() {
    this.count = 0,  //count不能是字符串;
    this.items = [];
  }
}

const stack = new Stack();
console.log(Object.getOwnPropertyNames(stack));
console.log(Object.keys(stack));

基于ES6 Symbol实现的半私有类

书中先尝试使用ES6 Symbol类型实现类, 来达到保护栈内元素不受损伤的目的.
Symbol是ES6新增的一种数据类型, 它是不可变的, 可用作对象的属性.
但是ES6对应的也伴生了getOwnPropertySymbols()方法可以破解这一保护措施:
“该种方法创建了一个假的私有属性, 因为ES6新增的Object.getOwnPropertySymbols方法能访问目标类里面所有的Symbols属性”

不论如何, 先看下这个例子(构建栈的方法还是没有做出改变的):

const _items = Symbol('stackItems');
//创建class Stack;
class Stack {
  constructor() {
    this[_items] = [];
  }

//构建自定义方法push;
  push(element) {
    this[_items].push(element);
  }

}

//初始化Stack类, 准备进行操作;
const myStack = new Stack();
myStack.push(5);  //操作:压入元素5;
myStack.push(8);  //操作:压入元素8;

let objectSymbols = Object.getOwnPropertySymbols(myStack);

//此处相当于是把myStack这个类砸开了一个叫objectSymbols的缺口
//经由objectSymbols这个缺口我们可以访问到myStack类中的各种属性;

对比未执行getOwnPropertySymbols()的结果输出一下:

console.log(myStack);  //直接输出myStack
console.log(objectSymbols);   //输出经过处理的myStack
console.log(myStack.length);  //直接输出长度
console.log(objectSymbols.length);  //输出经过处理后的长度


经过处理后的myStack返回的是symbol属性stackItems, 由于处理后只能返回symbol属性, 最终结果的详细程度反倒不如直接输出myStack高;

基于ES6 weakMap实现的私有类

weakMap类型可以确保属性是私有的, weakMap可以存储键值对, 其中键是对象, 值可以是任意数据类型.

const items = new WeakMap();  //weakMap型变量items;
//将一个变量转换为WeakMap类与将其转换为Symbol类的方法很像吧?
class Stack {
  constructor() {
    items.set(this, []);  //将代表栈的数组存入items;
  }

//定义用于操作栈的方法
  push(element) {
    const s = items.get(this);
    s.push(element);
  }

  pop() {
    const s = items.get(this);
    const r = s.pop();
    return r;
  }
}

//初始化Stack类, 准备使用栈
const myStack = new Stack();
myStack.push(3);  //操作:压入3;
myStack.push(6);  //操作:压入6;

这是受保护的栈了, 我们输出一下看看大概就可以证明:

console.log(myStack);
console.log(Object.getOwnPropertyNames(myStack));
console.log(Object.keys(myStack));


依靠这种简单的手段, 我们几乎不能获取到任何栈内部的数据和构造信息, 不能直接访问.


六、用栈解决问题

栈的实际应用十分广泛.
在有回溯需求的问题上, 它可以存储访问过的任务或路径、撤销的操作.
Java和C#用栈来存储变量和方法调用.

书上举的例子是一个十进制转二进制的进制转换问题.

//创建基于数组的栈
class Stack {
  constructor() {
    this.count = 0;
    this.items = [];
  }
  //定义用于操作数组的各项方法;
  push(element) {
    this.items.push(element);
  }

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

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

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

function decimalToBinary(decNumber) {
  const remStack = new Stack();
  //在哪里使用栈就在哪里初始化栈;
  let number = decNumber;
  let rem;
  let binaryString = '';

  while (number > 0) {
    rem = Math.floor(number % 2);  //除法结果不为0时就将取整后的余数入栈
    remStack.push(rem);
    number = Math.floor(number / 2);  //除完一次取整继续除
  }

  while (!remStack.isEmpty()) {
    //二进制字符
    binaryString += remStack.pop().toString();  //将可出栈的元素链接为字符串, 清栈;
  }

  return binaryString;  //return出二进制数值 
}

console.log(decimalToBinary(33333));

总结

下篇将是第四章《队列和双端队列》,可能会咕一段时间了,最近出了好多事情,有点累了。

以上是关于数据结构与算法栈的深入学习(上)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法学习笔记 栈和队列Ⅰ

Python数据结构与算法(3.1)——栈

java数据结构与算法之栈(Stack)设计与实现

Java学习书籍推荐

《学习JavaScript数据结构与算法》 第四章笔记 栈

javascript数据结构与算法学习笔记