Java数据结构之栈与计算器

栈的一个实际需求

请输入一个表达式
计算式:[722-5+1-5+3-3] 点击计算【如下图】
在这里插入图片描述
请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题-> 栈。

栈的介绍

  1. 栈的英文为(stack) 栈是一个先入后出(FILO-First In Last Out)的有序列表。
  2. 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。
  3. 允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
  4. 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
  5. 出栈(pop)和入栈(push)的概念(如图所示)
    在这里插入图片描述
    在这里插入图片描述

栈的应用场景

  1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
  2. 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
  3. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
  4. 二叉树的遍历。
  5. 图形的深度优先(depth一first)搜索法。

用数组模拟栈

思路:
在这里插入图片描述
代码:

import java.util.Scanner;

public class ArrayStackDemo {

	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.list();
				break;
			case "push":
				System.out.println("请输入一个数:");
				int value = scanner.nextInt();
				stack.push(value);
				break;
			case "pop":
				try {
					System.out.println("出栈的数据为:"+stack.pop());
				} catch (Exception e) {
					System.out.println(e.getMessage());
				}
				break;
			case "exit":
				scanner.close();
				loop = false;
				break;
			}
		}
		System.out.println("程序退出");
		
	}

}

//定义一个ArrayStack表示栈
class ArrayStack{
	private int maxSize;//栈的大小
	private int[] stack;//用数组模拟栈
	private int top=-1;//表示栈顶
	
	//构造函数
	public ArrayStack(int maxSize) {
		this.maxSize=maxSize;
		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;
		}
		top++;
		stack[top]=value;
	}
	
	//出栈
	public int pop() {
		//先判断是否为空
		if(isEmpty()) {
			//抛出异常
			throw new RuntimeException("栈空,没有数据");
		}
		return stack[top--];
	}
	
	//遍历栈,从栈顶开始显示数据
	public void list() {
		//判断是否为空
		if(isEmpty()) {
			System.out.println("栈空,没有数据");
			return;
		}
		//从栈顶显示
		for(int i=top;i>=0;i--) {
			System.out.printf("stack[%d]=%d\n",i,stack[i]);
		}
	}
}

编写一个计算器(支持正负、加减乘除运算,不支持括号)

public class Calculator {

	public static void main(String[] args) {
		// 测试
		String expression = "-10-6/3*2+5*2/2";//中缀表达式
		// 创建数栈和符号栈
		ArrayStack2 numStack = new ArrayStack2(10);
		ArrayStack2 operStack = new ArrayStack2(10);
		// 定义扫描
		int index = 0;
		int flag = 0;// 判断是否为负数
		int num1 = 0;
		int num2 = 0;
		int oper = 0;
		int res = 0;
		char ch = ' ';// 将每次扫描得到的char保存如ch
		String keepNum = "";// 用于拼接多位数
		// 开始扫描
		while (true) {
			// 依次得到每一个字符
			ch = expression.charAt(index);
			// 判断ch是什么,然后再做相应处理
			if (operStack.isOper(ch)) {// 如果是运算符
				// 判断是否为-号
				if (ch == '-') {
					// 判断是否在算式首位
					if (index == 0) {
						flag = 1;
						index++;
						continue;
					} else {
						flag = 1;
						ch = '+';
					}
				}
				// 判断当前符号栈是否为空
				if (!operStack.isEmpty()) {
					// 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符, 就需要从数栈中pop出两个数,
					// 在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
					if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
						num1 = numStack.pop();
						num2 = numStack.pop();
						oper = operStack.pop();
						res = numStack.cal(num1, num2, oper);
						// 把运算结果如数栈
						numStack.push(res);
						// 把当前操作符入符号栈
						operStack.push(ch);
					} else {
						operStack.push(ch);
					}
				} else {// 如果为空则直接入栈
					operStack.push(ch);
				}
			} else {// 如果是数,则直接入数栈
				// 如果当前操作符优先级大于栈顶的操作符,则直接如栈
				// numStack.push(ch - '0');不能发现一个数就立即入栈,因为可能是多位数

				// 故需要再往后看一位,如果是数则继续扫描,如果是符号则可以如栈
				// 因此需要定义一个字符串变量用于拼接
				keepNum += ch;

				// 如果ch已经是expression的最后一位,就直接入栈
				if (index == expression.length() - 1) {
					if (flag == 1) {
						numStack.push(-Integer.parseInt(keepNum));
					} else {
						numStack.push(Integer.parseInt(keepNum));
					}
				} else {
					// 判断下一个是不是数字,如果是数字,则继续扫描;如果是运算符,则入栈
					// 不能用index++
					if (operStack.isOper(expression.charAt(index + 1))) {
						// 入栈
						if (flag == 1) {
							numStack.push(-Integer.parseInt(keepNum));
						} else {
							numStack.push(Integer.parseInt(keepNum));
						}
						// 注意:一定要清空字符串
						keepNum = "";
						flag = 0;
					}
				}
			}
			// 让index加一,判断是否扫描到最后
			index++;
			if (index >= expression.length()) {
				break;
			}
		}
		// 当扫描完毕,则顺序从数栈和符号栈中pop出数和符号,并计算
		while (true) {
			// 当符号栈为空,则计算到最后的结果
			if (operStack.isEmpty()) {
				break;
			}
			num1 = numStack.pop();
			num2 = numStack.pop();
			oper = operStack.pop();
			res = numStack.cal(num1, num2, oper);
			numStack.push(res);
		}
		// 将数栈的最后数pop出来
		System.out.printf("表达式%s = %d", expression, numStack.pop());
	}
}

