假如有一个需求:我们需要计算如下的表达式722-5+1-5*3-3,而且只能接收这个字符串,不能单独的一次又一次的输入单个的数字或运算符,应该怎么解决?这里我们就需要用到栈。
其他的一些应用场景:
1)子程序的调用;
2)处理递归调用;
3)表达式的转换(中缀表达式转后缀表达式)与求值;
4)二叉树的遍历;
5)图形深度优先搜索法
栈是先入后出的有序列表
栈是限制线性表中元素只能在线性表的同一端进行的一种特殊线性表。允许插入和删除,进行变化的一端称为栈顶,另一端固定的称为栈底
根据栈的先入后出的定义可知,最先放入栈中的元素,最后出来,放在最底下,而删除刚好相反,最后放入的元素,在栈顶,最先被删除
根据上面的分析,我们可以大致确定用数组去实现队列至少需要一下方法:
一个构造队列的方法
一个入队方法
一个出队方法
一个判断队列是否为空的方法
一个判断是否为满的方法
还可以适当的加一下打印队列的检测方法
可以用数组和链表去实现栈,这里我们先用数组实现栈的去分析:
先定义一个实现栈的类,因为对于出栈和入栈移动的都是栈顶标志位,所以需要定义一个变量top用来表示栈顶,初始化为-1。此外还需要的成员变量有一个数组stack,和一个确定数组的大小,也就是确定了栈的大小的变量MaxSize
构造方法:
传入一个数值,用来确定栈的大小,同时构造出该栈
入栈方法:
先判断栈是否满,top == MaxSize-1
不满时,先让top++,往上移动一位,然后将传入的数值,入栈stack[top]=入栈的值
出栈方法:
判断栈是否为空,top == -1;
不为空时,需要出栈的数据就是当前栈顶的数据stack[top],然后直接top–即可
显示栈顶元素:
因为top始终指向栈顶元素,所以直接显示stack[top]即可
遍历方法:
直接用一个for循环,从栈顶开始往下遍历输出即可
/**
* 栈类
* @author centuowang
* @param
* MaxSize:栈的容量大小
* stack:栈数组
* top:用来指向栈顶位置
* @method
* Stack(int size):构造方法,构造出一个栈
* is_Full():判断栈满
* is_Empty():判断栈空
* push(int data):入栈
* pop():出栈
* show():遍历显示栈
*/
class Stack{
private int MaxSize;//栈的容量大小
private int[] stack;//栈数组
private int top = -1;//用来指向栈顶位置
//构造方法
Stack(int size){
MaxSize = size;
stack = new int[MaxSize];
}
//判断栈满
public boolean is_Full() {
return top == MaxSize-1;
}
//判断栈空
public boolean is_Empty() {
return top == -1;
}
//入栈
public void push(int data) {
if(is_Full()) {
System.out.println("栈满,不能再放入数据了");
return;
}
top++;
stack[top] = data;
}
//出栈
public void pop() {
if(is_Empty()) {
System.out.println("栈空,不能出栈");
return;
}
System.out.printf("出栈的元素是:stack[%d]:%d\n", top,stack[top]);
top--;
}
//显示栈顶元素
public void peek() {
if(is_Empty()) {
System.out.println("栈空,没有元素");
return;
}
System.out.printf("栈顶元素是:stack[%d]:%d\n", top,stack[top]);
}
//遍历
public void show() {
if(is_Empty()) {
System.out.println("栈空,没有元素");
return;
}
for(int i=top; i>=0; i--) {
System.out.printf("stack[%d]:%d\n", i,stack[i]);
}
}
}
上面我们已经用数组简单实现了一个栈,那么回到我们一开始的案例引入中的综合计算器问题,我们接下来就用对上面的代码进行扩展和改进来用栈实现一个综合计算器
这里的重点当然是对不同优先级的符号进行先后运算的这个问题,那么我们就利用栈的先入后出的特点来进行思路分析:
我们需要两个栈,一个用来存放数字(数栈),一个用来存放运算符(符号栈)
1.首先需要一个index索引来扫描一个算式的字符串中的每个字符
2.如果是数字直接入数栈
3.如果是符号,则分情况讨论:
3.1:如果当前符号栈为空,则直接入符号栈
3.2:如果当前符号栈不为空,则比较扫描到的该符号与符号栈栈顶的符号进行优先级比较。 如 果 当 前 符 号 优 先 级 大 于 符 号 栈 栈 顶 的 符 号 , 则 直 接 入 栈 \color{red}{如果当前符号优先级大于符号栈栈顶的符号,则直接入栈} 如果当前符号优先级大于符号栈栈顶的符号,则直接入栈, 否 则 , 将 数 栈 中 的 两 个 数 字 出 栈 , 再 从 符 号 栈 中 出 栈 一 个 符 号 , 将 这 三 者 进 行 运 算 , 并 将 运 算 后 的 结 果 放 入 数 栈 中 \color{blue}{否则,将数栈中的两个数字出栈,再从符号栈中出栈一个符号,将这三者进行运算,并将运算后的结果放入数栈中} 否则,将数栈中的两个数字出栈,再从符号栈中出栈一个符号,将这三者进行运算,并将运算后的结果放入数栈中, 然 后 再 将 当 前 扫 描 到 的 符 号 入 符 号 栈 \color{blue}{然后再将当前扫描到的符号入符号栈} 然后再将当前扫描到的符号入符号栈这样我们就利用了栈先入后出的特点,解决了符号运算的优先级问题,优先级高的运算符先被计算了
4.扫描完后,依次从数栈中出栈两个数,和符号栈中出栈一个符号进行运算,再将结果入栈数栈中,直到符号栈为空,数栈中只剩最后一个数字时即为最终结果。
(以3+2×6-2为例)进行图解:
1.扫描到3,入数栈
2.扫描到+号,符号栈为空,入符号栈
3.扫描到2,入数栈
4.扫描到×,且符号栈目前不为空,则将乘号×与+号的优先级进行比较,发现乘号×的优先级较大(进行上面思路分析中的红色字体部分),直接入栈
5.扫描到6,直接入数栈
6.扫描到-号,且符号栈不为空,比较-号与乘号×的优先级,-号优先级较小(执行上面思路分析中的蓝色文字部分),从数栈中依次出栈两个数(6和2),从符号栈中出栈一个符号(乘号×)1,并进行运算,6×2=12,再将结果放入数栈,将-号入栈符号栈
7.扫描到数字2,直接入数栈,那么扫描结束后的两个栈的情况如图
8.接下来依次进行数栈和符号栈的出栈符号运算,再将运算结果入符号栈,反复进行,直到得出最终结果,这里也就是先进行12-2=10,将10入数栈后,再计算10+3=13,此时数栈只有一个数,且符号栈为空,那么13就是最终结果
1.注意我们前面的代码中,在peek(),pop()两个方法中,返回值都用的是void,只是简单的打印了一下数字而已,那么在实际应用中我们是需要栈顶元素和出栈元素的的话,我们首先将返回值改为int,因为我们要获取这些元素
2.我们需要添加额外的三个方法:因为运算符的优先级是程序员自己定的,所以增加一个方法取确定运算符的优先级;加一个方法去判断扫描到的是数字还是运算符;需要一个计算方法
3.开始编写我们上面思路分析中的逻辑
package com.stack;
/**
* 用数组实现的栈实现一个综合计算器
* 这里以3+2*6-2
* @author centuowang
* @param
* index:用来扫描算是字符串的索引
* ch:每次扫描算式得到的char都保存在该变量中
* num1,num2:定义两个变量用来接收数栈中出栈需要被计算的两个数
* oper:用来接收符号栈中出栈的运算符
* rs:用来存放运算的结果
* numstack:数栈
* operstack:符号栈
*/
public class Calculator {
public static void main(String[] args) {
int num1,num2;//定义两个变量用来接收数栈中出栈需要被计算的两个数
int oper;//用来接收符号栈中出栈的运算符
int rs = 0;//用来存放运算的结果
int index = 0;//用来扫描算是字符串的索引
char ch = ' ';//每次扫描算式得到的char都保存在该变量中
//先构造一个数栈和一个符号栈
CalculatorStack numstack = new CalculatorStack(10);//数栈
CalculatorStack operstack = new CalculatorStack(10);//符号栈
//接着给一个算是字符串
String exam = "3+2*6-2";
//开始扫描字符串
while(true) {
//依次得到exam中的每一个字符
ch = exam.substring(index, index+1).charAt(0);
//判断扫描到的是数字还是运算符
if(operstack.is_Oper(ch)) {
//如果是运算符,继续判断符号栈中是否为空
if(operstack.is_Empty()) {
//如果为空,直接入栈
operstack.push(ch);
}else {
//如果不为空,则比较扫描到的运算符与符号栈栈顶的运算符的优先级
if(operstack.priority(ch) < operstack.priority(operstack.peek())) {
//如果扫描到的运算符比符号栈栈顶的运算符的优先级小或相等
//那么数栈出栈两个数,符号栈中出栈两个数
num1 = numstack.pop();
num2 = numstack.pop();
oper = operstack.pop();
//接着计算
rs = numstack.cal(num1, num2, oper);
//将结果放入数栈
numstack.push(rs);
//再将刚才扫描到的运算符,入符号栈
operstack.push(ch);
}else {
//如果扫描到的运算符比符号栈栈顶的运算符的优先级大,直接入栈
operstack.push(ch);
}
}
}else {
//如果是数字,直接入数栈
numstack.push(ch-48);//注意这里有个ASCII表的char和int的数值问题
}
index++;//索引往下移动
//判断是否扫描完
if(index >= exam.length()) {
break;//跳出while循环
}
}
//扫描完之后,开始度数栈和符号栈中的残留数据进行运算
while(true){
if(operstack.is_Empty()) {
//如果符号栈为空,说明数栈中只剩一个数字,即为最终结果
rs = numstack.pop();
break;
}
//否则
//进行运算:取数栈中的两个数和符号栈中的运算符进行运算,将结果放回数栈,反复进行直到数栈中剩一个最终结果
num1 = numstack.pop();
num2 = numstack.pop();
oper = operstack.pop();
rs = numstack.cal(num1, num2, oper);
numstack.push(rs);
}
System.out.println("计算结果为:"+ rs);
}
}
/**
* 栈类
* @author centuowang
* @param
* MaxSize:栈的容量大小
* stack:栈数组
* top:用来指向栈顶位置
* @method
* Stack(int size):构造方法,构造出一个栈
* is_Full():判断栈满
* is_Empty():判断栈空
* push(int data):入栈
* pop():出栈
* show():遍历显示栈
*/
class CalculatorStack{
private int MaxSize;//栈的容量大小
private int[] stack;//栈数组
private int top = -1;//用来指向栈顶位置
//构造方法
CalculatorStack(int size){
MaxSize = size;
stack = new int[MaxSize];
}
//判断栈满
public boolean is_Full() {
return top == MaxSize-1;
}
//判断栈空
public boolean is_Empty() {
return top == -1;
}
//入栈
public void push(int data) {
if(is_Full()) {
System.out.println("栈满,不能再放入数据了");
return;
}
top++;
stack[top] = data;
}
//出栈
public int pop() {
int val = 0;
if(is_Empty()) {
System.out.println("栈空,不能出栈");
return 0;
}
System.out.printf("出栈的元素是:stack[%d]:%d\n", top,stack[top]);
val = stack[top];
top--;
return val;
}
//显示栈顶元素
public int peek() {
if(is_Empty()) {
System.out.println("栈空,没有元素");
return 0;
}
System.out.printf("栈顶元素是:stack[%d]:%d\n", top,stack[top]);
return stack[top];
}
//遍历
public void show() {
if(is_Empty()) {
System.out.println("栈空,没有元素");
return;
}
for(int i=top; i>=0; i--) {
System.out.printf("stack[%d]:%d\n", i,stack[i]);
}
}
//新增方法1:因为运算符的优先级是程序员自己定的,所以增加一个方法取确定运算符的优先级
public int priority(int i) {//这里注意一下,char和int在底层是可以相互通用比较的
switch(i) {
case '*':
return 1;
case '/':
return 1;
case '+':
return 0;
case '-':
return 0;
}
return-1;
}
//新增方法2:加一个方法去判断扫描到的是数字还是运算符
public boolean is_Oper(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
//新增方法3:还需要一个计算方法
public int cal(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.比如给个式子:43-24×3+10×25,那么我们上面的代码实现中,还是根据一个一个字符去扫描的话,扫描43这个整体数字的时候,就会扫描成一个4和一个3,那么最终的计算结果肯定也是错误的
2.对于括号的运算,又要怎么处理了呢,括号的优先级很高,而且最关键的是括号是成对出现才能进行运算的,我们不能对单个的括号进行运算
针对上面的两个问题,这里暂且先解决第一个,第二个括号问题,在后面一篇博客将前中后缀表达式中实现逆波兰计算器里会做详细介绍
针对上面的第一个多位数问题,很明显问题出在代码的这个地方:
如果是数字则直接入栈,这里是不对的,我们改进的话,就应该从这里下手,在这里入栈之前还需要加上一个判断,判断这个数字的下一个是不是还是数字,如果不是的话才能入栈,如果是的话,我们就用个字符串拼接的方式,将前后几个扫描到的数字,拼接在一起
前面我们都是用数组的方式去实现栈,用链表的方式实现作为一个练手,日后有时间补上
下一篇: 从1开始学Java数据结构与算法——栈的三种表达式:前中后缀表达式与逆波兰计算器的实现.