2.1 栈和队列—栈

什么是栈

栈本质是表,只不过其限制了插入和删除都在一个位置上进行,这个位置是表的末端,称之为栈顶。栈也被称作后进先出(LIFO:Last In First Out)表。

栈的模型

栈是一种抽象型数据模型,以下就是栈的模型,每次数据进栈都会添加到栈顶(top),每次访问只能访问栈顶的数据
栈的模型

因为栈的本质是表,所以可以通过数组或者链表进行实现。

操作

  • 进栈:private boolean push(E e)

进栈

进栈也称之为压栈,将对象或者数据压入栈中,更新栈顶指针,使其指向最后入栈的对象或数据。

  • 出栈:private E pop()

出栈

出栈也称之为弹栈,返回栈顶指向的对象或数据,并从栈中删除该对象或数据,更新栈顶。

  • 获取栈顶元素: public E peek()

直接获取栈属性表的最后一个元素即可。

  • 判断栈是否为空:public boolean isEmpty()

判断栈中属性表是否存在元素即可。

  • 获取栈中的元素数目:public boolean size()

直接返回栈属性表的大小。

  • 置空栈:public void empty()

直接调用栈属性表的 clear() 方法即可。

完整代码


package com.qucheng.qingtian.wwjd.datastructure;

/**
 * 栈
 *
 * @author 阿导
 * @CopyRight 万物皆导
 * @Created 2019-12-06 12:07:00
 */
public class DaoStack<E> {
    /**
     * 声明表,这里通过链表来实现
     */
    private DaoLinkedList<E> list;

    public DaoStack() {
        this.list = new DaoLinkedList<>();
    }

    /**
     * 进栈
     *
     * @param e
     * @return boolean
     * @author 阿导
     * @time 2019/12/6 :00
     */
    public boolean push(E e) {
        return this.list.add(e);
    }


    /**
     * 出栈
     *
     * @return E
     * @author 阿导
     * @time 2019/12/6 :00
     */
    public E pop() {
        if(this.list.size()>0) {
            return this.list.remove(this.list.size() - 1);
        }
        throw new RuntimeException("栈已空,无元素可出栈");
    }
    /**
     * 返回栈顶元素
     *
     * @return E
     * @author 阿导
     * @time 2019/12/6 :00
     */
    public E peek() {
        if (this.list.size() > 0) {
            return this.list.getData(this.list.size() - 1);
        }
        return null;
    }
    /**
     * 判断栈是否为空
     *
     * @return boolean
     * @author 阿导
     * @time 2019/12/6 :00
     */
    public boolean isEmpty() {
        return this.list.isEmpty();
    }

    /**
     * 获取栈的大小
     *
     * @return int
     * @author 阿导
     * @time 2019/12/6 :00
     */
    public int size() {
        return this.list.size();
    }

    /** 
     * 置空栈
     *
     * @author 阿导
     * @time 2019/12/6 :00
     * @return void
     */
    public void empty(){
        this.list.clear();
    }
    
}


栈的应用

逆序输出 :输出次序与处理过程颠倒;递归深度和输出长度不易知道。

这个利用栈的后进先出的特点,将给定的字符串进行逆序输出,如输入 abcde,将字母依次进栈,然后出栈,输出为 edcba。具体代码如下:


public class DaoStack<E> {
    /** 
     * 逆序输出
     *
     * @author 阿导
     * @time 2019/12/10 :00
     * @return void
     */
    private static void reverse(){
        String s="abcde";
        DaoStack<Character> daoStack = new DaoStack();
        for(char c:s.toCharArray()){
            daoStack.push(c);
        }

        while (!daoStack.isEmpty()){
            System.out.print(daoStack.pop());
        }
    }
    
}
    

递归嵌套:具有自相似性的问题可递归描述,但分支位置和嵌套深度不固定。