// 定义一个ArrayStack2表示栈
class ArrayStack2 {
	private int maxSize;// 栈的大小
	private int[] stack;// 用数组模拟栈
	private int top = -1;// 表示栈顶

	// 构造函数
	public ArrayStack2(int maxSize) {
		this.maxSize = maxSize;
		stack = new int[this.maxSize];
	}

	// 增加一个方法,返回栈顶元素,但不是真正的pop
	public int peek() {
		return stack[top];
	}

	// 栈满
	public boolean isFull() {
		return top == maxSize - 1;
	}

	// 栈空
	public boolean isEmpty() {
		return top == -1;
	}

	// 入栈
	public void push(int value) {
		// 先判断是否满
		if (isFull()) {
			System.out.println("栈满");
			return;
		}
		top++;
		stack[top] = value;
	}

	// 出栈
	public int pop() {
		// 先判断是否为空
		if (isEmpty()) {
			// 抛出异常
			throw new RuntimeException("栈空,没有数据");
		}
		return stack[top--];
	}

	// 遍历栈,从栈顶开始显示数据
	public void list() {
		// 判断是否为空
		if (isEmpty()) {
			System.out.println("栈空,没有数据");
			return;
		}
		// 从栈顶显示
		for (int i = top; i >= 0; i--) {
			System.out.printf("stack[%d]=%d\n", i, stack[i]);
		}
	}

	// 返回运算符优先级,用数字表示
	// 数字越大,优先级越高
	public int priority(int i) {
		if (i == '*' || i == '/') {
			return 1;
		} else if (i == '+' || i == '-') {
			return 0;
		} else {
			return -1;
		}
	}

	// 判断是不是一个运算符
	public boolean isOper(int val) {
		return val == '+' || val == '-' || val == '*' || val == '/';
	}

	// 计算方法
	public int cal(int num1, int num2, int oper) {
		int res = 0;// 用于存放计算结果
		switch (oper) {
		case '+':
			res = num1 + num2;
			break;
		case '-':
			res = num2 - num1;// 注意顺序
			break;
		case '*':
			res = num1 * num2;
			break;
		case '/':
			res = num2 / num1;
			break;
		default:
			break;
		}
		return res;
	}
}

前缀、中缀、后缀表达式

前缀表达式(波兰表达式):

  • 缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
  • 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6

前缀表达式的计算机求值:
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。

