栈的实现、应用

224 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

本文学习栈,数据结构、设计实现、基本使用。

简介

栈是一种用于存储数据的简单数据结构,只允许在一端进行插入和删除元素的线性表。整体表现为一个后进先出的数据结构(Last IN First Out 【LIFO】)。栈的基本操作包括初始化栈,栈判空,入栈,出栈,获取栈顶元素等,注意栈不支持对指定位置进行删除,插入。

名词&概念

栈顶(Top)

允许进行插入和删除元素的一端称为栈顶。

栈底(Bottom)

不可操作的一端称为栈底

空栈

不包含任何元素的栈

入栈(Push)

元素加入栈的操作称为入栈。

出栈(Pop)

弹出栈顶元素的操作称为出栈。

栈的数据结构如下: image-20220521173320667.png

stack类

java为我们提供的基于Vector实现的栈。

 package java.util;
 public
 class Stack<E> extends Vector<E> {
     public Stack() {
     }
     //入栈操作
     public E push(E item) {
         addElement(item);
         return item;
     }
     //出栈操作
     public synchronized E pop() {
         E obj;
         int len = size();
         obj = peek();
         removeElementAt(len - 1);
         return obj;
     }
     //获取元素操作,注意里不会对栈数据结构做修改
     public synchronized E peek() {
         int len = size();
         if (len == 0)
             throw new EmptyStackException();
         return elementAt(len - 1);
     }
     //栈判空
     public boolean empty() {
         return size() == 0;
     }
     //返回o对应下标
     public synchronized int search(Object o) {
         int i = lastIndexOf(o);
         if (i >= 0) {
             return size() - i;
         }
         return -1;
     }
 }
  • 首先,这个类也不太使用,基于Vector实现,synchronized保证同步
  • 依赖Vector实现,主要逻辑都在Vector定义,stack相当于做一个封装
  • 这是一个顺序栈

顺序栈的设计与实现

采用顺序表实现栈,这里使用数组是实现。先声明一个顺序栈接口其代码如下。

 public interface Stack<T> {
     /**
      * 栈判空
      *
      * @return boolean
      */
     boolean isEmpty();
     /**
      * 入栈
      *
      * @param t
      */
     void push(T t);
     /**
      * 获取栈顶元素,未出栈
      *
      * @return t
      */
     T peek();
     /**
      * 出栈
      *
      * @return t
      */
     T pop();
     /**
      * 返回栈元素个数
      *
      * @return int
      */
     int size();
     /**
      * 清除栈
      */
     void clear();
 }

自定义顺序栈实现它和序列化接口

public class ArrayStack<T> implements Stack<T>, Serializable {

    //序列号
    private static final long serialVersionUID = 6348894725654782270L;

    //栈元素个数
    private int size;

    // 容量大小默认为10
    private int capacity = 10;

    //栈顶指针,-1代表空栈
    private int top = -1;

    //存放元素的数组
    private T[] element;

    public ArrayStack() {
        element = (T[]) new Object[capacity];
    }

    public ArrayStack(int capacity) {
        this.capacity = capacity;
        element = (T[]) new Object[capacity];
    }

    @Override
    public boolean isEmpty() {
        return size() <= 0;
    }

    @Override
    public void push(T t) {
        //确保容量够
        ensureCapacity();
        element[++top] = t;
        size++;
    }

    @Override
    public T peek() {
        //栈判空
        if (isEmpty())
            throw new EmptyStackException();
        return element[top];
    }

    @Override
    public T pop() {
        //栈判空
        if (isEmpty())
            throw new EmptyStackException();
        size--;
        return element[top--];
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void clear() {
        Arrays.fill(element, null);
        size = 0;
        top = -1;
    }

    //确保容量足够
    public void ensureCapacity() {
        final int oldCapacity = capacity;
        int newCapacity;
        if (oldCapacity > size())
            return;
        //扩容
        newCapacity = oldCapacity * 2 + 1;
        element = Arrays.copyOf(element, newCapacity);
    }

    public void foreach(Consumer<T> action) {
        for (int i = 0; i < size; i++) {
            action.accept(element[i]);
        }
    }
}

测试:

@Test
public void test01() {
    ArrayStack<String> aStack = new ArrayStack<>();
    //入栈
    aStack.push("a");
    aStack.push("b");
    aStack.push("c");
    //遍历方法
    aStack.foreach((element)->{
        System.out.printf(element+",");
    });
    System.out.println();
    //出栈
    while (!aStack.isEmpty()){
        System.out.println(aStack.pop());
    }

}

image-20220521170040843.png


链式栈的设计与实现

image-20220521175542979.png

public class LinkedStack<T> implements Stack<T>, Serializable {
    @Data
    @AllArgsConstructor
    class Node {
        T value;
        Node next;
    }

    //序列号
    private static final long serialVersionUID = 6348894725654782270L;

    //栈元素个数
    private int size;

    //栈顶引用 为空代表 空栈
    private Node top = null;

    public LinkedStack() {

    }

    @Override
    public boolean isEmpty() {
        return size() <= 0;
    }

    @Override
    public void push(T t) {
        top = new Node(t, top);
        size++;
    }

