一、实际需求
输入一个表达式,并计算表达式的值,如:计算 1+2*3-4
的值
请问:计算机底层是如何计算的呢?它是如何识别运算符的优先级,并按照正确的计算顺序计算出结果呢?这篇文章将通过栈这种数据结构来实现简单的计算器。
二、栈的介绍
2.1 基本概念
- 栈的英文为stack
- 栈是一种先入后出的有序列表(FILO)
- 栈是限制线性表中元素的插入和删除只能在线性表的一段进行操作的特殊线性表;允许插入和删除的一端称为栈顶,另一端称为栈底
- 最先放入的元素会放入栈底,而删除数据会从栈顶依次删除
- 添加元素称为入栈(push),删除元素称为出栈(pop)
2.2 应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以 回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆 栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth一first)搜索法。
三、数组模拟
3.1 基本思路
- 使用数组模拟栈
- 定义一个
top
指针用来表示栈顶,初始值为-1 - 入栈操作
stack[++top] = value ;
- 出栈操作
renturn stack[top--] ;
3.2 代码实现
package 栈;
/**
* Project: data structure
* Package: 栈
* Version: 1.0
* Author: wjun
* <p>
* Description: 数组模拟栈
* Created by hc on 2021/02/05 10:48
* © 1996 - 2021 Zhejiang Hong Cheng Computer Systems Co., Ltd.
*/
// 定义一个top 来表示栈顶,初始化 -1
// 入栈:top++ stack[top] = data;
// 出栈:tmp = stack[top] top-- return tmp
public class ArrayStack {
private int top; // 栈顶
private final int maxSize; // 栈深度
private final int[] stack; // 存储数据
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
top = -1;
stack = new int[maxSize];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int value) {
// 是否栈满
if (isFull()) {
System.out.println("栈满无法入栈");
return;
}
stack[++top] = value;
}
// 出栈
public int pop() {
// 是否栈空
if (isEmpty()) {
throw new RuntimeException("栈空无法出栈");
}
return stack[top--];
}
// 显示栈数据
public void show() {
// 是否栈空
if (isEmpty()) {
System.out.println("栈空...");
return;
}
for (int i = top; i >= 0; i--) {
System.out.println(stack[i]);
}
}
}
演示代码
package 栈;
import java.util.Scanner;
/**
* Project: data structure
* Package: 栈
* Version: 1.0
* Author: wjun
* <p>
* Description:
* Created by hc on 2021/02/05 11:05
* © 1996 - 2021 Zhejiang Hong Cheng Computer Systems Co., Ltd.
*/
public class Demo {
public static void main(String[] args) {
//先创建一个 ArrayStack 对象->表示栈
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true; //控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("show: 表示显示栈");
System.out.println("exit: 退出程序");
System.out.println("push: 表示添加数据到栈(入栈)");
System.out.println("pop: 表示从栈取出数据(出栈)");
System.out.println("请输入你的选择");
key = scanner.next();
switch (key) {
case "show":
stack.show();
break;
case "push":
System.out.println("请输入一个数");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res = stack.pop();
System.out.printf("出栈的数据是 %d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~~");
}
}
四、实现计算器
4.1 整体思路
- 通过一个index值(索引),来遍历我们的表达式
- 如果是一个数字,直接入数栈
- 如果是一个符号
- 若当前符号栈为空,直接入栈
- 若当前符号栈有操作符,进行比较
- 若当前操作符优先级小于或等于栈中操作符,从数栈中pop两个数,从符号栈中pop出一个符号进行运算,将结果入数栈,然后当前操作符入符号栈
- 若当前操作符优先级大于栈中操作符,直接入符号栈
- 表达式扫描完毕,顺序从数栈和符号栈中pop出相对应的数和符号,并运算
- 最后数栈只有一个数字,就是表达式的结果
4.2 画图演示
表达式 1 + 2 * 3 - 4
索引指向 1
,数字直接入数栈
索引指向+
,符号栈为空直接入符号栈
索引指向2
,数字直接入数栈
索引指向x
,且优先级高于栈顶操作符,直接入符号栈
索引指向3
,数字直接入数栈
索引指向-
,优先级低于栈顶操作符,从数栈pop
两个数,符号栈pop
一个操作符,运算2 x 3 = 6
,结果入数栈,-
入符号栈
索引指向4
,数字直接入数栈
表达式遍历完,从数栈pop
两个数v1,v2
,从符号栈pop
一个符号op
,运算v2 op v1 = v3
,结果入数栈,直到符号栈为空,此时数栈一定只有一个元素,即为结果。
4.3 前期准备
如何提取数字和操作符
public static Object[] getElem(String line) {
ArrayList<String> list = new ArrayList<>();
int index = 0;
for (int i = 0; i < line.length(); i++) {
int c = line.charAt(i);
// 分别对应+、-、*、/的ASCII码
if (c == 42 || c == 43 || c == 45 || c == 47) {
list.add(line.substring(index, i));
list.add("" + (char) c);
index = i + 1;
}
}
list.add(line.substring(index));
return list.toArray();
}
如何比较操作符优先级
// 转换操作符
public static int tranOp(char c) {
return c == '+' || c == '-' ? 0 : 1;
}
如何计算二元表达式
// 计算二元表达式的数值
public static Double getResult(Double v1, String op, Double v2) {
switch (op) {
case "+":
return v1 + v2;
case "-":
return v1 - v2;
case "*":
return v1 * v2;
case "/":
return v1 / v2;
default:
throw new RuntimeException("error");
}
}
4.4 完整代码
package 栈;
import java.util.ArrayList;
import java.util.Stack;
/**
* Project: data structure
* Package: 栈
* Version: 1.0
* Author: wjun
* <p>
* Description: 用栈模拟计算器
* Created by hc on 2021/02/05 15:39
* © 1996 - 2021 Zhejiang Hong Cheng Computer Systems Co., Ltd.
*/
public class StackComputer {
public static void main(String[] args) {
// 声明数栈
Stack<Double> intStack = new Stack<>();
// 声明符号栈
Stack<String> charStack = new Stack<>();
// 给定一个表达式
String line = "1+2+3-4";
// 顺序拆分表达式
Object[] elem = getElem(line);
for (Object e : elem) {
try {
// 转换成功是数字
double value = Double.parseDouble("" + e);
// 数字直接入栈
intStack.push(value);
} catch (NumberFormatException numberFormatException) {
// 转换失败是运算符
if (charStack.isEmpty()) {
// 符号栈空,直接入栈
charStack.push("" + e);
} else {
// 获取栈顶数据
char op = charStack.peek().charAt(0);
// 转换操作符
int srcOp = tranOp(("" + e).charAt(0));
int stackOp = tranOp(op);
// 若当前操作符优先级大于栈中操作符,直接入符号栈
if (srcOp > stackOp) {
charStack.push("" + e);
} else {
// 若当前操作符优先级小于或等于栈中操作符
// 从数栈中pop两个数,从符号栈中pop出一个符号进行运算,将结果入数栈
// 然后当前操作符入符号栈
Double v1 = intStack.pop();
String opTmp = charStack.pop();
Double v2 = intStack.pop();
Double result = getResult(v2, opTmp, v1);
intStack.push(result);
charStack.push("" + e);
}
}
}
}
// 如果操作符栈为空,数栈为结果
while (!charStack.isEmpty()) {
Double v1 = intStack.pop();
String op = charStack.pop();
Double v2 = intStack.pop();
intStack.push(getResult(v2, op, v1));
}
System.out.println("表达式:" + line);
System.out.println("结果:" + intStack.pop());
}
public static Object[] getElem(String line) {
ArrayList<String> list = new ArrayList<>();
int index = 0;
for (int i = 0; i < line.length(); i++) {
int c = line.charAt(i);
if (c == 42 || c == 43 || c == 45 || c == 47) {
list.add(line.substring(index, i));
list.add("" + (char) c);
index = i + 1;
}
}
list.add(line.substring(index));
return list.toArray();
}
// 转换操作符
public static int tranOp(char c) {
return c == '+' || c == '-' ? 0 : 1;
}
// 计算二元表达式的数值
public static Double getResult(Double v1, String op, Double v2) {
switch (op) {
case "+":
return v1 + v2;
case "-":
return v1 - v2;
case "*":
return v1 * v2;
case "/":
return v1 / v2;
default:
throw new RuntimeException("error");
}
}
}
结果展示
考虑带括号的如何实现,提示:识别出一对括号中的数据,递归调用核心逻辑中的代码,将得出来的数值替换原来的括号中的数值。