55 阅读9分钟

基本介绍

  1. 栈是一个先入后出(FILO-FirstInLastOut)的有序列表;
  2. 栈(stack)限制线性表中元素的插入和删除只能在线性表的同一端进行,允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom);
  3. 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除;
  4. 图解方式说明出栈(pop)和入栈(push)的概念:

image.png

数组模拟栈

实现思路

image.png

  1. 使用数组来模拟栈;
  2. 定义一个 top 来表示栈顶,初始化为 -1;
  3. 入栈的操作:当有数据加入到栈时:top++; stack[top] = data;
  4. 出栈的操作:int value = stack[top]; top--; return value;

实现逻辑

public class ArrayStack {
    // 栈的最大容量
    private int maxSize;
    // 使用数组模拟栈,数据就放在该数组
    private Integer[] stack;
    // top表示栈顶,初始化为-1
    private int top = -1;

    public ArrayStack(int maxSize) {
        this.maxSize = maxSize;
        this.stack = new Integer[maxSize];
    }

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

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

    // 入栈
    public void push(int value) {
        if (isFull()) {
            System.out.println("stack is full");
            return;
        }
        this.top++;
        this.stack[this.top] = value;
    }

    // 出栈,并回收该栈空间
    public int pop() {
        if (isEmpty()) {
            System.out.println("stack is empty");
            return -1;
        }
        int value = this.stack[this.top];
        // 回收栈空间
        this.stack[this.top] = null;
        this.top--;
        return value;
    }

    // 显示栈顶元素
    public int peek() {
        return this.stack[this.top];
    }

    // 显示栈当前空间大小
    public int size() {
        return this.top + 1;
    }

    // 遍历栈
    public void show() {
        if (isEmpty()) {
            System.out.println("stack is empty");
            return;
        }
        // 遍历时,需要从栈顶开始显示数据
        for (int i = this.top; i >= 0; i--) {
            System.out.println("stack element is " + stack[i]);
        }
    }
}

链表模拟栈

实现思路

image.png

实现逻辑

public class LinkStack {
    // 栈顶元素
    private Node top;
    // 栈大小
    private int size;

    // 创建空栈
    public LinkStack() {
        this.top = null;
        this.size = 0;
    }

    // 以value为元素创建栈
    public LinkStack(Object value) {
        this.top = new Node(value, null);
        this.size++;
    }

    // 判断是否为空栈
    public boolean isEmpty() {
        return this.size == 0;
    }

    // 获取栈长度
    public int size() {
        return this.size;
    }

    // 压栈
    public void push(Object value) {
        // 让 top 指向新创建的元素,新元素的 next 引用指向原来的栈顶元素
        this.top = new Node(value, this.top);
        this.size++;
    }

    // 出栈
    public Object pop() {
        if (isEmpty()) {
            // System.out.println("目前是空栈,没法进行出栈");
            return "error";
        }
        Node temp = this.top;
        // 更新头节点
        this.top = this.top.getNext();
        // 释放原栈顶元素的 next 引用,删除指针指向
        temp.setNext(null);
        this.size--;
        return temp.getValue();
    }

    // 访问栈顶元素
    public Object peek() {
        if (isEmpty()) {
            // System.out.println("目前是空栈,没法进行出栈");
            return "error";
        }
        return this.top.getValue();
    }

    // 遍历栈
    public void show() {
        if (isEmpty()) {
            // System.out.println("目前是空栈,没法进行出栈");
            return;
        }
        while (null != this.top) {
            System.out.println(this.top.getValue() + "\t");
            this.top = this.top.getNext();
        }
    }
}

class Node {
    private Object value;
    private Node next;

    public Node() {
    }

    public Node(Object value, Node next) {
        this.value = value;
        this.next = next;
    }
...get/set
}

注意事项

如果在出栈前就已经对链栈中的栈内元素进行打印输出,那么在执行出栈操作的时候会产生空指针异常,因为在进行打印镇内元素时,栈顶指针发生了变化,整个栈内元素打印完毕,top指针为空,此时在进行出栈时,便会产生空指针异常问题。

栈实现计算器

实现思路

image.png

实现逻辑

计算表达式的结果

