栈术高超:栈的应用场景

108 阅读10分钟

1. 反转字符串

package com.yc.huke;

import com.sun.deploy.util.StringUtils;

import javax.xml.stream.events.Characters;
import java.util.Stack;

/**
 * @Description: 用栈来反转字符串
 * @Author: Andy
 * @Date: 2023/6/15 11:59
 */
public class UseByStack1 {
    public static void main(String[] args) {
        String str1 = "I Love you, hello world!";
        System.out.println(getReversalStrByStack(str1));
        String str2 = "我爱中国";
        System.out.println(getReversalStrByStack(str2));
    }

    public static String getReversalStrByStack(String str) {
        char[] chars = str.toCharArray();
        Stack<Character> stack = new Stack<>();
        for (char aChar : chars) {
            stack.push(aChar);
        }
        StringBuffer stringBuffer = new StringBuffer();
        while (!stack.isEmpty()){
           stringBuffer.append(stack.pop());
        }
        return stringBuffer.toString();
    }
}

2. 判断括号是否匹配

写过xml标签或者html标签的,我们都知道"<"必须和最近的">"进行匹配,"[" 也必须和最近的 "]" 进行匹配。

比如:<abc[123]abc>这是符号相匹配的,如果是 <abc[123>abc] 那就是不匹配的。

对于 12<a[b{c}]>,我们分析在栈中的数据:遇到匹配正确的就消除

最后栈里的内容是空的就代表匹配成功,否则就匹配失败。

package com.yc.huke;

import java.util.Stack;

/**
 * @Description: 
 * @Author: Andy
 * @Date: 2023/6/15 19:29
 */
public class BracketMatch {
    public static void main(String[] args) {
        String str = "12<a[b{c}]>";
        System.out.println(isBracketMatch(str));
        String str1 = "<<abc>]>123";
        System.out.println(isBracketMatch(str1));
        // 特殊情况 右括号先出现
        String str2 = "a]<<abc>]>123";
        System.out.println(isBracketMatch(str2));
    }

    /**
     * 判断字符串中括号是否匹配
     */
    public static boolean isBracketMatch(String str) {
        char[] chars = str.toCharArray();
        Stack<Character> stack = new Stack<>();
        for (char aChar : chars) {
            // 如果是左边的括号则入栈
            if (aChar == '<' || aChar == '[' || aChar == '{') {
                stack.push(aChar);
            } else if (aChar == '>' || aChar == ']' || aChar == '}') {
                // 如果是右边的括号,则判断是否是空栈,若是空栈则说明先出现右括号,很明显不符合匹配规则
                if (stack.isEmpty()) {
                    return false;
                } else {
                    // 如果是不是空栈,那就读取栈顶元素,判断是否和当前要入栈的右括号成对,成对则将栈顶元素出栈,不成对则说明不符合匹配规则
                    Character top = stack.peek();
                    if ((aChar == '>' && top == '<') || (aChar == ']' && top == '[') || (aChar == '}' && top == '{')) {
                        stack.pop();
                    } else {
                        return false;
                    }
                }
            }
        }
        // 如果最终是空栈,则说明所有括号均成对抵消了,符合匹配规则
        return stack.isEmpty();
    }
}

3. 进制转换

package com.yc.huke;

import java.util.Stack;

/**
 * @Description: 用栈来实现进制转换
 * @Author: Andy
 * @Date: 2023/6/15 20:21
 */
public class BaseConversion {
    public static void main(String[] args) {
        // 10进制转8进制
        System.out.println("159的8进制是:"+handleBaseConversion(159));
    }
    public static int handleBaseConversion(int i){
        int quotient;
        int remainder;
        Stack<Integer> stack = new Stack<>();
        while (true){
            quotient = i / 8;
            remainder = i % 8;
            stack.push(remainder);
            if (quotient == 0){
                break;
            }
            i = quotient;
        }
        StringBuffer stringBuffer = new StringBuffer();
        while (!stack.isEmpty()){
            stringBuffer.append(stack.pop());
        }
        String string = stringBuffer.toString();
        return Integer.valueOf(string);
    }
}