例如:(3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:

  1. 从右至左扫描,将6、5、4、3压入堆栈;
  2. 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈;
  3. 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈;
  4. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

中缀表达式:

  • 中缀表达式就是常见的运算表达式,如(3+4)×5-6
  • 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)
  • 上面的例子就是用中缀表达式来做的一个计算器

后缀表达式(逆波兰表达式):

  • 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。
  • 举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
  • 再比如:
    在这里插入图片描述

后缀表达式的计算机求值:
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。

例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:

  1. 从左至右扫描,将3和4压入堆栈;
  2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
  3. 将5入栈;
  4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
  5. 将6入栈;
  6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

下面我们完成一个逆波兰计算器:(已经将算式转换为了逆波兰表达式)

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class PolandNotation {

	public static void main(String[] args) {
		//先定义一个逆波兰表达式
		//(30+4)*5-6 => 30 4 + 5 * 6 -
		//为了方便,数字和符号间用空格隔开
		String suffixExpression = "30 4 + 5 * 6 -";
		//思路:
		//1.先将3 4 + 5 * 6 -放到ArrayList中
		//2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
		
		List<String> list = getListString(suffixExpression);
		
		int res = calculate(list);
		System.out.println("计算的结果是:"+res);
	}
	
	//将逆波兰表达式,依次将数据放入ArrayList中
	public static List<String> getListString(String suffixExpression){
		//将suffixExpression分割
		String[] split = suffixExpression.split(" ");
		List<String> list = new ArrayList<String>();
		for(String str:split) {
			list.add(str);
		}
		return list;
	}
	
	//完成对逆波兰表达式的计算
	public static int calculate(List<String> list) {
		//创建一个栈
		Stack<String> stack = new Stack<String>();
		//遍历
		for(String item:list) {
			//这里使用正则表达式取出数
			if(item.matches("\\d+")) {//匹配的是多位数
				//入栈
				stack.push(item);
			}else {
				//pop出两个数并计算,再入栈
				int num2=Integer.parseInt(stack.pop());
				int num1=Integer.parseInt(stack.pop());
				int res=0;
				switch(item) {
				case "+":
					res=num1+num2;
					break;
				case "-":
					res=num1-num2;
					break;
				case "*":
					res=num1*num2;
					break;
				case "/":
					res=num1/num2;
					break;
				default:
					throw new RuntimeException("运算符有误");
				}
				//把res入栈
				stack.push(""+res);//把整数转换为字符串
			}
		}
		//最终留在stack中的数据即为运算结果
		return Integer.parseInt(stack.pop());
	}
}

如何将中缀表达式转换为逆波兰表达式并计算

