栈(Java中缀及后缀表达式计算器实现)
4、栈
基本概念
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的
一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。 - 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
素最先删除,最先放入的元素最后删除。
应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以
回到原来的程序中。 - 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆
栈中。 - 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一first)搜索法。
数组模拟栈的实现
- 使用数组模拟。
- 定义一个top表示栈顶,初始化为-1。
//用数组模拟栈
public class ArrayStackTest {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(10);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
stack.show();
stack.pop();
stack.pop();
stack.show();
}
}
class ArrayStack {
private int maxSize;//定义栈的大小
private int top = -1;//top表示栈顶,初始化为-1
private int[] stack;//用数组模拟栈
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack=new int[maxSize];
}
//栈是否为满
public boolean isFull() {
return top == maxSize - 1;
}
//栈是否为空
public boolean isEmpty() {
return top == -1;
}
//入栈
public void push(int num) {
if (isFull()) {
System.out.println("栈满,无法入栈");
return;
}
stack[++top] = num;
}
//出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空,无法出栈");
}
return stack[top--];
}
//显示栈的情况[遍历栈], 遍历时,需要从栈顶开始显示数据
public void show() {
if (isEmpty()) {
System.out.println("栈空,无法遍历");
}
System.out.println("栈内元素为:");
for (int i = top; i >= 0; i--) {
System.out.println(stack[i]);
}
}
}
链表模拟栈的实现
- 使用单链表不需要担心栈满的问题。
- 当只有头结点时,此时栈便为空。
- 入栈:在单链表后添加一个新的结点。
- 出栈:让末尾结点前一个结点指向null。
public class LinkListStackTest {
public static void main(String[] args) {
LinkListStack stack = new LinkListStack();
stack.push(2);
stack.push(3);
stack.push(4);
stack.show();
System.out.println(stack.pop() + "出栈");
System.out.println(stack.pop() + "出栈");
stack.show();
}
}
class LinkListStack {
//结点类
class Node {
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
//定义头节点
private Node head = new Node(0);
public Node getHead() {
return head;
}
//判断栈是否为空
public boolean isEmpty() {
return head.next == null;
}
//入栈
public void push(int num) {
Node temp = head;
Node node = new Node(num);
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
}
//出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈为空,无法入栈");
}
Node temp = head;
//找到要删除的前一个结点也就是末尾结点的前一个结点
while (temp.next.next != null) {
temp = temp.next;
}
int value = temp.next.value;
temp.next = null;
return value;
}
//栈的结点数(不包括head结点)
public int getLength() {
Node temp = head;
int len = 0;
while (temp.next != null) {
len++;
temp = temp.next;
}
return len;
}
//遍历栈,自栈顶向下输出
public void show() {
if (isEmpty()) {
System.out.println("栈为空,无法输出");
return;
}
Node temp = head;
int length = getLength();
int[] numStack = new int[length];
for (int i = 0; i < length; i++) {
numStack[i] = temp.next.value;
temp = temp.next;
}
System.out.println("栈内元素:");
for (int i = length - 1; i >= 0; i--) {
System.out.println(numStack[i]);
}
}
}
使用栈实现简单计算器
- 通过建立索引值遍历表达式
- 如果扫描到数字,直接入栈
- 如果扫描到符号:
- 当前操作符优先级小于或等于栈内操作符时,弹出数字栈中的两个数字以及符号栈中一个符号,计算结果入栈,再判断当前操作符优先级是否小于或等于栈内操作符,循环直至当前操作符优先级高于栈内操作符。
- 将操作符入栈。
原因:我们的想法是判断表达式的计算是否会被后面影响,不确定的情况下我们就先入栈,等到确定下一个操作符的出现不会影响前面的计算时(即下一个操作符优先级小于当前操作符),我们就将前面的出栈并计算。比如1*2-1,当出现减号时就代表前面可以计算。
- 当表达式扫描完成,就顺序弹出数字栈中两个数字及符号栈一个操作符并计算,计算结果放入数字栈,循环直至只剩下数字栈中一个数字,即为表达式结果。
原因:当表达式扫描完毕后,扫描完成的两个栈已经可以直接计算,就相当于第一遍是将从左到右可以直接计算的都计算完毕,而剩下的如1-2*2是扫描中无法计算的,就通过从右到左的计算方式,先计算2与2的乘积,再计算1-4。 - 弹出最后一个值并输出。
(其实很多人应该是不理解第三步为什么要做循环,如果看的是尚硅谷视频应该会发现老师并没有做,依然似乎没问题,但其实如果不做循环,从左到右计算不彻底就可能出现一些问题,比如:1-2*2+100。从左到右彻底的话最后数字栈中是-3、100,但如果不彻底就会出现104 、1,看上去没问题,但计算结果后者就却是了-103,因为它先计算了100+4,再计算1-104。)
import java.util.Stack;
public class SimpleCalculatorTest {
public static void main(String[] args) {
String expression = "3-2*2+100";
System.out.println(SimpleCalculator(expression));
}
public static int SimpleCalculator(String expression) {
//数字栈
Stack<Integer> numberStack = new Stack<>();
//符号栈
Stack<Character> symbolStack = new Stack<>();
//符号数组
char[] array = {'+', '-', '*', '/'};
//指示器
int index = 0;
//判断是否到底
while (index != expression.length()) {
char temp = expression.charAt(index);
//判断是否为数字
if (temp > 47 && temp < 58) {
int num = 0;
//解决多位数问题
while (index < expression.length() && expression.charAt(index) > 47 && expression.charAt(index) < 58) {
temp = expression.charAt(index);
num = num * 10 + temp - '0';
index++;
}
numberStack.add(num);
}
//当为操作符时
if (temp == '+' || temp == '-' || temp == '*' || temp == '/') {
while (!symbolStack.isEmpty() && priority(temp) <= priority(symbolStack.peek())) {
int a = numberStack.pop();
int b = numberStack.pop();
char c = symbolStack.pop();
numberStack.add(calculator(a, b, c));
}
symbolStack.add(temp);
index++;
}
}
while (!symbolStack.isEmpty()) {
int a = numberStack.pop();
int b = numberStack.pop();
char c = symbolStack.pop();
numberStack.add(calculator(a, b, c));
}
return numberStack.pop();
}
public static int priority(char ch) {
if (ch == '*' || ch == '/') {
return 1;
}
if (ch == '+' || ch == '-') {
return 0;
}
throw new RuntimeException("符号不正确");
}
public static int calculator(int a, int b, char c) {
switch (c) {
case '+':
return a + b;
case '-':
return b - a;
case '*':
return a * b;
case '/':
return b / a;
}
throw new RuntimeException("符号不正确");
}
}
逆波兰表达式(后缀表达式)
- 后缀表达式运算
- 初始化一个栈stack;
- 如果遍历到数字,直接入栈;
- 如果遍历到符号,将两个数字出栈并计算,计算结果入栈;
- 循环2-3两步;
- 返回唯一的栈顶元素。
注意:第三步计算顺序:次顶元素(运算符)栈顶元素。
- 中缀表达式转后缀表达式步骤
- 初始化两个栈:运算符栈s1 和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压s2;
- 遇到运算符时,比较其与s1 栈顶运算符的优先级:
- 如果s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
- 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
- 否则,将s1 栈顶的运算符弹出并压入到s2 中,再次转到(4-1)与s1 中新的栈顶运算符相比较;
- 遇到括号时:
- 如果是左括号“(”,则直接压入s1
- 如果是右括号“)”,则依次弹出s1 栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
- 重复步骤2 至5,直到表达式的最右边
- 将s1 中剩余的运算符依次弹出并压入s2
- 依次弹出s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。
(对于这一套标准步骤,我们可以发现其实s2在整个过程中是没有出栈操作的,所以我们可以用字符串/数组来代替s2,这样实现的话也可以省去第八步得逆序过程)
其实概括出来就是:
- 每当遇到数字就直接入s2
- 每当遇到运算符,与s1栈顶运算符比较优先级,优先级高则入s1栈,否则则将s1栈顶运算符入s2,继续比较直至s1栈空或者运算符优先级大于栈顶运算符,此时将运算符入s1栈。
- 每当遇到括号,左括号入栈,右括号出现则将左括号与右括号之间内容压入s2。
- 很显然,所有的步骤都是为了计算时优先级高的计算步骤能够先得到计算。
代码:
(其实主要是理清楚步骤以及原理,代码直接看可能繁琐,但自己理清楚步骤后就可以将大问题化成一步步的小问题,这样写起来代码其实就不是很复杂了)
import java.util.Stack;
//逆波兰表达式完成计算器
public class ReversePolishNotationTest {
public static void main(String[] args) {
String expression = "100+2+3*20+12";
String infixExpression = toInfixExpression(expression);
System.out.println(infixExpression);
System.out.println(reversePolishNotation(infixExpression));
}
//中缀表达式转后缀表达式
private static String toInfixExpression(String expression) {
int index = 0;
Stack<String> s1 = new Stack<>();
String s2 = "";
while (index < expression.length()) {
char temp = expression.charAt(index);
//遍历到数字直接入s2,并index++
if (temp > 47 && temp < 58) {
String num = "";
while (index < expression.length() && expression.charAt(index) > 47 && expression.charAt(index) < 58) {
num += expression.charAt(index);
index++;
}
s2 += num + " ";
} else if (temp == '+' || temp == '-' || temp == '*' || temp == '/' || temp == '(') {
//遍历到运算符判断优先级
//这里之所以加入(处理是因为左括号也会入栈,优先级比较可能出现运算符与括号比较
String symbol = temp + "";
boolean flag = true;
while (!s1.isEmpty() && !symbol.equals("(") && priority(symbol) <= priority(s1.peek())) {
s2 = s2 + s1.pop() + " ";
flag = false;
}
s1.add(symbol);
index++;
} else if (temp == ')') {
//对于右括号,我们只需要将栈中左括号以上运算符出s1栈到s2中即可
if (!s1.peek().equals("(")) {
s2 += s1.pop() + " ";
}
//注意左括号本身也需要出栈
s1.pop();
index++;
} else {
throw new RuntimeException("请输入规范的字符串");
}
}
while (!s1.isEmpty()) {
s2 += s1.pop() + " ";
}
return s2;
}
//后缀表达式运算
public static int reversePolishNotation(String expression) {
String[] elements = expression.split(" ");
Stack<Integer> stack = new Stack<>();
for (String element : elements) {
if (element.equals("+") || element.equals("-") || element.equals("*") || element.equals("/")) {
int a = stack.pop();
int b = stack.pop();
int result = calculator(a, b, element);
stack.add(result);
} else if (element.matches("\\d+")) {
stack.add(Integer.parseInt(element));
} else {
throw new RuntimeException("字符串不符合规范");
}
}
return stack.peek();
}
private static int calculator(int a, int b, String element) {
switch (element) {
case "+":
return a + b;
case "-":
return b - a;
case "*":
return a * b;
case "/":
return b / a;
}
throw new RuntimeException("操作符有误");
}
public static int priority(String ch) {
if (ch.equals("*") || ch.equals("/")) {
return 1;
}
if (ch.equals("+") || ch.equals("-")) {
return 0;
}
//因为如果遇见左括号需要直接将运算符入栈,所以将左括号优先级设为-1
if (ch.equals("(")) return -1;
throw new RuntimeException("符号不正确");
}
}