文章目录
栈
概述
百度百科:栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
简单来说:就是一种先入后出的存储数据的容器。
想象一下乌鸦喝水,瓶子就是栈,乌鸦投入的石子就是元素,投入的第一个石子直接到栈底,然后最后一个石子到栈顶,如果乌鸦要把石子取出来,也只能从栈顶一个一个取石子,即按照后入先出的顺序取。
找图的时候,这个图片笑死我了,加上!
用数组作为底层实现一个栈
- 栈中应有的属性
用一个属性来记录栈顶,方便判断栈是否为空,是否已满。初始化为-1表示栈为空
maxSize用来表示栈的最大容量,可用此值创建底层的数组
创建一个数组作为栈的底层 - 构造方法
构造方法中接收一个参数,此参数表示栈的最大容量,并用来创建栈底层的数组 - 判断栈满和栈空
top从-1开始,maxSize从0开始所以这里是maxSize-1 - 入栈
先判断是否已满,满则结束方法,不满则将top加一位,然后用top作数组的索引,向数组对应的地方存入数据 - 出栈
出栈不需要对数组中的元素进行删除一类的操作,只需要将此事top的值作为索引,返回此索引下数组的值,并将top减一位 - 遍历显示栈
先判断,通过后直接按照top——0的顺序,遍历数组即可 - 显示栈顶元素
此操作仅显示一下栈顶的数据,并不出栈
全部代码
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
String choice;
ArrayStack stack=null;
while (true){
System.out.println("创建栈,1。退出,0。");
boolean flag=false;
choice=sc.next();
if (choice.equals("1")){
String Size;
int ISize;
while (true){
System.out.println("输入大小");
Size=sc.next();
try {
ISize=Integer.parseInt(Size);
break;
}catch (Exception e){
System.out.println("输入错误");
}
}
stack=new ArrayStack(ISize);
flag=true;
}else if(choice.equals("0")){
break;
}
if (flag){
while (true) {
System.out.println("入栈,1。出栈,2。全部显示,3。返回,0");
String choice2;
choice2=sc.next();
if (choice2.equals("1")) {
String Value;
int IValue;
while (true) {
System.out.println("输入数值");
Value = sc.next();
try {
IValue = Integer.parseInt(Value);
break;
} catch (Exception e) {
System.out.println("输入错误");
}
}
stack.push(IValue);
}else if (choice2.equals("2")){
try {
System.out.println(stack.pop());
}catch (Exception e){
System.out.println(e.getMessage());
}
}else if (choice2.equals("3")){
stack.show();
}else if (choice2.equals("0")){
break;
}
}
}
}
}
}
class ArrayStack{
private int top=-1; //栈顶,初始化为-1
private int maxSize; //栈最大容量
private int[] Stack; //栈底层用数组实现
// 构造方法,对栈初始化
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
this.Stack = new int[this.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+"]-------"+Stack[i]);
}
}
// 显示栈顶的数据
public int peek(){
// 先判断栈是否为空
if (isEmpty()){
// 若为空,则抛出异常
throw new RuntimeException("栈空,无数据");
}
return Stack[top];
}
}
中缀和后缀表达式
中缀表达式
百度百科:
(或中缀记法)是一个通用的算术或逻辑公式表示方法,操作符是以中缀形式处于操作数的中间(例:3 + 4),中缀表达式是人们常用的算术表示方法。
与前缀表达式(例:+ 3 4)或后缀表达式(例:3 4 +)相比,中缀表达式不容易被计算机解析,但仍被许多程序语言使用,因为它符合人们的普遍用法。
与前缀或后缀记法不同的是,中缀记法中括号是必需的。计算过程中必须用括号将操作符和对应的操作数括起来,用于指示运算的次序。
简单来说,中缀表达式是对数字计算的一种表达方式,如1+(2-1)*3+4。就是我们小学学习的数学计算式。
后缀表达式(逆波兰表达式)
后缀表达式又被称为逆波兰表达式,前缀表达式被称为波兰表达式
百度百科
一个表达式E的后缀形式可以如下定义:
(1)如果E是一个变量或常量,则E的后缀式是E本身。
(2)如果E是E1 op E2形式的表达式,这里op是任何二元操作符,则E的后缀式为E1’E2’ op,这里E1’和E2’分别为E1和E2的后缀式。
(3)如果E是(E1)形式的表达式,则E1的后缀式就是E的后缀式。
如:我们平时写a+b,这是中缀表达式,写成后缀表达式就是:ab+
简单来说,后缀表达式也是对数字计算的一种表达方式,
如一个稍微复杂一点的:1, 2, 3, +, *, 6, -, 6, 1, 2, +, /, -
他是1*(2+3)-6-(6/(1+2))
这个中缀表达式转为后缀表达式后的样子
一般他的运算符都是在数字的靠后面部分,而且他不能有括号
中缀转后缀的思路
百度百科
将一个普通的中缀表达式转换为逆波兰表达式的一般算法是:
首先需要分配2个栈,一个作为临时存储运算符的栈S1(含一个结束符号),一个作为存放结果(逆波兰式)的栈S2(空栈),S1栈可先放入优先级最低的运算符#,注意,中缀式应以此最低优先级的运算符结束。可指定其他字符,不一定非#不可。从中缀式的左端开始取字符,逐序进行如下步骤:
(1)若取出的字符是操作数,则分析出完整的运算数,该操作数直接送入S2栈。
(2)若取出的字符是运算符,则将该运算符与S1栈栈顶元素比较,如果该运算符(不包括括号运算符)优先级高于S1栈栈顶运算符(包括左括号)优先级,则将该运算符进S1栈,否则,将S1栈的栈顶运算符弹出,送入S2栈中,直至S1栈栈顶运算符(包括左括号)低于(不包括等于)该运算符优先级时停止弹出运算符,最后将该运算符送入S1栈。
(3)若取出的字符是“(”,则直接送入S1栈顶。
(4)若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
(5)重复上面的1~4步,直至处理完所有的输入字符。
(6)若取出的字符是“#”,则将S1栈内所有运算符(不包括“#”),逐个出栈,依次送入S2栈。
完成以上步骤,S2栈便为逆波兰式输出结果。不过S2应做一下逆序处理。便可以按照逆波兰式的计算方法计算了!
或者
中缀转后缀的思路:
创建两个栈(这里为了方便,用的是一个栈和一个集合)S1,S2。
遍历中缀表达式集合,对其中的元素进行如下判断和操作
- 若元素是数字,则直接入S2。
- 若为左括号"(",则直接入S1栈。
- 若为运算符,则判断S1是否为空,若为空则直接如S1,若不为空,则将此元素与S1栈顶的运算符作比较,若此运算符级别高,则直接如S1,若此元素级别低,则将S1栈顶运算符出栈,并,入S2
然后再将此元素与栈顶运算符作比较,一直循环,直到S1变为空,或栈顶优先级低于此元素,或遇到左括号"("停止,停止后将此元素入栈 - 若为右括号,则持续将S1栈顶元素作出栈,并,入S2,直到遇到右括号")“停止,此时再对S1栈作一次出栈操作,是为了将其中的左括号”("删去,此时,右括号也不需要,直接忽略即可
遍历结束后,将S1栈边出栈边入S2,最后得到的S2,按照从栈底向栈顶顺序来,此即为后缀表达式的栈。所以此处我用集合替代S2,到最后,集合S2从索引0到尾部即为一个后缀表达式集合
实现两种计算器
这里根据中缀表达式实现两种计算器。一种是直接边分析中缀表达式边实现计算,另一种是先对中缀表达式进行分析,将其转换为后缀表达式后再进行计算
两者优劣
明显,中缀转后缀,再计算结果更符合计算机的逻辑原理,且在计算过程中更不容易出错
中缀表达式计算器
这里的这个实现未实现加入括号的情况,懒得再搞了,头痛
中缀表达式计算器思路
- 将中缀表达式的字符串转为字符数组
- 创建两个栈,一个作为符号栈,一个作为数字栈
- 然后遍历字符数组,若遇到数字,则直接入数字栈
若遇运算符,则进行循环操作,操作如下:
若判断符号栈是否为空,为空则直接入符号栈,跳出循环
若不为空,则判断当前字符和栈顶字符的优先级,若栈顶低,则直接入栈,跳出循环
若栈顶的级别高或和当前运算符优先级相等,则把栈顶的符号出栈,再从数字栈中出栈两个数字num1,num2,然后将其进行运算,然后将得到的结果如数字栈,然后再将当前字符和栈顶字符进行对比,进行循环
循环结束条件:符号栈为空,或栈顶级别低于当前运算符 - 上述操作结束后,对符号栈作出栈操作,每次符号栈出一个,数字栈出俩进行运算,将结果如数字栈,持续操作,到最后留在数字栈中的那个数字即为最终结果
实现
这里使用自己手写的以数组作为底层的栈
class ArrayStack2{
private int top=-1; //栈顶,初始化为-1
private int maxSize; //栈最大容量
private int[] Stack; //栈底层用数组实现
// 构造方法,对栈初始化
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
this.Stack = new int[this.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+"]-------"+Stack[i]);
}
}
// 返回栈顶的值,但并不是出栈
public int peek(){
//这里不用抛出异常,后面用到这个方法是栈里一定有数据的情况下
return Stack[top];
}
}
这里在实现计算器的类中需要三个工具方法
因为char和int是相通的,所以这里的一些本该定义为char类型的形参我用int类型替代
下面直接来实现
public int calculate(String str){
//接受一个中缀表达式的字符串
ArrayStack2 numStack=new ArrayStack2(50);
ArrayStack2 operStack=new ArrayStack2(50);
int index,num1,num2,oper,res;
//index作为字符数组索引,num1和num2用来接收从栈中取出的数字,oper用来接收从栈中取出的运算符,res接收num1、num2、oper运算出的结果
int sum; //在分析中缀表达式的时候,数字可能是多位数,sum用来整合数字
char[] chars=str.toCharArray(); //直接通过字符串的toCharArray()方法把字符串转为字符数组
for (index = 0; index < chars.length; index++) {
//遍历循环此数组
// 当字符为运算符
if (isOper(chars[index])){
while (true) {
// 先判断符号栈是否为空
if (!operStack.isEmpty()) {
// 不为空则先判断当前字符和符号栈顶字符的优先级
if (priority(chars[index]) <= priority(operStack.peek())) {
// 若栈顶的级别高或和当前运算符优先级相等,则把栈顶的符号出栈
oper = operStack.pop();
// 再从数字栈中出栈两个数字num1,num2
num1 = numStack.pop();
num2 = numStack.pop();
// 然后将其进行运算
res = cal(num1, num2, oper);
// 然后将得到的结果如数字栈
numStack.push(res);
} else {
// 栈顶级别低,则直接当前符号入栈
operStack.push(chars[index]);
break;
}
}else {
// 为空则直接入栈
operStack.push(chars[index]);
break;
}
}
}else {
sum=chars[index]-48;
// 当字符为数字,这里整合多位数
while (index+1<chars.length&&!isOper(chars[index+1])){
sum=sum*10+(chars[index+1]-48);
index++;
}
numStack.push(sum);
}
}
while (!operStack.isEmpty()){
// 对残留在栈中的数字字符依次运算
oper=operStack.pop();
num1=numStack.pop();
num2=numStack.pop();
res=cal(num1,num2,oper);
numStack.push(res);
}
return numStack.pop();
}
全部代码
public class Calculator {
public static void main(String[] args) {
CalculatorDemo calculator=new CalculatorDemo();
System.out.println(calculator.calculate("1-2*2+3"));
}
}
class ArrayStack2{
private int top=-1; //栈顶,初始化为-1
private int maxSize; //栈最大容量
private int[] Stack; //栈底层用数组实现
// 构造方法,对栈初始化
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
this.Stack = new int[this.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+"]-------"+Stack[i]);
}
}
// 返回栈顶的值,但并不是出栈
public int peek(){
//这里不用抛出异常,后面用到这个方法是栈里一定有数据的情况下
return Stack[top];
}
}
class CalculatorDemo{
private int priority(int oper){
//获得运算符优先级
if (oper=='/'||oper=='*'){
return 1;
}else if (oper=='+'||oper=='-'){
return 0;
}else {
return -1;
}
}
private boolean isOper(int val){
return val=='*'||val=='/'||val=='-'||val=='+';
}//判断传入的字符是否是运算符
private int cal(int num1,int num2,int pre){
//运算
int res=0;
switch (pre){
case '+':
res = num1+num2;
break;
case '-':
res = num2-num1;
break;
case '*':
res = num1*num2;
break;
case '/':
res = num2/num1;
break;
}
return res;
}
public int calculate(String str){
//接受一个中缀表达式的字符串
ArrayStack2 numStack=new ArrayStack2(50);
ArrayStack2 operStack=new ArrayStack2(50);
int index,num1,num2,oper,res;
//index作为字符数组索引,num1和num2用来接收从栈中取出的数字,oper用来接收从栈中取出的运算符,res接收num1、num2、oper运算出的结果
int sum; //在分析中缀表达式的时候,数字可能是多位数,sum用来整合数字
char[] chars=str.toCharArray(); //直接通过字符串的toCharArray()方法把字符串转为字符数组
for (index = 0; index < chars.length; index++) {
//遍历循环此数组
// 当字符为运算符
if (isOper(chars[index])){
while (true) {
// 先判断符号栈是否为空
if (!operStack.isEmpty()) {
// 不为空则先判断当前字符和符号栈顶字符的优先级
if (priority(chars[index]) <= priority(operStack.peek())) {
// 若栈顶的级别高或和当前运算符优先级相等,则把栈顶的符号出栈
oper = operStack.pop();
// 再从数字栈中出栈两个数字num1,num2
num1 = numStack.pop();
num2 = numStack.pop();
// 然后将其进行运算
res = cal(num1, num2, oper);
// 然后将得到的结果如数字栈
numStack.push(res);
} else {
// 栈顶级别低,则直接当前符号入栈
operStack.push(chars[index]);
break;
}
}else {
// 为空则直接入栈
operStack.push(chars[index]);
break;
}
}
}else {
sum=chars[index]-48;
// 当字符为数字,这里整合多位数
while (index+1<chars.length&&!isOper(chars[index+1])){
sum=sum*10+(chars[index+1]-48);
index++;
}
numStack.push(sum);
}
}
while (!operStack.isEmpty()){
// 对残留在栈中的数字字符依次运算
oper=operStack.pop();
num1=numStack.pop();
num2=numStack.pop();
res=cal(num1,num2,oper);
numStack.push(res);
}
return numStack.pop();
}
}
中缀转后缀计算器
这个更严谨一些,且更准确。更符合计算机来阅读
整体思路就是先把中缀表达式转为后缀表达式,再对后缀表达式进行求值
对后缀表达式求值非常简单,这里面的关键在于如何把中缀表达式转为后缀表达式
中缀表达式转为后缀表达式思路
和百度百科讲的八九不离十,上面有
这里再说一下这个程序的思路
中缀转后缀的思路:
创建两个栈(这里为了方便,用的是一个栈和一个集合)S1,S2。
遍历中缀表达式集合,对其中的元素进行如下判断和操作
- 若元素是数字,则直接入S2。
- 若为左括号"(",则直接入S1栈。
- 若为运算符,则判断S1是否为空,若为空则直接如S1,若不为空,则将此元素与S1栈顶的运算符作比较,若此运算符级别高,则直接如S1,若此元素级别低,则将S1栈顶运算符出栈,并,入S2
然后再将此元素与栈顶运算符作比较,一直循环,直到S1变为空,或栈顶优先级低于此元素,或遇到左括号"("停止,停止后将此元素入栈 - 若为右括号,则持续将S1栈顶元素作出栈,并,入S2,直到遇到右括号")“停止,此时再对S1栈作一次出栈操作,是为了将其中的左括号”("删去,此时,右括号也不需要,直接忽略即可
遍历结束后,将S1栈边出栈边入S2,最后得到的S2,如果S2是栈,按照从栈底向栈顶顺序来,此即为后缀表达式的栈。所以此处我用集合替代S2,到最后,集合S2从索引0到尾部即为一个后缀表达式集合
实现
这里为了方便,不再重复自己编写栈,直接用Java中的栈(Stack类)
- 先两个工具方法,一个判断运算符优先级,一个运算,这里不需要判断是否是优先级的方法(可以搞也可以不搞)
- 处理接收到的中缀表达式,将其中的元素转换为字符串,存储到集合中
- 中缀转后缀
最后得到的S2即为后缀表达式的集合 - 计算后缀表达式
先接收中缀表达式的字符串,在调用方法,转换为后缀表达式,再遍历集合,进行运算
最后将结果进行类型的转换,然后返回即可
全部代码
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class CalculatorSecondTest {
public static void main(String[] args) {
String str="1*(2+3)-6-(6/(1+2))"; //定义一个中缀表达式
CalculatorSecond Calculator=new CalculatorSecond();
try {
System.out.println(str+"="+Calculator.GetResult(str)); //调用方法
}catch (Exception e){
System.out.println(e);
}
}
}
class CalculatorSecond{
// 第一步,中缀表达式字符串转集合
public List<String> ReturnList(String str){
// 创建集合,用来存储中缀表达式中的元素
List<String> StrList=new ArrayList<String>();
int i=0; //索引,用来遍历中缀字符串
char c;//c用来暂存字符串中的单个字符
String string; //string用来暂存中缀表达式中的元素
do {
// 先判断单个字符是否是运算符,是的话直接存入集合中
if ((c=str.charAt(i))<48||(c=str.charAt(i))>57){
StrList.add(c+"");
i++;
}else {
// 若不是,则说明是数字,下面判断数字后面是否紧跟有数字,若有,则说明是多位数
string=""; //初始化string
while (i<str.length()&&((c=str.charAt(i))>=48&&(c=str.charAt(i))<=57)){
string=string+str.charAt(i); //若为多位数,则进行字符串拼接,将多个数字拼在一起成为多位数,并用string接收
i++;
}
StrList.add(string); //将string存入集合
}
}while (i<str.length()); //循环结束的条件是索引小于字符串的长
return StrList; //返回得到的集合
}
// 中缀转后缀
public List<String> ReturnRPN(List<String> Str){
//这里接收一个由中缀表达式中的元素构成的集合
Stack<String> S1=new Stack<String>(); //创建第一个栈S1
List<String> S2=new ArrayList<String>(); //因为最后得到的栈需要进行逆序,这里为了方便,直接用集合代替S2
for (String str: Str){
if (str.matches("\\d+")){
//1. 用正则来判断是否是数字,若元素是数字,则直接入S2。
S2.add(str);
}else if (str.equals("(")){
// 2. 若为左括号"(",则直接入S1栈。
S1.push("(");
}else if (str.equals(")")){
// 3. 若为右括号,则持续将S1栈顶元素作出栈,并,入S2,直到遇到右括号")"停止
while (!S1.peek().equals("(")){
S2.add(S1.pop());
}
S1.pop();//此时再对S1栈作一次出栈操作,是为了将其中的左括号"("删去,此时,右括号也不需要,直接忽略即可
}else{
// 4. 若为运算符,则判断S1是否为空,若为空则直接如S1,若不为空,则将此元素与S1栈顶的运算符作比较,若此运算符级别高,则直接如S1,若此元素级别低,则将S1栈顶运算符出栈,并,入S2
// 然后再将此元素与栈顶运算符作比较,一直循环,直到S1变为空,或栈顶优先级低于此元素,或遇到左括号"("停止,停止后将此元素入栈
while (S1.size()!=0&&!S1.peek().equals("(")&&compare(S1.peek())>=compare(str)){
// 为防止出现异常,先判断S1,是否为空,再判断S1栈顶是否是左括号,再判断优先级 compare是自己编写的一个根据运算符返回对应级别的方法
S2.add(S1.pop());
}
S1.push(str); //循环结束后,将当前运算符入栈
}
}
while (S1.size()!=0){
// 遍历结束后,将S1栈边出栈边入S2,最后得到的S2,按照从栈底向栈顶顺序来,此即为后缀表达式的栈。所以此处我用集合替代S2,到最后,集合S2从索引0到尾部即为一个后缀表达式集合
// 最后循环遍历S1,将S1中元素入S2
S2.add(S1.pop());
}
System.out.println("S2----"+S2);
return S2;
}
// 判断运算符优先级
public int compare(String string){
if (string.equals("+")||string.equals("-")){
return 0;
}else if (string.equals("*")||string.equals("/")){
return 1;
}else {
throw new RuntimeException("符号错误");
}
}
// 计算后缀表达式
public int GetResult(String str){
// 计算后缀表达式非常简单,先定义栈,然后对后缀表达式的集合进行遍历。若为数字则直接如栈,若为运算符,则从占中出栈两个元素根据此运算符进行运算,将运算结果再入栈。
// 循环此操作,最后留在栈中的那个元素则为原中缀表达式的结果
List<String> list=ReturnRPN(ReturnList(str));
Stack<String> stack=new Stack<String>();
for (String string:list){
if (string.matches("\\d+")){
stack.push(string);
}else {
int num1=Integer.parseInt(stack.pop());
int num2=Integer.parseInt(stack.pop());
int num=operation(num1,num2,string);
stack.push(num+"");
}
}
return Integer.parseInt(stack.pop()); //转为整形
}
// 运算
public int operation(int num1,int num2,String str){
if (str.equals("+")){
return num1+num2;
}else if (str.equals("-")){
return num2-num1;
}else if (str.equals("*")){
return num1*num2;
}else if(str.equals("/")){
return num2/num1;
}else {
throw new RuntimeException("运算出错");
}
}
}