数据结构系列(3)— 栈

486 阅读11分钟

学 无 止 境 , 与 君 共 勉 。


相关系列

介绍

栈(stack)是限制插入和删除只能在一个位置上的数据结构,该位置是栈的末端,叫做栈的顶(top),它是属于后进先出(LIFO)的数据结构。对栈的基本操作有push(入栈)和pop(出栈),前者相当于在栈的末端插入数据,后者相当于删除最后插入的数据。栈的大小通常比较小,可用于临时数据结构。通常我们通过数组实现一个栈。

身边的栈

在JAVA中我们每次方法的调用其实就是入栈操作,把这个方法的返回地址(使内部方法运行完后知道返回到哪里去)和参数(隔离各个方法的参数)压入栈中,方法调用结束后方法出栈,获取到方法的返回值,传给外部方法,并释放栈空间。还有对异常处理 e.printStackTrace()的实质就是打印异常调用的堆栈信息。

操作

Push(入栈)

top 指向了栈顶,如果是个空栈,top = -1;新数据入栈的时候执行2步操作:

  1. 先将top指向下一位置: top++;
  2. 然后将新插入的数据放入top的位置;

注意如果顺序相反了,新插入的数据会替换掉原先栈顶的数据,top指向的数据为空数据

pop(出栈)

移除并返回栈顶的数据;同样有2步操作:

  1. 获取顶部数据;
  2. top--;

这里同样不能颠倒顺序,如果先将top减1了,获取到的数据为移除后的栈顶数据;

通过数组实现栈

public class StackUseArray<E>{

    // 最大容量
    private int maxSize;

    // 数据数组
    private Object[] array;

    // 执行栈顶
    private int top;

    public StackUseArray(int maxSize) {
        this.maxSize = maxSize;
        this.array = new Object[maxSize];
        this.top = -1;
    }

    /**
     * 顶端插入数据
     *
     * @param data 数据
     */

    public void push(E data) {
        if (isFull()) {
            System.out.println("栈已满,不能再添加了!!!");
            return;
        }
        this.array[++this.top] = data;
    }

    /**
     * 移除顶端数据,并返回移除的数据
     *
     * @return data
     */

    @SuppressWarnings("unchecked")
    public E pop() {
        if (isEmpty()) {
            System.out.println("栈已空了,不能再删除了!!!");
            return null;
        }
        return (E) this.array[this.top--];
    }

    /**
     * 查询顶部数据
     *
     * @return data
     */

    @SuppressWarnings("unchecked")
    public E peek() {
        if (isEmpty()) {
            System.out.println("这是个空栈!!!");
            return null;
        }
        return (E) this.array[this.top];
    }

    /**
     * 检查是否为空
     *
     * @return boolean
     */

    public boolean isEmpty() {
        return this.top == -1;
    }

    /**
     * 检查是否已经满了
     *
     * @return boolean
     */

    public boolean isFull() {
        return this.top == (this.maxSize - 1);
    }

}

应用场景

我们通过IDE编写代码时,对于大量嵌套代码有时候会漏掉或者多写一个括号,这时会显示错误信息。这里通过栈的方式去验证文本中括号的格式合法性。

思路:读取文本,每次遇到左括号就入栈,每次遇到右括号就从栈顶弹出左括号去和当前右括号配对,校验是否合法。当所有文本全部读取完成后,查看栈是否为空,不为空,说明文本中少了右括号

  • 实现
public class BracketChecker {

    /**
     * 左括号,需要入栈
     */

    private static final String LEFT_BRACKET = "([{";

    /**
     * 右括号,用于校验,每次遇到需要校验和出栈的左括号是否匹配
     */

    private static final String RIGHT_BRACKET = ")]}";

    /**
     * 用于匹配校验,每个右括号对应它的左括号
     */

    private static final Map<Character, Character> MATCH_MAP = new HashMap<Character, Character>() {{
        put(')''(');
        put(']''[');
        put('}''{');
    }};

    /**
     * 要校验的文本
     */

    private String text;

    public BracketChecker(String text) {
        this.text = text;
    }

    /**
     * 执行校验
     *
     * @return boolean
     */

    public boolean check() {
        int stackSize = this.text.length();
        StackUseArray<Character> stack = new StackUseArray<>(stackSize);
        // 当前字符
        char curChar;
        for (int i = 0; i < stackSize; i++) {
            curChar = this.text.charAt(i);
            if (LEFT_BRACKET.indexOf(curChar) > -1) {
                // 左括号入栈
                stack.push(curChar);
                System.out.println("push:" + curChar);
            } else if (RIGHT_BRACKET.indexOf(curChar) > -1) {
                // 右括号,出栈匹配
                System.out.println("curChar:" + curChar + ", match:" + stack.peek());
                if (!stack.pop().equals(MATCH_MAP.get(curChar))) {
                    System.out.println("Error:" + curChar + " at " + i);
                    return false;
                }
            }
        }
        // 校验栈是否已空
        if (!stack.isEmpty()) {
            System.out.println("Error: mission " + MATCH_MAP.get(stack.peek()));
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        BracketChecker bracketChecker = new BracketChecker("(a{b[c]d}e)");
        System.out.println("验证格式:" + bracketChecker.check());
    }

}
  • 运行结果
push:(
push:{
push:[
curChar:], match:[
curChar:}, match:{
curChar:), match:(
验证格式:true

扩展,使用链表去实现栈

传送门:链表相关介绍可以查看之前的文章

对比

  • 在通过数组实现栈中,pushpop是通过数组操作来完成的。
    array[++top] = data;
    data = array[top--];
  • 用链表存储数据的情况下,我们通过头插法的方式创建单链表来实现入栈和出栈
    linkList.insertFirst(data);
    linkList.deleteFirst;
  • 因为链表没有大小限制,所以链表实现的栈也没有大小限制

实现

public class StackUseLink<E{

    private LinkList<E> linkList;

    public StackUseLink() {
        this.linkList = new LinkList<>();
    }

    /**
     * 顶端插入数据
     *
     * @param data 数据
     */

    public void push(E data) {
        linkList.insertFirst(data);
    }

    /**
     * 移除顶端数据,并返回移除的数据
     *
     * @return data
     */

    public E pop() {
        return linkList.deleteFirst();
    }

    /**
     * 检查是否为空
     *
     * @return boolean
     */

    public boolean isEmpty() {
        return false;
    }

    /**
     * 内部类实现头插法单链表
     *
     * @param <E>
     */

    private static class LinkList<E{

        Node<E> first;

        void insertFirst(E data) {
            first = new Node<>(data, first);
        }

        deleteFirst() {
            if (isEmpty()) {
                System.out.println("删除失败,已经空了");
                return null;
            }
            Node<E> delNode = first;
            first = first.next;
            delNode.next = null;
            return delNode.data;
        }

        boolean isEmpty() {
            return first == null;
        }

        /**
         * 链表节点
         *
         * @param <E>
         */

        private static class Node<E{
            /**
             * 数据
             */

            E data;

            /**
             * 指向下一个节点
             */

            Node<E> next;

            Node(E data, Node<E> next) {
                this.data = data;
                this.next = next;
            }
        }

    }
}

总结

  • 只允许访问一个数据项,即最后插入的数据项,叫做栈的顶(top)
  • 属于后进先出(LIFO)
  • 通常很小,时临时的数据机构
  • push(进栈)
  • pop(出栈)
  • 可以通过数组或者链表的方式实现

访问源码

本系列所有代码均上传至Github上,方便大家访问

>>>>>> 数据结构-栈 <<<<<<

日常求赞

创作不易,如果各位觉得有帮助,求点赞支持

求关注