一、栈的数组实现
栈是一种先进先出的数据结构,可以用数组进行模拟。下面摆出代码,ArrayStack里面有几个方法 : isEmpty(), isFull(), push(), pop(), stackList(), peek()这几个方法是属于栈的特有方法。 isOper(), priority(), calculate()是计算器的特有方法。这是栈的一些常用方法和下面计算器需要用到的方法。其中里面的几个属性比较重要,maxSize栈的大小, stack定义数组栈, top表示栈顶指针。
class ArrayStack2{
private int maxSize; //栈的大小
private int[] stack; //定义栈
private int top = -1; //栈顶,一定要初始化为-1,不然就有默认值0了
//初始化栈的大小
public ArrayStack2(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 data){
//栈满时,不能入栈
if(isFull()){
System.out.println("栈满~~不能入栈");
return;
}
top ++;
stack[top] = data;
}
//出栈
public int pop(){
//栈空,不能出栈
if(isEmpty()){
throw new RuntimeException("栈空~~,莫得数据");
}
//定义中间值接收栈顶元素值
int temp = stack[top];
top --;
return temp;
}
//输出栈(遍历栈),不过是反向遍历数组
public void stackList(){
//当栈空时,不需要遍历
if(isEmpty()){
System.out.println("栈空~莫得数据");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
//返回栈顶的数据,但并不出栈,也就是说top指针并不移动
public int peek(){
return stack[top];
}
//判断是否是运算符
public boolean isOper(int oper){
return oper == '+' || oper == '-' || oper == '*' || oper == '/' || oper == '(' || oper == ')';
}
//加减乘除的优先级
public int priority(int oper){
if(oper == '*' || oper == '/'){ //优先级最高的
return 1;
}else if(oper == '+' || oper == '-'){
return 0;
}else {
return -1; //错误的运算符
}
}
//进行运算
public int calculate(int num1, int num2, int oper){
int result = 0;
switch (oper){ //进行正常的运算
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1; //这里要注意运算的优先级
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
break;
default:
break;
}
return result;
}
}
二、栈实现计算器
本文中,用的是中缀表达式实现的计算器,可以完成多位数的基本运算,支持小括号优先级运算。
(1)、在主方法里定义数栈和符号栈
其中,数栈存放表达式的数字部分,符号栈存放表达式的符号。
(2)、定义指针,指针指向当前表达式的最后出栈位置,初始为0
int index = 0; //这是指针,指向当前表达式的位置
(3)、入栈
入栈的步骤会很复杂,里面写了很多if语句判断,在这里我们逐条进行分析解释。
①、首先我们需要判断当前指针所指位置的元素是数字还是符号。
②、如果是符号
i.如果符号栈为空,则直接入栈。
ii.符号栈不为空,首先判断当前元素是否为 '(' 号或者 ')'号。如果是 '('号,那直接将 '('号入符号栈。并且当前指针后移,用'continue'继续执行接下来的代码。如果是 ')'号,可知当前符号栈中必然含有 '(' 号,因为这两个符号是成对出现的。这时调用peek()方法偷看一眼符号栈栈顶元素,如果不是 '(' 号,则继续循环。含有 ')'号的执行代码如下。在最后执行完while循环后,一定要pop出符号栈中的'('符号。
int operation = operStack.peek();
while (operation != '('){
//小于就要从数栈中出两个数据,和栈顶元素进行运算
num1 = numStack.pop();
num2 = numStack.pop();
int oper = operStack.pop();
int result = operStack.calculate(num1, num2, oper);
//把运算的结果再入栈
numStack.push(result);
operation = operStack.peek();
}
operStack.pop();
//跳出循环
index ++;
if(index >= expression.length()){
break;
}
continue;
iii.在执行完上述 ii 步骤后,要继续执行接下来的代码。执行这段代码,主要是根据符号的优先级来判断的。如果当前指针所指的符号比符号栈栈顶的符号优先级低的话,就要先从数栈中弹出两个数,符号栈弹出符号进行计算,再将结果压入数栈。这样循环往复,直到符号栈中没有优先级更高的符号。最后将指针所指符号压入符号栈。
while (!operStack.isEmpty() && operStack.priority(ch) <= operStack.priority(operStack.peek())){
//小于就要从数栈中出两个数据,和栈顶元素进行运算
num1 = numStack.pop();
num2 = numStack.pop();
int oper = operStack.pop();
int result = operStack.calculate(num1, num2, oper);
//把运算的结果再入栈
numStack.push(result);
}
operStack.push(ch);
本来这里的while我是写成if的,不过后来发现了一个bug,改成了while。bug描述及解决思路如下:
如果当前符号优先级小于或者等于栈顶的符号,这里要反复的进行比较,
原因: 2-2*5+10 这个表达式进行运算,如果这里的最后一个'+'号,如果只与前面的'*'号比较,
那进行运算的后的结果是 2-10+10,这时'+'号入符号栈,在最后出栈进行运算时,数栈中此时
数据为 [2 10 10] 符号栈数据为 [- +],进行运算时首先出来的 是 10+10,计算结果20压入数栈
,然后是2-20,这显然不符合运算规则,用if不合适,只能用while循环。
③、如果是数字
i.如果已经到达了表达式的末尾,那就直接将指针所指数字压入数栈。
if(index+1 == expression.length()){
numStack.push(ch - 48);
}
这里之所以ch - 48,与ascii码有关,char型数据与int型数据的转换。
ii.没有到达末尾,需要判断是否是多位数。我定义了一个中间指针,初始值为当前表达式指针的下一位。定义一个变量接收中间指针所指向的值,判断这个变量是否是数字,如果是,那就进入while循环进行操作,利用字符串拼接方法拼接好多为数。最后再压入数栈。
//如果是多位数
int temp = index+1; //获取当前表达式的下一个索引
char multi = expression.substring(temp, temp+1).charAt(0); //多位数进行拼接的元素
String num = String.valueOf(ch); //数字的第一个
while(!operStack.isOper(multi)){ //当不是运算符时,需要拼接字符串
num += multi; //这里进行拼接
temp = temp + 1; //下一个索引
index++; //这个相应的全局指针也要后移!!!!!!!!!
if(temp < expression.length()){ //小于则直接拆分赋值
multi = expression.substring(temp, temp+1).charAt(0);
}else if(temp == expression.length()){ //到了表达式的末尾,直接退出循环
break;
}
}
numStack.push(Integer.parseInt(num));
④、最后记得指向表达式的指针后移
此时数栈中的栈顶元素为最终结果。
三、完整代码
package com.yc.stack;
/**
* @author JavaJxh
* @create 2019-11-29 20:48
*/
public class Calculator {
public static void main(String[] args) {
//先创建两个栈,一个存放数字,一个存放运算符
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
int index = 0; //这是指针,指向当前表达式的位置
int num1 = 0;
int num2 = 0;
String expression = "10-2-2*(5+10)+8-(21+3)*2"; //14+28=42
//入栈
while(1 == 1){
char ch = expression.substring(index, index+1).charAt(0); //得到当前指针所指的字符串内容
//判断是符号还是数字
if(operStack.isOper(ch)){ //如果是符号
if(!operStack.isEmpty()){ //如果符号栈不为空
if(ch == '('){ //如果是'('号,则直接将'('号入栈
operStack.push(ch);
//跳出循环
index ++;
if(index >= expression.length()){
break;
}
continue;
}else if(ch == ')'){ //如果是')'号,需要将 '('号之前的东西都进行计算,将结果压入数栈
//返回操作栈的栈顶的数据
int operation = operStack.peek();
while (operation != '('){
//小于就要从数栈中出两个数据,和栈顶元素进行运算
num1 = numStack.pop();
num2 = numStack.pop();
int oper = operStack.pop();
int result = operStack.calculate(num1, num2, oper);
//把运算的结果再入栈
numStack.push(result);
operation = operStack.peek();
}
operStack.pop();
//跳出循环
index ++;
if(index >= expression.length()){
break;
}
continue;
}
//如果当前符号优先级小于或者等于栈顶的符号,这里要反复的进行比较,
//原因: 2-2*5+10 这个表达式进行运算,如果这里的最后一个'+'号,如果只与前面的'*'号比较,
//那进行运算的后的结果是 2-10+10,这时'+'号入符号栈,在最后出栈进行运算时,数栈中此时
//数据为 [2 10 10] 符号栈数据为 [- +],进行运算时首先出来的 是 10+10,计算结果20压入数栈
// ,然后是2-20,这显然不符合运算规则
while (!operStack.isEmpty() && operStack.priority(ch) <= operStack.priority(operStack.peek())){
//小于就要从数栈中出两个数据,和栈顶元素进行运算
num1 = numStack.pop();
num2 = numStack.pop();
int oper = operStack.pop();
int result = operStack.calculate(num1, num2, oper);
//把运算的结果再入栈
numStack.push(result);
}
operStack.push(ch);
}else{ //符号栈为空则直接入栈
operStack.push(ch);
}
}else{ //如果是数字,那就直接入栈
if(index+1 == expression.length()){
numStack.push(ch - 48);
}else{
//如果是多位数
int temp = index+1; //获取当前表达式的下一个索引
char multi = expression.substring(temp, temp+1).charAt(0); //多位数进行拼接的元素
String num = String.valueOf(ch); //数字的第一个
while(!operStack.isOper(multi)){ //当不是运算符时,需要拼接字符串
num += multi; //这里进行拼接
temp = temp + 1; //下一个索引
index++; //这个相应的全局指针也要后移!!!!!!!!!
if(temp < expression.length()){ //小于则直接拆分赋值
multi = expression.substring(temp, temp+1).charAt(0);
}else if(temp == expression.length()){ //到了表达式的末尾,直接退出循环
break;
}
}
numStack.push(Integer.parseInt(num));
//numStack.push(ch - 48); //因为是char型,char型的'1'对应ascii码表的49,所以要减48
}
}
//跳出循环
index ++;
if(index >= expression.length()){
break;
}
}
//入栈结束后,就要逐个的进行计算了
while (1==1){
//当运算符栈空时,代表计算结束
if(operStack.isEmpty()){
int result = numStack.pop(); //这时的栈顶是计算后的结果
System.out.printf("%s = %d", expression, result);
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
int oper = operStack.pop();
int result = operStack.calculate(num1, num2, oper);
//把运算的结果再入栈
numStack.push(result);
}
}
}
class ArrayStack2{
private int maxSize; //栈的大小
private int[] stack; //定义栈
private int top = -1; //栈顶,一定要初始化为-1,不然就有默认值0了
//初始化栈的大小
public ArrayStack2(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 data){
//栈满时,不能入栈
if(isFull()){
System.out.println("栈满~~不能入栈");
return;
}
top ++;
stack[top] = data;
}
//出栈
public int pop(){
//栈空,不能出栈
if(isEmpty()){
throw new RuntimeException("栈空~~,莫得数据");
}
//定义中间值接收栈顶元素值
int temp = stack[top];
top --;
return temp;
}
//输出栈(遍历栈),不过是反向遍历数组
public void stackList(){
//当栈空时,不需要遍历
if(isEmpty()){
System.out.println("栈空~莫得数据");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
//返回栈顶的数据,但并不出栈,也就是说top指针并不移动
public int peek(){
return stack[top];
}
//判断是否是运算符
public boolean isOper(int oper){
return oper == '+' || oper == '-' || oper == '*' || oper == '/' || oper == '(' || oper == ')';
}
//加减乘除的优先级
public int priority(int oper){
if(oper == '*' || oper == '/'){ //优先级最高的
return 1;
}else if(oper == '+' || oper == '-'){
return 0;
}else {
return -1; //错误的运算符
}
}
//进行运算
public int calculate(int num1, int num2, int oper){
int result = 0;
switch (oper){ //进行正常的运算
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1; //这里要注意运算的优先级
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
break;
default:
break;
}
return result;
}
}
四、总结
总的来说,中缀表达式计算器的实现不是很难,但其中有不少需要注意的点。我之前按照老师的思路写,发现了bug,然后自己想了很久,终于将代码的bug改掉,同时也拓展了 '(' ')'这两个符号的运算。这让我感觉到了数据结构的魅力。我越发觉得数据结构是一个宝藏,很多问题都能用数据结构来解决。比如:我要出一个随机数,每一个随机数对应不同的人。这个功能应用于抽签,也就是说,每次随机数出的数字都不能重复,或者说如果出到重复的数字,那就再生成随机数,直到没有重复的数字为止。要是我以前,我会搞两个数组,其中一个int数组,一个String数组,String数组对应人,一个int数组对应生成的随机数,随机数的范围为String数组的大小。如果挑出来一个人,我就将其下标存入int数组,下次再挑时,对应int数组,如果里面没有当前生成的随机数,则直接挑出来,有的话则再重新生成随机数。有了数据结构后,我可能会用单链表来解决这个问题。