4. 表达式解析

1、人类如何解析算术表达式

如何解析算术表达式?或者换种说法,遇到某个算术表达式,我们是如何计算的:

①、求值 3+4-5

②、求值 3+4*5

2、计算机如何解析算术表达式

对于前面的表达式 3+4-5,我们人是有思维能力的,能根据操作符的位置,以及操作符的优先级别能算出该表达式的结果。但是计算机怎么算?

先看看什么是前缀表达式,中缀表达式,后缀表达式:这三种表达式其实就是算术表达式的三种写法,以 3+4-5为例

①、前缀表达式:操作符在操作数的前面,是一种没有括号的算术表达式,例如,- 1 + 2 3,它等价于1-(2+3)

②、中缀表达式:操作符在操作数的中间,这也是人类最容易识别的算术表达式 3+4-5

③、后缀表达式:操作符在操作数的后面,是一种没有括号的算术表达式,比如 34+5-,等价于(3+4)-5

上面我们讲的人是如何解析算术表达式的,也就是解析中缀表达式,这是人最容易识别的,但是计算机不容易识别,所以我们程序员如果要写计算表达式的程序,就应该先把中缀表达式在程序内部转换为前缀或后缀表达式!怎么转呢?

3、以后缀表达式为例子

后缀表达式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则)。

由于后缀表达式的运算符在两个操作数的后面,那么计算机在解析后缀表达式的时候,只需要从左向右扫描,也就是只需要向前扫描,而不用回头扫描,遇到运算符就将运算符放在前面两个操作符的中间(这里先不考虑乘方类似的单目运算),一直运算到最右边的运算符,那么就得出运算结果了。这样实现的逻辑就简单了,在计算机底层的硬件实现就要简单的多了!

①、如何将中缀表达式转换为后缀表达式(逆波兰表达式)

对于这个问题,转换的规则如下:

1、初始化两个栈:运算符栈s1,中间结果栈s2

2、从左到右扫描表达式

3、遇到操作数压入s2

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