具体步骤如下:

  1. 初始化两个栈:运算符栈s1和储存中间结果的栈s2;

  2. 从左至右扫描中缀表达式;

  3. 遇到操作数时,将其压s2;

  4. 遇到运算符时,比较其与s1栈顶运算符的优先级:
    (1) 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
    (2) 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
    (3) 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;

  5. 遇到括号时:
    (1) 如果是左括号“(”,则直接压入s1;
    (2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃;

  6. 重复步骤2至5,直到表达式的最右边;

  7. 将s1中剩余的运算符依次弹出并压入s2;

  8. 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。

分析图解:
在这里插入图片描述
代码:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class PolandNotation {

	public static void main(String[] args) {

		// 完成将中缀表达式转后缀表达式
		// 1. 1+((2+3)*4)-5 => 1 2 3 + 4 * + 5 –
		// 2. 因为对字符串操作不方便,因此可以先将1+((2+3)*4)-5的中缀表达式转为ArrayList
		// 3.将中缀表达式对应的List转为后缀表达式对应的List
		String expression = "1+((2+3)*4)-5";
		System.out.println("原算式为:"+expression);
		List<String> infixExpression = toInfixExpressionList(expression);
		System.out.println("中缀表达式" + infixExpression);
		List<String> suffixExpression = parseSuffixExpressionList(infixExpression);
		System.out.println("后缀表达式" + suffixExpression);
		System.out.println("最终结果为:" + calculate(suffixExpression));
	}

	// 将中缀表达式转换为List
	public static List<String> toInfixExpressionList(String s) {
		// 定义一个List存放中缀表达式内容
		List<String> list = new ArrayList<String>();
		int index = 0;// 用来扫描字符串
		String str;// 用于拼接多位数
		char c;// 每遍历一个字符存入c中
		do {
			// 如果是一个非数字,则加入list中
			if ((c = s.charAt(index)) < '0' || (c = s.charAt(index)) > '9') {
				list.add("" + c);// 将字符转换为字符串
				index++;// 后移
			} else {// 如果是一个数,则要考虑多位数
				str = "";// 先将str清空
				while (index < s.length() && (c = s.charAt(index)) >= '0' && (c = s.charAt(index)) <= '9') {
					str += c;// 进行拼接
					index++;
				}
				list.add(str);
			}
		} while (index < s.length());
		return list;
	}
	
	//将List转换为逆波兰表达式
	// [1,+,(,(,2,+,3,),*,4,),-,5] -> [1,2,3,+,4,*,+,5,–]
	public static List<String> parseSuffixExpressionList(List<String> list) {
		// 定义两个栈
		Stack<String> s1 = new Stack<String>();
		// 说明:因为s2栈在整个过程没有pop操作,而且最后需要逆序输出,太麻烦,所以可以直接用List替代s2栈
		List<String> s2 = new ArrayList<String>();
		// 遍历list
		for (String item : list) {
			if (item.matches("\\d+")) {// 如果是一个数,则直接加入s2中
				s2.add(item);
			} else if (item.equals("(")) {
				s1.push(item);
			} else if (item.equals(")")) {
				// 如果是右括号,则依次弹出s1栈顶的元素,并加入s2中,直到遇到左括号为止,此时将这一对括号丢弃
				while (!s1.peek().equals("(")) {
					s2.add(s1.pop());
				}
				s1.pop();// 消除小括号
			} else {
				// 当item的优先级小于等于s1栈顶的优先级,将s1栈顶的运算符弹出并加入到s2中,再将item与s1中新的栈顶运算符相比较
				// 添加一个比较优先级高低的方法
				while (s1.size() != 0 && getValue(item) <= getValue(s1.peek())) {
					s2.add(s1.pop());
				}
				// 还需要将item压入栈中
				s1.push(item);
			}
		}
		// 将s1中剩余的元素符加入s2中
		while (s1.size() != 0) {
			s2.add(s1.pop());
		}
		return s2;
	}

	// 完成对逆波兰表达式的计算
	public static int calculate(List<String> list) {
		// 创建一个栈
		Stack<String> stack = new Stack<String>();
		// 遍历
		for (String item : list) {
			// 这里使用正则表达式取出数
			if (item.matches("\\d+")) {// 匹配的是多位数
				// 入栈
				stack.push(item);
			} else {
				// pop出两个数并计算,再入栈
				int num2 = Integer.parseInt(stack.pop());
				int num1 = Integer.parseInt(stack.pop());
				int res = 0;
				switch (item) {
				case "+":
					res = num1 + num2;
					break;
				case "-":
					res = num1 - num2;
					break;
				case "*":
					res = num1 * num2;
					break;
				case "/":
					res = num1 / num2;
					break;
				default:
					throw new RuntimeException("运算符有误");
				}
				// 把res入栈
				stack.push("" + res);// 把整数转换为字符串
			}
		}
		// 最终留在stack中的数据即为运算结果
		return Integer.parseInt(stack.pop());
	}
	
	//一个方法,用于返回优先级
	public static int getValue(String operator) {
		switch (operator) {
		case "+":
			return 1;
		case "-":
			return 1;
		case "*":
			return 2;
		case "/":
			return 2;
		case "(":
			return 0;
		default:
			System.out.println("不存在该运算符");
			break;
		}
		return 0;
	}
}

注: 仅支持加减乘除、括号的整数运算

发布了30 篇原创文章 · 获赞 33 · 访问量 1254

猜你喜欢

转载自blog.csdn.net/weixin_45949075/article/details/105621918