这里主要应用于括号匹配问题和栈混洗问题。

  • 括号匹配问题:如我们在开发代码的时候,括号都是对称的,编译器怎么判断括号是否对称?我们可以这样做,每次输入 “(”进栈,当输入“)”就出栈,这样要是栈中还存在“(”就会报丢失括号。或者栈中没有“(” 就输入 “)” 则会提示缺少 “(”。

  • 栈混洗:栈混洗的概念是说给定三个栈 A、B、S,其中 B,S 初始是空栈,A 是需要被栈混洗的栈,整个流程的操作只允许 A 出栈的元素入栈 S,S 出栈的元素入栈 B,最后将栈 A 中所有元素都转移到 栈 B ,这样就完成了对栈 A 的一次混洗。在这里我们需要了解一下栈混洗的甄别。

    • 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。

    我们分析以下这个问题:这个问题可以用栈混洗来解决,如果下一个弹出的数字刚好是辅助栈S的栈顶数字,那么直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入S中,直到把下一个需要弹出的元素压入栈顶为止。如果所有元素都压入了栈还没有找到下一个弹出的数字,那么该序列就不是一个栈混洗。下面给出 java 代码。


public class DaoStack<E> {
    /** 
     * 栈混洗
     *
     * @author 阿导
     * @time 2019/12/10 :00
     * @return void
     */
    private static void stackShuffle() {
        // 待入栈的序列
        String a = "abcdefg";
        // 待检测的序列
        String b = "gefdcba";
        // 栈 S
        DaoStack<Character> ss = new DaoStack<>();
        // 栈 A
        DaoStack<Character> as = new DaoStack<>();
        // 栈 B
        DaoStack<Character> bs = new DaoStack<>();
        // 长度都不一样,定然不是栈混洗
        if (a.length() != b.length()) {
            System.out.println("a 和 b 长度不一致,显然不是栈混洗。");
        }
        // 先将 a 进 栈 A
        for (char ac : a.toCharArray()) {
            as.push(ac);
        }
        // 然后进行栈甄别
        for (char bc : b.toCharArray()) {
            // 栈甄别递归(核心)
            Character temp = doStackShuffleCore(ss, as, bc);
            // 返回 null 跳出循环
            if (temp == null) {
                break;
            }
            // 入栈 B
            bs.push(temp);
        }
        // 长度相等,定然是栈混洗
        if (a.length() == bs.size()) {
            System.out.println("a 和 bs 长度一致,是栈混洗。");
        } else {
            System.out.println("a 和 bs 长度不一致,显然不是栈混洗。");
        }

    }

    /** 
     * 嵌套递归
     * 
     * @author 阿导
     * @time 2019/12/10 :00
     * @param ss 栈 S
     * @param as 栈 A
     * @param bc 比对值
     * @return java.lang.Character
     */
    private static Character doStackShuffleCore(DaoStack<Character> ss, DaoStack<Character> as, char bc) {
        // 若栈顶元素不等于 bc 继续递归
        if (ss.isEmpty() || !ss.peek().equals(bc)) {
            // 若栈 A 已空,仍然不能匹配,则直接返回
            if (as.isEmpty()) {
                return null;
            }
            // A 出栈 , S 入栈
            ss.push(as.pop());
            // 继续甄别
           return doStackShuffleCore(ss, as, bc);

        }
        // 出栈
        return ss.pop();
    }

}

延迟缓冲:在线性扫描算法模式中,在预读足够长之后,方能确定可处理的前缀。

在一些应用问题中,输入可以分解为多个单元并通过迭代依次扫描处理,但过程中的各步计算往往滞后于扫描的速度,需要待到必要的信息完整到一定程度之后,才能做出判断并实施计算。在此类场合中,栈结构则可以扮演数据缓冲区的角色。比如算数中的优先级比较,先找出优先级最高的运算,当出栈的时候遇到较低的运算,先放入临时栈延后处理,等优先级较高的运算执行玩再执行,这里不多做解释了。

栈式计算:基于栈结构的特定计算模式。

  • 逆波兰法(后缀表达式):将运算符写在操作数之后

这个场景也比较常见,我这边举个例子进行描述,不再写 java 代码了,就是想偷个懒,请谅解。

比如我们要计算:23+5+67=?
那么我们将这种操作的顺序书写如下:2 3 * 5 + 6 7 * +

  • 执行操作的时候,依次出栈2,3后然后出栈第一个运算符 *,计算出结果为 6,然后入栈,序列为: 6 5 + 6 7 * +

  • 再次出栈数字序列为 6,5,运算符为+,则计算结果为 11,入栈后序列为 11 6 7 * +

  • 再次出栈数字序列为 11,6,7 运算符为 *,那么计算运算符最近的两个数字,结果为 42,将 11,42 入栈,序列为 11 42 +

  • 最后一次出栈数字为11,42,运算符为 +,运算结果为 53

最小栈

最小栈和原来的栈相比就是能够返回栈中的最小值,实现起来相对比较简单,就加一个辅助栈记录最小值入栈即可,具体代码如下(辅助栈的大小一定和主栈相等吗?)。



    /**
     * 最小栈
     *
     * @author 阿导
     * @time
     * @copyRight 万物皆导
     */
    class MiniDaoStack<E> {
        /**
         * 主栈
         */
        private DaoStack<E> mainStack;
        /**
         * 寻找最小栈辅助类
         */
        private DaoStack<E> minStack;

        public MiniDaoStack() {
            this.mainStack = new DaoStack<>();
            this.minStack = new DaoStack<>();
        }

        /**
         * 入栈
         *
         * @param e
         * @return boolean
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public boolean push(E e) {
            boolean status = mainStack.push(e);
            if (status && (minStack.isEmpty() || minStack.peek().hashCode() >= e.hashCode())) {
                status = minStack.push(e);
            }
            return status;
        }

        /**
         * 出栈
         *
         * @return E
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public E pop() {
            E e = mainStack.pop();
            if (e.equals(minStack.peek())) {
                minStack.pop();
            }
            return e;
        }

        /**
         * 获取栈顶数据
         *
         * @return E
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public E top() {
            return mainStack.peek();
        }

        /**
         * 获取最小的元素
         *
         * @return E
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public E getMin() {
            return minStack.peek();
        }

    }
    

栈排序

实现排序核心是怎么利用一个辅助的栈巧妙进出栈达到排序的效果,直接给代码


public class DaoStack<E> {
    /** 
     * 从小到大排序
     *
     * @author 阿导
     * @time 2019/12/10 :00
     * @param daoStack
     * @return com.qucheng.qingtian.wwjd.datastructure.DaoStack<java.lang.Integer>
     */
    private static DaoStack<Integer> sortNumber(DaoStack<Integer> daoStack) {
        // 声明结果
        DaoStack<Integer> rs = new DaoStack<>();
        // 循环条件直到给定的栈出栈结束
        while (!daoStack.isEmpty()) {
            Integer cur = daoStack.pop();
            // 这里进行找寻到结果栈比给定栈小的位置,这步很关键
            while (!rs.isEmpty() && rs.peek() > cur) {
                daoStack.push(rs.pop());
            }
            // 栈顶数据小于等于当前给定栈定数据,直接入栈
            rs.push(cur);
        }
        // 返回排序后的结果
        return rs;
    }

}

两个栈实现一个队列

这边阿导还没有进入队列的专题,这边不对队列进行详细介绍,只说一下队列的特性 FIFO(First In First Out,即先进先出),所以通过两个栈实现队列核心就是保证数据的先进先出,直接撸代码,具体说明会在注释中进行讲解(主要理解出队列的两个 while 的写法即可)。




    /**
     * 两个栈实现队列
     *
     * @author 阿导
     * @time
     * @copyRight 万物皆导
     */
    class DaoStackQueue<E> {
        /**
         * 入队列保存的栈
         */
        private DaoStack<E> inStack = new DaoStack<>();
        /**
         * 出队列操作的栈
         */
        private DaoStack<E> outStack = new DaoStack<>();

        /**
         * 构造方法
         *
         * @param inStack
         * @param outStack
         * @return
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public DaoStackQueue(DaoStack<E> inStack, DaoStack<E> outStack) {
            this.inStack = inStack;
            this.outStack = outStack;
        }

        /**
         * 进队
         *
         * @param e
         * @return void
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public void enter(E e) {
            // 直接添加到入栈里
            inStack.push(e);
        }

        /**
         * 出队
         *
         * @return E
         * @author 阿导
         * @time 2019/12/10 :00
         */
        public E exit() {
            // 如果 outStack 不为空,直接从 outStack 里面弹栈返回即可
            while (!outStack.isEmpty()) {
                return outStack.pop();
            }
            // 要是 outStack 为空,且 inStack 不为空,则将 inStack 弹栈出的元素压栈到 outStack
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
            // 最后返回数据
            return outStack.pop();
        }
    }

延伸

大家都清楚再 Java 内存中包含堆、栈、方法区,这里只延伸栈的知识。首先我们思考下面几个问题

  • Java 栈里面存储了什么内容?为什么要存储这些东西?

栈里面存储的内容如下:

  1. 存放基本变量类型,包含这个基本类型的具体数值

  2. 引用对象的变量,会存放这个引用再堆里面的具体地址

JVM 基本架构就是采用栈进行来设计的。程序需要运行的时候,由于要预先内存空间和运行的生命周期,所以需要进行指针的变动,来进行内存大小的分配。因为这个操作会对程序的执行带来一定的不方便,所以一般栈被用来存放一些基本的变量类型或者引用对象的地址,而对于存储数据量较为庞大的 java 对象责备存储在了堆里面了。

  • 为什么说栈的提取速度比堆要快?
  1. 栈里面的内存大小一般都是程序启动的时候由系统分配好的。

  2. 堆的内存大小需要在使用的时候才回去申请,而且每次对于内存大小的申请和归还都会比较消耗性能,开销较大。

  3. cpu 里面会有专门的寄存器来操作栈,堆里面都是使用间接寻址的方式来进行对象查找的,所以栈会快一些。

  • 虚拟机栈数据有共享一说吗?

在概念模型中,两个栈帧是完全独立的,但是在虚拟机的实现里会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时,就可以共用一部分数据,无须进行额外的参数复制传递。
另外网上有人举例 int i=10,j=10,说 i 和 j 共享一个栈里面的数据,阿导认为这个解释有点牵强,栈帧里面包含了局部变量表和操作数栈,阿导认为若是他们有重叠部分,则会有共享情况发生,因为这样优化无须做额外参数复制传递。

虚拟机栈重叠区域数据共享

  • 栈帧里面包含哪些信息

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它位于虚拟机栈里面,每个栈帧包含信息如下图所示。

栈帧里面包含的内容

  1. 局部变量表(Local Variable Table)

这里面的作用主要是存储一系列的变量信息,而且这些变量都是以数字数组的形式来存储的,一般而言 byte,short,char,类型的数据在存储的时候会变为 int 类型,boolean 类型也是存储为数字类型,long,double 则是转换为双字节大小的控件存储在栈里面。

  1. 操作数栈(Openrand Stack)

    2.1 与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
    2.2 存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。

    2.3 数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。

    2.4 java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令(可见java虚拟机栈的指令主要取于操作数栈)。

    2.5 int类型在-15、-128127、-3276832767、-21474836482147483647范围分别对应的指令是iconst、bipush、sipush、ldc(这个就直接存在常量池了)

  2. 动态链接(Dynamic Linking)

动态链接的作用主要还是提供栈里面的对象在进行实例化的时候,能够查找到堆里面相应的类地址,并进行引用。这一整个过程,我们称之为动态链接。

  1. 返回地址(Return Adress)

某个子方法执行完毕之后,需要回到主方法的原有位置继续执行程序,方法出口主要就是记录该信息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值