a、如果s1栈不是空的,并且栈顶元素不是“( ”, 并且栈顶元素优先级大于或等于当前元素,就弹出s1栈顶的元素压入s2栈,再把当前元素压入s1

b、否则,就把当前元素直接压入栈s1

5、遇到括号

a、遇到左括号( ,直接压入s1栈

b、遇到右括号 ),依次弹出s1栈顶的元素,压入s2栈,知道遇到左括号为止,然后将这对括号丢掉

6、重复2-5,扫描完所有表达式

7、将s1中剩余的运算符压入s2栈

package com.yc.huke;

import java.util.Stack;

/**
 * @Description: 中缀表达式转后缀表达式(逆波兰表达式)
 * @Author Andy
 * @Date 2023/6/16 18:34
 */
public class PolandExpression {
    public static void main(String[] args) {
        String expression = "(34.6+56.2+58)*63-69";
        middleToSuffix(expression);
    }

    /**
     * 中缀转后缀
     * @param expression
     */
    public static void middleToSuffix(String expression){
        // 运算符栈
        Stack<Character> s1 = new Stack<>();
        // 中间结果栈
        Stack<String> s2 = new Stack<>();
        int length = expression.length();
        int i;
        for (i = 0; i < length; i++) {
            if (Character.isDigit(expression.charAt(i))){
                // 如果是数字则压入s2
                // 但是有些数字不止一位,需要截取(截取到下一个非数字的字符)
                String operand = "";
                int j;
                for (j = i+1; j < length; j++) {
                    if (!Character.isDigit(expression.charAt(j)) && '.' != expression.charAt(j)){
                        operand = expression.substring(i, j);
                        // 下次从j开始遍历
                        i = j-1;
                        break;
                    }
                }
                if (j == length){
                    // 说明此数字后面的所有内容是一个数,从i开始截取后面全部
                    operand = expression.substring(i);
                    i = j -1;
                }
                s2.push(operand);
            } else if (isOperator(expression.charAt(i))) {
                // 如果是运算符,比较其与s1栈顶运算符的优先级
                if (!s1.isEmpty() && s1.peek() != '(' && priorityCompare(s1.peek(), expression.charAt(i)) >= 0){
                    // 如果s1栈不是空的,并且栈顶元素不是“( ” ,并且优s1栈顶元素优先级大于等于当前元素,则将s1的栈顶元素弹出压入s2,再把当前元素压入s1
                    s2.push(String.valueOf(s1.pop()));
                }
                // 否则,就直接压入栈s1
                s1.push(expression.charAt(i));
            } else if (expression.charAt(i) == '(') {
                // 遇到左括号( ,直接压入s1栈
                s1.push(expression.charAt(i));
            } else if (expression.charAt(i) == ')') {
                while (true){
                    if (!s1.isEmpty() && s1.peek() != '('){
                        s2.push(String.valueOf(s1.pop()));
                    }else if (s1.peek() == '('){
                        s1.pop();
                        break;
                    }
                }
            }
        }
        // 扫描完表达式所有内容后,将s1中剩余的运算符压入s2栈
        while (!s1.isEmpty()){
            s2.push(String.valueOf(s1.pop()));
        }
        while (!s2.isEmpty()){
            System.out.print(s2.pop() + " ");
        }
    }

    /**
     * 比较优先级
     * @param before
     * @param after
     * @return
     */
    private static int priorityCompare(char before, char after) {
        if (before == '+' || before == '-') {
            if (after == '*' || after == '/') {
                return -1;
            }
        } else if (before == '*' || before == '/') {
            if (after == '+' || after == '-') {
                return 1;
            }
        }
        return 0;
    }

    /**
     * 是否是运算符(只支持 + - * /)
     * @param character
     * @return
     */
    public static boolean isOperator(Character character) {
        if (character == '+' || character == '-' || character == '*' || character == '/') {
            return true;
        }
        return false;
    }

}

②、在后缀表达式的基础上实现计算

只需要从左向右扫描,遇到运算符就将运算符放在前面两个操作符的中间,一直运算到最右边的运算符,那么就得出运算结果了。

package com.yc.huke;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;

/**
 * @Description: 中缀表达式转后缀表达式(逆波兰表达式)
 * @Author Andy
 * @Date 2023/6/16 18:34
 */
public class PolandExpression {
    public static void main(String[] args) {
        String expression = "(34.6+56.2+58)*63-69";
        System.out.println(middleToSuffix(expression));
    }

    /**
     * 中缀转后缀 再计算结果输出
     * @param expression
     */
    public static String middleToSuffix(String expression){
        // 运算符栈
        Stack<Character> s1 = new Stack<>();
        // 中间结果栈
        Stack<String> s2 = new Stack<>();
        Double result = null;
        List<Double> doubles = new ArrayList<>();
        int length = expression.length();
        int i;
        for (i = 0; i < length; i++) {
            if (Character.isDigit(expression.charAt(i))){
                // 如果是数字则压入s2
                // 但是有些数字不止一位,需要截取(截取到下一个非数字的字符)
                String operand = "";
                int j;
                for (j = i+1; j < length; j++) {
                    if (!Character.isDigit(expression.charAt(j)) && '.' != expression.charAt(j)){
                        operand = expression.substring(i, j);
                        // 下次从j开始遍历
                        i = j-1;
                        break;
                    }
                }
                if (j == length){
                    // 说明此数字后面的所有内容是一个数,从i开始截取后面全部
                    operand = expression.substring(i);
                    i = j -1;
                }
                s2.push(operand);
            } else if (isOperatorChar(expression.charAt(i))) {
                // 如果是运算符,比较其与s1栈顶运算符的优先级
                if (!s1.isEmpty() && s1.peek() != '(' && priorityCompare(s1.peek(), expression.charAt(i)) >= 0){
                    // 如果s1栈不是空的,并且栈顶元素不是“( ” ,并且优s1栈顶元素优先级大于等于当前元素,则将s1的栈顶元素弹出压入s2,再把当前元素压入s1
                    s2.push(String.valueOf(s1.pop()));
                    Double calculate = calculate(s2.pop().charAt(0), Double.valueOf(s2.pop()), Double.valueOf(s2.pop()));
                    s2.push(String.valueOf(calculate));
                }
                // 否则,就直接压入栈s1
                s1.push(expression.charAt(i));
            } else if (expression.charAt(i) == '(') {
                // 遇到左括号( ,直接压入s1栈
                s1.push(expression.charAt(i));
            } else if (expression.charAt(i) == ')') {
                while (true){
                    if (!s1.isEmpty() && s1.peek() != '('){
                        s2.push(String.valueOf(s1.pop()));
                        Double calculate = calculate(s2.pop().charAt(0), Double.valueOf(s2.pop()), Double.valueOf(s2.pop()));
                        s2.push(String.valueOf(calculate));
                    }else if (s1.peek() == '('){
                        s1.pop();
                        break;
                    }
                }
            }
        }
        // 扫描完表达式所有内容后,将s1中剩余的运算符压入s2栈
        while (!s1.isEmpty()){
            s2.push(String.valueOf(s1.pop()));
        }
        Double calculate = calculate(s2.pop().charAt(0), Double.valueOf(s2.pop()), Double.valueOf(s2.pop()));
        s2.push(String.valueOf(calculate));
        return s2.peek();
    }

    /**
     * 比较优先级
     * @param before
     * @param after
     * @return
     */
    private static int priorityCompare(char before, char after) {
        if (before == '+' || before == '-') {
            if (after == '*' || after == '/') {
                return -1;
            }
        } else if (before == '*' || before == '/') {
            if (after == '+' || after == '-') {
                return 1;
            }
        }
        return 0;
    }

    /**
     * 是否是运算符(只支持 + - * /)
     * @param character
     * @return
     */
    public static boolean isOperatorChar(Character character) {
        if (character == '+' || character == '-' || character == '*' || character == '/') {
            return true;
        }
        return false;
    }
    public static boolean isOperatorStr(String str) {
        if ("+".equals(str) || "-".equals(str) || "*".equals(str) || "/".equals(str)) {
            return true;
        }
        return false;
    }
    public static Double calculate(Character c, Double after,  Double before){
        Double result = 0d;
        switch (c){
            case '+':
                result = before + after;
                break;
            case '-':
                result = before - after;
                break;
            case '*':
                result = before * after;
                break;
            case '/':
                result = before / after;
                // TODO 分子不为0
                break;
        }
        return result;
    }

}

以上代码不能识别负数的情况,下面是一个适配负数计算的代码,同时也是牛客算法题 www.nowcoder.com/practice/95… 的一种解法:

import java.util.Scanner;
import java.util.Stack;


public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String expression = in.nextLine();
        Stack<Character> s1 = new Stack<>();
        Stack<String> s2 = new Stack<>();
        String number = "";
        // 遍历表达式的每个字符,将中缀转化成后缀表达式
        for (int i = 0; i < expression.length(); i++) {
            if (isOperator(expression.charAt(i), i, expression)) {
                while (!s1.isEmpty() && s1.peek() != '(' &&
                       priorityCompare(s1.peek(), expression.charAt(i)) >= 0) {
                    s2.add(String.valueOf(s1.pop()));
                }
                s1.add(expression.charAt(i));
            } else if (expression.charAt(i) == '(') {
                s1.add(expression.charAt(i));
            } else if (expression.charAt(i) == ')') {
                while (s1.peek() != '(') {
                    s2.add(String.valueOf(s1.pop()));
                }
                s1.pop();
            } else if (Character.isDigit(expression.charAt(i))
                       || (expression.charAt(i) == '-' && (i == 0 ||
                                                           isNegativeNumberStart(i, expression)))) {
                // 当前字符是数字或者负号时
                number = String.valueOf(expression.charAt(i));
                for (int j = i + 1; j < expression.length(); j++) {
                    if (!Character.isDigit(expression.charAt(j))) {
                        break;
                    }
                    number = number + String.valueOf(expression.charAt(j));
                    i++;
                }
                s2.add(number);
            }
        }
        while (!s1.isEmpty()) {
            s2.add(String.valueOf(s1.pop()));
        }
        // 此时s2里就是我们已经转化好的后缀表达式,但是由于要从栈尾开始遍历,但栈只能从栈顶开始遍历,
        // 所以这里再遍历s2换到新的栈s3,此时s3的栈顶就是s2的栈尾,正常遍历s3就可以了(当然也可以使用双端栈就不用这么麻烦了)
        Stack<String> s3 = new Stack<>();
        while (!s2.isEmpty()) {
            s3.add(s2.pop());
        }
        Stack<Integer> s4 = new Stack<>();
        while (!s3.isEmpty()) {
            // 如果当前元素长度大于1,则必定是数字,比如负数-1 或 多位数11
            // 如果当前元素长度是1且isDigit==true,必定是数字
            if (s3.peek().length() > 1 || (s3.peek().length() == 1 &&
                                           Character.isDigit(s3.peek().charAt(0)))) {

                s4.add(Integer.parseInt(s3.pop()));
            } else {
                // 如果遇到运算符,则弹出栈里前两位元素进行运算
                s4.add(calculate(s4.pop(), s4.pop(), s3.pop().charAt(0)));
            }
        }
        System.out.println(s4.peek());
    }
    // 比较运算符的优先级
    public static int priorityCompare(char before, char after) {
        if (before == '+' || before == '-') {
            if (after == '*' || after == '/') {
                return -1;
            }
        } else if (before == '*' || before == '/') {
            if (after == '+' || after == '-') {
                return 1;
            }
        }
        return 0;
    }
    // 判断当前字符是否为运算符
    public static boolean isOperator(char c, int index, String expr) {
        if (c == '+' || c == '*' || c == '/') {
            return true;
        }
        // 如果是-,且不是首位,且不是负号,则说明是减号
        if (c == '-' && (index > 0 && !isNegativeNumberStart(index, expr))) {
            return true;
        }
        return false;
    }

    /**
     * 判断当前-字符是否为负号
     * @param index 当前字符的下标
     * @param expr 表达式
     * @return
     */
    public static boolean isNegativeNumberStart(int index, String expr) {
        // 如果-在首位,那么必定是负号而不是减号
        if (index == 0) {
            return true;
        }
        // 既然不是首位,那么获取前一位的字符
        char prevChar = expr.charAt(index - 1);
        // 如果前一位是以下字符则说明是负号而不是减号
        // 比如:1+(-1)、1+-1、1--1、1*-1、1/-1 这些都是负号前一位字符的特征
        return prevChar == '(' || prevChar == '+' || prevChar == '-' ||
               prevChar == '*' || prevChar == '/';
    }
    // 计算
    public static int calculate(int first, int second, char operator) {
        int result = 0;
        switch (operator) {
            case '+':
                result = first + second;
                break;
            case '-':
                // 注意这里是后一位 减去 前一位
                result = second - first;
                break;
            case '*':
                result = first * second;
                break;
            case '/':
                // 注意这里是后一位 除以 前一位
                result = second / first;
                break;
        }
        return result;
    }
}

以上就是栈这种数据结构的四种常见用法,当然栈的运用远不止以上四种,欢迎补充。