private static int calculator(String expression) {
    // 创建数值栈
    ArrayStack numStack = new ArrayStack(10);
    // 创建符号栈
    ArrayStack signStack = new ArrayStack(10);
    // 定义在表达式扫描时变动的下标
    int index = 0;
    // 保存每次扫描到的字符
    char temp = ' ';
    // 用于拼接多个数字
    String keepTemp = "";
    int result = 0;
    while (true) {
        // 依次得到expression的每一个字符
        temp = expression.substring(index, index + 1).charAt(0);
        if (isSign(temp)) {
            if (signStack.isEmpty() || temp == '(') {
                // 如果当前符号栈为空,就直接入符号栈
                signStack.push(temp);
            } else if (temp == ')') {
                // 如果为右括号
                // 就需要从数值栈中pop出两个数,再从符号栈中pop出一个符号,进行运算并将结果入数值栈
                // 直到符号栈栈顶元素为左括号,弹出左括号结束
                while (signStack.peek() != '(') {
                    int num1 = numStack.pop();
                    int num2 = numStack.pop();
                    int sign = signStack.pop();
                    result = calResult(num1, num2, sign);
                    // 把运算的结果压入数值栈
                    numStack.push(result);
                }
                signStack.pop();
            } else if (signStack.peek() == '(') {
                // 如果当前符号栈栈顶元素为左括号,就直接入符号栈,[此逻辑须在右括号判断之后]
                signStack.push(temp);
            } else if (priority(temp) <= priority(signStack.peek())) {
                // 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,
                // 就需要从数值栈中pop出两个数,再从符号栈中pop出一个符号,进行运算并将结果压入数值栈
                // 然后将当前的操作符入符号栈
                int num1 = numStack.pop();
                int num2 = numStack.pop();
                int sign = signStack.pop();
                result = calResult(num1, num2, sign);
                // 把运算的中间结果压入数值栈
                numStack.push(result);
                // 将当前的操作符入符号栈
                signStack.push(temp);
            } else {
                // 如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
                signStack.push(temp);
            }
        } else {
            // 1、当处理多位数时,不能发现是一个数就立即入栈,因为它可能是多位数
            // 2、处理数值时,需要向expression的index后再看一位,如果是数就进行扫描,如果是符号才入栈
            // 3、因此我们需要定义一个变量字符串,用于拼接
            keepTemp = keepTemp + temp;
            // 如果 temp 已经是 expression 的最后一位,就直接入栈
            if (index == expression.length() - 1) {
                numStack.push(Integer.parseInt(keepTemp));
            } else if (isSign(expression.substring(index + 1, index + 2).charAt(0))) {
                // 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
                // 注意是看后一位,不是index++
                numStack.push(Integer.parseInt(keepTemp));
                keepTemp = "";
            }
        }
        // 让 index+1, 并判断是否扫描到expression最后
        index++;
        if (index >= expression.length()) {
            break;
        }
    }
    // 当表达式扫描完毕,顺序地从数栈和符号栈中 pop 出相应的数和符号,并运行
    while (true) {
        //如果符号栈为空,则计算到最后的结果, 数栈中只有一个数字【结果】
        if (signStack.isEmpty()) {
            break;
        }
        int num1 = numStack.pop();
        int num2 = numStack.pop();
        int sign = signStack.pop();
        result = calResult(num1, num2, sign);
        numStack.push(result);
    }
    // 将数栈的最后数,pop出,就是结果
    return numStack.peek();
}

返回运算符的优先级,优先级是程序员来确定,优先级使用数字表示,数字越大,则优先级就越高。

private static int priority(int sign) {
    if (sign == '(' || sign == ')') {
        return 2;
    }
    if (sign == '*' || sign == '/') {
        return 1;
    }
    if (sign == '+' || sign == '-') {
        return 0;
    }
    return -1;
}

判断是不是一个运算符

private static boolean isSign(char sign) {
    return sign == '*' || sign == '/' || sign == '+' || sign == '-' || sign == '(' || sign == ')';
}

算术运算

private static int calResult(int num1, int num2, int sign) {
    switch (sign) {
        case '+':
            return num1 + num2;
        case '-':
            return num2 - num1;
        case '*':
            return num1 * num2;
        case '/':
            return num2 / num1;
        default:
            throw new RuntimeException("sign is error");
    }
}

逆波兰表达式