    @Override
    public T peek() {
        //栈判空
        if (isEmpty())
            throw new EmptyStackException();
        return top.getValue();
    }

    @Override
    public T pop() {
        //栈判空
        if (isEmpty())
            throw new EmptyStackException();
        T value = top.getValue();
        top = top.next;
        size--;
        return value;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void clear() {
        //栈判空
        if (isEmpty()) return;
        top = null;
        size = 0;
    }

    public void foreach(Consumer<T> action) {
        Node topT = top;
        //栈判空
        if (isEmpty()) return;
        do {
            action.accept(topT.value);
        } while ((topT = topT.next) != null);
    }
}

测试:

@Test
public void test(){

    LinkedStack<Object> lStack = new LinkedStack<>();

    lStack.push(1);
    lStack.push(2);
    lStack.push(3);

    lStack.foreach((element)->{
        System.out.println(element);
    });

    System.out.println("----");

    while (!lStack.isEmpty()){
        System.out.println(lStack.pop());
    }

}

栈的应用

  • 符号匹配
    • 括号成对出现
    • html和xml标签匹配
  • 中缀表达式转后缀表达式
  • 计算后缀表达式
  • 函数嵌套引用
符号匹配

在编写代码时,编译会检查语法格式,其中关于符号匹配的就是利用栈来实现的。

比如说:

((5-3)*4-1  ##这就是不符合规则的
((5-3)*4-1) ##这样才会通过校验

匹配思路

出现左括号则入栈,出现右括号则左括号出栈,检验结束,栈内存在元素则匹配失败,不存在元素则匹配成功。

图示: image-20220522232350718.png 代码实现:

public class SymbolMatch {

    ArrayStack<Character> stack = new ArrayStack<>();

    /**
     * 符号匹配这里以()为例,比如 (1*2)
     *
     * 例子:((5-3)*4-1)
     */
    public boolean check(String expression){

        char[] chars = expression.toCharArray();

        for (Character aChar : chars) {

            if (aChar.equals('('))
                stack.push(aChar);

            if (aChar.equals(')'))
                stack.pop();
        }
        return stack.isEmpty();
    }

    public void clearStack(){
        stack.clear();
    }
    //测试
    public static void main(String[] args) {

        String expression01 = "((5-3)*4-1)";
        String expression02 = "((5-3)*4-1";
        String expression03 = "5-3*4-1";

        SymbolMatch symbolMatch = new SymbolMatch();

        System.out.println(expression01+"是否匹配成功=>"+symbolMatch.check(expression01));
        symbolMatch.clearStack();
        System.out.println(expression02+"是否匹配成功=>"+symbolMatch.check(expression02));
        symbolMatch.clearStack();
        System.out.println(expression03+"是否匹配成功=>"+symbolMatch.check(expression03));
        symbolMatch.clearStack();
    }

}

image-20220522232932255.png


中缀表达式转后缀表达式
  • 中缀表达式

    日常使用的表达式,如:9-3*(1+2)+9。

  • 后缀表达式

    计算机认识的表达式,如:9 3 1 2 - * + 9 +。

设计与实现

栈A用于管理运算符优先级、数组B用于存储转换后的后缀表达式:

  • 如果遇到操作数,我们就直接将其放入数组B中。
  • 遇到运算符,如(+-*/(等),从栈中弹出元素存入数组B直到遇到发现更低优先级的元素为止,并将其压入栈A中。只有在遇到)时才弹出(,弹出的(不会加入数组B。
  • 如果我们读到了末尾,则将栈中剩下的所有元素依次弹出并存入到数组B中。
public class ExpressionInfixToSuffix {
    //协助栈A
    ArrayStack<Character> aStack = new ArrayStack<>();

    public String infixToSuffix(String express) {
        //存放后缀表达式的数组B
        Character[] b;
        char[] chars = express.toCharArray();
        b = new Character[express.length()];
        //数组b下标
        int arrayBIndex = 0;
        for (Character aChar : chars) {
            //如果式操作数,直接加入数组
            if (aChar.compareTo('9') <= 0 && aChar.compareTo('0') >= 0){
                b[arrayBIndex++] = aChar;
                //进行下一次循环
                continue;
            }
            /**
             * + - 号优先级最低。
             * ①弹出(之前的所有 运算符
             * ②压栈
             */
            if(aChar.equals('+') || aChar.equals('-')){
                Character temp;
                //栈不为空、且不是 (
                while (!aStack.isEmpty() && !(temp = aStack.peek()).equals('(')){
                    //弹栈、加入数组B
                    aStack.pop();
                    b[arrayBIndex++] = temp;
                }
                //否则压栈
                aStack.push(aChar);
                //进行下一次循环
                continue;
            }
            /**
             * * / 号优先级比 + - 高。
             * ①弹出(之前的的所有 * / 运算符
             * ②压栈
             */
            if(aChar.equals('*') || aChar.equals('/')){
                Character temp;
                //栈不为空、且弹出的不是(、且是 * 或/
                while (!aStack.isEmpty() && !(temp = aStack.peek()).equals('(') && (temp.equals('*') || temp.equals('/'))){
                    //弹栈、加入数组B
                    aStack.pop();
                    b[arrayBIndex++] = temp;
                }
                //否则压栈
                aStack.push(aChar);
                //进行下一次循环
                continue;
            }
            //如果是(  直接入栈
            if(aChar.equals('(')){
                aStack.push(aChar);
                //进行下一次循环
                continue;
            }

            /**
             * 如果是)括号
             * 弹出所有(之前的
             */
            if(aChar.equals(')')) {
                Character temp;
                //栈不为空、且弹出的不是(
                while (!aStack.isEmpty() && !(temp = aStack.pop()).equals('(')){
                    //加入数组B
                    b[arrayBIndex++] = temp;
                }
                continue;
            }
        }
        //遍历结束,将A栈中剩余的,弹出来放入数组B

        //栈不为空、且弹出的不是(
        while (!aStack.isEmpty()){
            //加入数组B
            b[arrayBIndex++] = aStack.pop();
        }
        StringBuffer sb = new StringBuffer();
        for (Character character : b)
            sb.append(ObjectUtils.isEmpty(character)?"":character);

        return sb.toString();
    }
}

测试:

@Test
public void test(){
    SymbolMatch symbolMatch = new SymbolMatch();
    ExpressionInfixToSuffix expressionInfixToSuffix = new ExpressionInfixToSuffix();
	String express = "6-3*(5+2)-9+(5-4)";
    //符号检验
    if (symbolMatch.check(express))
        System.out.println(express+"转化为后缀表达式=>"+expressionInfixToSuffix.infixToSuffix(express));
    else
        System.out.println("符号监测错误");
}

image-20220523010908574.png


计算后缀表达式
  • 如果字符是数字,转换为数字再加入栈
  • 如果是运算符(后缀表达式已经去除掉括号),则弹出栈前两个元素并与运算符计算,将计算后的结果入栈。
  • 最终栈内元素即是表达式结果
public class CalculateSuffix {
    ArrayStack<Integer> stack = new ArrayStack<>();

    public Integer calculate(String suffix) {
        char[] chars = suffix.toCharArray();
        Integer first;
        Integer second;
        Integer finalValue = null;
        for (Character aChar : chars) {

            //如果是数字,转化为数字,压栈
            if (aChar.compareTo('9') <= 0 && aChar.compareTo('0') >= 0) {
                stack.push(Integer.valueOf(aChar.toString()));
                continue;
            } else {
                first = stack.pop();
                second = stack.pop();

                switch (aChar) {
                    case '+':
                        finalValue = second + first;
                        break;
                    case '-':
                        finalValue = second - first;
                        break;
                    case '*':
                        finalValue = second * first;
                        break;
                    case '/':
                        finalValue = second / first;
                        break;
                    default:
                        break;
                }
                stack.push(finalValue);
            }
        }

        return stack.pop();
    }
}

测试:

@Test
public void test() {
    SymbolMatch symbolMatch = new SymbolMatch();
    ExpressionInfixToSuffix expressionInfixToSuffix = new ExpressionInfixToSuffix();
    CalculateSuffix calculateSuffix = new CalculateSuffix();
    String express = "6-3*(5+2)-9+(5-4)";
    //符号检验
    if (symbolMatch.check(express)) {
        String suffix;
        System.out.println(express + "转化为后缀表达式=>" + (suffix = expressionInfixToSuffix.infixToSuffix(express)));

        System.out.println("结果为=>" + calculateSuffix.calculate(suffix));
    } else
        System.out.println("符号监测错误");
}

image-20220523013921621.png


基于Stack类的思考

其实,上面我们的两种栈的实现,很多逻辑都是重新写的,但是java为我们提供的很多容器类可以帮我们实现。顺序栈就可以用ArrayQueue,链式栈就可以用LinkedList。


借助ArrayList实现顺序栈

代码也很简洁

public class ArrayStackByArrayList<T> extends ArrayList<T> implements Stack<T> {

    private static final long serialVersionUID = 893214352865117027L;

    @Override
    public void push(T t) {
        super.add(t);
    }

    @Override
    public T peek() {
        return get(size()-1);
    }

    @Override
    public T pop() {
        return remove(size()-1);
    }
}

测试:

@Test
public void test02() {
    Stack<String> aStack = new ArrayStackByArrayList<>();
    //入栈
    aStack.push("a");
    aStack.push("b");
    aStack.push("c");

    //出栈
    while (!aStack.isEmpty()) {
        System.out.println(aStack.pop());
    }

}

image-20220523020546158.png

借助LinkedList实现链式栈

发现什么都不用做,相当于给LinkedList做封装

public class LinkedStackByLinkedList<T> extends LinkedList<T> implements Stack<T> {

    private static final long serialVersionUID = 895807720383163370L;

}

测试:

@Test
public void test2(){
    Stack<Object> lStack = new LinkedStackByLinkedList<>();
    lStack.push(1);
    lStack.push(2);
    lStack.push(3);
    while (!lStack.isEmpty()){
        System.out.println(lStack.pop());
    }
}

image-20220523020923576.png