逆波兰表达式又叫做后缀表达式,把运算量写在前面,把运算符写在后面。中缀表达式就是我们平时书写的数学表达式,例如:(2 + 3) * 5,转为逆波兰表达式为:2 3 5 + *。

转逆波兰表达式

实现思路

中缀表达式转逆波兰表达式,步骤如下:

  1. 初始化两个栈:运算符栈s1和储存中间结果的栈s2;

  2. 从左至右扫描中缀表达式;

  3. 遇到操作数时,将其压s2;

  4. 遇到运算符时,比较其与s1栈顶运算符的优先级:

    4.1. 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;

    4.2. 否则,若优先级比栈顶运算符的高,也将运算符压入s1;

    4.3. 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;

  5. 遇到括号时:

    5.1. 如果是左括号“(”,则直接压入s1;

    5.2. 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃

  6. 重复步骤2至5,直到表达式的最右边

  7. 将s1中剩余的运算符依次弹出并压入s2

  8. 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。

将中缀表达式 1+((2+3)×4)-5 转换为后缀表达式的过程如下:

image.png

逆波兰表达式求值

实现思路

(3+4)×5-6 对应的后缀表达式就是:3 4 + 5 × 6 -,针对后缀表达式求值步骤如下:

  1. 从左至右扫描,将3和4压入堆栈;

  2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;

  3. 将5入栈;

  4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;

  5. 将6入栈;

  6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

实现逻辑

将中缀表达式拆分放入list集合

private static List toInfix(String str) {
    List<String> result = new ArrayList<>();
    int startIndex = 0;
    int endIndex = 0;
    while (endIndex != str.length()) {
        // 遍历到符号前,把符号前的(endIndex-startIndex)之间的数值加入到结果集合中
        // 否则将符号直接加入结果集合中
        if (str.charAt(endIndex) < 48 || str.charAt(endIndex) > 57) {
            String substring = str.substring(startIndex, endIndex);
            if (!Strings.isNullOrEmpty(substring)) {
                result.add(substring);
            }
            result.add(str.charAt(endIndex) + "");
            startIndex = endIndex + 1;
        }
        endIndex++;
    }
    String substring = str.substring(startIndex, endIndex);
    if (!Strings.isNullOrEmpty(substring)) {
        result.add(substring);
    }
    return result;
}

将中缀表达式转为逆波兰表达式

private static List<String> toSuffix(List<String> list) {
    // 存放操作符的栈
    Stack<String> stack = new Stack();
    // 储存结果的栈
    List<String> result = new ArrayList<>();
    for (String temp : list) {
        if (temp.matches("\d+")) {
            result.add(temp);
        } else if (temp.equals("(")) {
            stack.push(temp);
        } else if (temp.equals(")")) {
            // 如果是右括号“)”,则依次弹出操作符栈顶的运算符,并压入结果栈,直到遇到左括号为止,
            // 此时将这一对括号丢弃
            while (!stack.peek().equals("(")) {
                result.add(stack.pop());
            }
            // 弹出操作符栈,消除小括号
            stack.pop();
        } else {
            // 当temp的优先级小于等于符号栈栈顶运算符, 将符号栈栈顶的运算符弹出并加入到结果栈中,
            // 再次转到(4.1步骤),与符号栈中新的栈顶运算符相比较
            while (stack.size() != 0 && compare(stack.peek()) >= compare(temp)) {
                result.add(stack.pop());
            }
            stack.push(temp);
        }
    }
    // 将符号栈中剩余的运算符依次弹出并加入结果栈
    while (stack.size() != 0) {
        result.add(stack.pop());
    }
    return result;
}

逆波兰表达式运算

private static String calStack(List<String> toSuffix) {
    Stack<String> stack = new Stack();
    for (String temp : toSuffix) {
        // // 匹配的是多位数,入栈
        if (temp.matches("\d+")) {
            stack.push(temp);
        } else {
            // pop出两个数,并运算,再入栈
            int temp1 = Integer.parseInt(stack.pop());
            int temp2 = Integer.parseInt(stack.pop());
            stack.push(String.valueOf(cal(temp1, temp2, temp)));
        }
    }
    return stack.pop();
}

应用场景

  1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

  2. 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。

  3. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。

  4. 二叉树的遍历。

  5. 图形的深度优先(depth-first)搜索法。

  6. JDK 中的 Stack:blog.csdn.net/qq_42859864…