Java数据结构与算法 day03 栈

文章目录

  • 第四章 栈
    • 栈的应用场景和介绍
      • 栈的介绍
      • 栈的应用场景
    • 栈的思路分析及实现
    • 栈实现综合计算器-思路分析及实现
    • 前缀 中缀 后缀表达式规则
      • 前缀表达式(波兰表达式)
      • 中缀表达式
      • 后缀表达式
    • 逆波兰计算器分析和实现
    • 中缀转后缀表达式思路分析及实现
    • 完整版逆波兰计算器
    • 本章导图总结

第四章 栈

本章源码:https://github.com/name365/Java-Data-structure

栈的应用场景和介绍

先看一个例子

请输入一个表达式

计算式:[7*2*2-5+1-5+3-3] 点击计算【如下图】

Java数据结构与算法 day03 栈_第1张图片

请问: 计算机底层是如何运算得到结果的?

注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。—>

栈的介绍

1.栈的英文为(stack)  
2.栈是一个先入后出(FILO-First In Last Out)的有序列表。
    > 先入后出:先进入的数据后出来。

3.(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)4.根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
5.出栈(pop)和入栈(push)的概念(如下图所示)    

Java数据结构与算法 day03 栈_第2张图片
Java数据结构与算法 day03 栈_第3张图片

栈的应用场景

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

栈的思路分析及实现

1. 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈,入栈等操作。
2. 实现思路分析,示意图如下:
  1. 使用数组来模拟栈
  2. 定义一个 top 来表示栈顶,初始化 为 -1

Java数据结构与算法 day03 栈_第4张图片

  1. 入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
  2. 出栈的操作, int value = stack[top]; top–, return value;
  3. 代码如下(用数组描述):
import java.util.Scanner;

public class ArrayStackDemo {

	public static void main(String[] args) {
		// 测试代码如下:
		// 先创建一个对象---》表示栈
		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 {
					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("程序退出!!!!");
	}

}

// 定义一个类,表示栈
class ArrayStack {
	private int maxSize; // 栈的大小
	private int[] stack; // 数组,模拟栈,数据就放在该数组
	private int top = -1; // 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;
	}

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

	// 显示栈的情况[便利栈],遍历时,需要从栈顶开始显示数据
	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]);
		}
	}
}
  • ​ 使用链表来模拟栈
import java.util.Scanner;

public class LinkTest {
	public static void main(String[] args) {
		// 测试代码如下:
		// 先创建一个对象---》表示栈
		LinkedListStack stack = new LinkedListStack();
		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();
				LinkedList list = new LinkedList(value);
				stack.push(list);
				break;
			case "pop":
				try {
					LinkedList 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("程序退出!!!!");
	}
}

// 链表操作
class LinkedListStack {
	private LinkedList head = new LinkedList(-1); // 先初始化一个头结点,头结点不动
	private LinkedList top = null; // 定义一个节点,代表栈顶所指节点

	public LinkedList getHead() { // 创建一个获取头结点的方法
		return head;
	}

	// 判断栈空
	public boolean isEmpty() {
		return top == null;
	}

	public void push(LinkedList num) {
		// 因为头结点不能动,所以需要一个辅助节点来完成
		LinkedList temp = head;
		// 对链表进行遍历,遍历到最后一个节点,然后进行添加节点
		while (true) {
			// 判断是否为节点的最后
			if (temp.next == null) {
				// 当为空时,说明已经是最后一个节点
				break;
			}
			// 如果不为空,则指针向后移动
			temp = temp.next;
		}
		// 当退出while循环时,说明到了最后一个节点
		// 将节点添加到链表的最后
		temp.next = num;
		// 将top指向这个节点
		top = num;
	}

	// 删除节点从单向链表实现的栈中,即出栈
	public LinkedList pop() {
		// 将top指针指向的节点出栈
		// 因为出栈后,top指针需要向前移动,所以需要一个辅助指针完成出栈
		LinkedList tmp = top;
		// 将top指针向前移动一个
		// 需要重新遍历链表找到top节点的前一个节点,再由top指针指向这个节点
		LinkedList h = head;
		while (true) {
			// 找到top节点的前一个节点
			if (h.next == top) {
				// 说明找到了这个节点
				break;
			}
			// 如果不是,则指针向后移动
			h = h.next;
		}
		// 退出循环后,找到top节点的前一个节点,对top节点进行删除
		h.next = top.next;
		// 将top指针指向这个节点,完成top指针的前移动作
		top = h;
		return tmp;
	}

	// 显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据,即从链表尾遍历链表
	// 将链表进行反转,然后再打印链表
	public void list() {
		// 判断链表是否为空
		if (head.next == null) {
			System.out.println("链表为空!!!");
			return;
		}
		// 因为头结点不能动,需要辅助变量来遍历
		reverseList(head);
		LinkedList temp = head.next;
		while (true) {
			// 判断是否到链表最后
			if (temp == null) {
				break;
			}
			// 输出节点信息
			System.out.println(temp);
			// 将temp后移
			temp = temp.next;
		}
	}

	// 栈的遍历需要先将链表进行反转
	// 链表反转
	public void reverseList(LinkedList head) {
		// 如果当前链表为空,或者只有一个节点,无需反转,直接返回
		if (head.next == null || head.next.next == null) {
			return;
		}
		// 定义一个辅助的指针(变量),帮助遍历原来的链表
		LinkedList cur = head.next;
		// 定义一个变量存储当前节点的下个节点,即指向当前节点[cur]的下个节点
		LinkedList next = null;
		// 定义一个新链表的头结点
		LinkedList reverseHead = new LinkedList(-1);
		// 遍历原来的链表,每遍历一个节点就将其取出,并放在新的链表reverseHead的最前端
		while (cur != null) {
			// 暂时保存当前节点的下一个节点(后续需要使用)
			next = cur.next;
			// 将cur的下个节点指向新的链表的最前端
			cur.next = reverseHead.next;
			// 将cur连接到新的链表上
			reverseHead.next = cur;
			// 让cur后移
			cur = next;
		}
		// 将head.next指向reverseHead.next,实现单链表的反转
		head.next = reverseHead.next;
	}
}

// 节点
class LinkedList {
	public int val; // 存储的数据
	public LinkedList next; // 下一个节点

	public LinkedList(int val) {
		this.val = val;
	}

	@Override
	public String toString() {
		return "LinkedList [val=" + val + ", next=" + next + "]";
	}
}

栈实现综合计算器-思路分析及实现

使用栈完成计算 一个表达式的结果

在这里插入图片描述

使用栈完成表达式的计算思路

Java数据结构与算法 day03 栈_第5张图片

  1. 通过一个 index 值(索引),来遍历我们的表达式

  2. 如果我们发现是一个数字, 就直接入数栈

  3. 如果发现扫描到是一个符号, 就分如下情况

    1. 如果发现当前的符号栈为 空,就直接入栈
    2. 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符, 就需要从数栈中pop出两个数,在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈, 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈.

Java数据结构与算法 day03 栈_第6张图片

  1. 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行.

  2. 最后在数栈只有一个数字,就是表达式的结果

验证: 3+2*6-2 = 13

Java数据结构与算法 day03 栈_第7张图片
Java数据结构与算法 day03 栈_第8张图片
Java数据结构与算法 day03 栈_第9张图片
Java数据结构与算法 day03 栈_第10张图片
Java数据结构与算法 day03 栈_第11张图片
Java数据结构与算法 day03 栈_第12张图片
Java数据结构与算法 day03 栈_第13张图片

代码实现如下(单位数运算写法):

public class Calculator {

	public static void main(String[] args) {
		//测试一下
		String exception  = "3+2*6-2";
		//创建两个栈,一个数栈,一个符号栈
		ArrayStack numS = new ArrayStack(10);
		ArrayStack operS = new ArrayStack(10);
		//定义两个相关变量
		int index = 0;	//用于扫描
		int num = 0;
		int num2 = 0;
		int oper = 0;
		int res = 0;
		char ch = ' ';	//每次的扫描结果保存到ch
		//k=开始用while语句循环扫描exception
		while(true){
			//依次得到exception 的每一个字符
			ch = exception.substring(index,index+1).charAt(0);
			//判断ch,做出相应的处理
			if(operS.isOper(ch)){
				//判断当前的符号栈是否为空,为空则入栈
				if(!operS.isEmpty()){
					//如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,
					//在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
					if(operS.priority(ch) <= operS.priority(operS.peek())) {
						num = numS.pop();
						num2 = numS.pop();
						oper = operS.pop();
						res = numS.cal(num, num2, oper);
						//将运算结果入数栈
						numS.push(res);
						//将当前的操作符入符号栈
						operS.push(ch);
					}else{
						//如果当前的优先级大于栈中的操作符,就直接入符号栈
						operS.push(ch);
					}
				}else{
					//如果为空,则直接入符号栈
					operS.push(ch); 	//1 + 3
				}
			}else{//如果是数,则直接入数栈
//				numS.push(ch); //错误的写法:读取的是字符不是数字
				numS.push(ch - 48);
			}
			//让index+1,并判断是否扫描到exception的最后
			index++;
			if(index >= exception.length()){
				break;
			}
		}
		//当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
		while(true) {
			//如果符号栈为空,则计算到最后的结果,数栈中只有一个数字[结果]
			if(operS.isEmpty()) {
				break;
			}
			num = numS.pop();
			num2 = numS.pop();
			oper = operS.pop();
			res = numS.cal(num, num2, oper);
			numS.push(res);		//入栈
		}
		//将数栈的最后数,pop出,就是结果
		int res2 = numS.pop();
		System.out.printf("表达式 %s = %d", exception, res2);
	}

}
//先创建一个栈,直接使用前面的
//定义一个类,表示栈
class ArrayStack {
	private int maxSize; // 栈的大小
	private int[] stack; // 数组,模拟栈,数据就放在该数组
	private int top = -1; // top表示栈,初始化为-1

	// 构造器
	public ArrayStack(int maxSize) {
		this.maxSize = maxSize;
		stack = new int[this.maxSize];
	}
	
	//自定义一个方法,可以返回当前栈顶的值 ,但不是真的出栈
	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;
	}

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

	// 显示栈的情况[便利栈],遍历时,需要从栈顶开始显示数据
	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 oper){
		if(oper == '*' || oper == '/'){
			return 1;
		}else if(oper == '+' || oper == '-'){
			return 0;
		}else{
			return -1;	//假设目前表达式只有+ - * /
		}
	}
	//判断是不是一个运算符
	public boolean isOper(char val){
		return val == '+' || val == '-' || val == '*' || val == '/';
	}
	//计算方法
	public int cal(int num,int num2,int oper){
		int res = 0;	//用于存放反计算的结果
		switch(oper){
		case '+':
			res = num + num2;
			break;
		case '-':
			res = num2 - num;	//注意顺序
			break;
		case '*':
			res = num * num2;
			break;
		case '/':
			res = num2 / num;
			break;	
		default:
			break;
		}
		return res;
	}
}

多位数运算写法:

public class Calculator2 {

	public static void main(String[] args) {
		// 测试一下
		String exception = "7*2*2-5+1-5+3-4";
		// 创建两个栈,一个数栈,一个符号栈
		ArrayStack2 numS = new ArrayStack2(10);
		ArrayStack2 operS = new ArrayStack2(10);
		// 定义两个相关变量
		int index = 0; // 用于扫描
		int num = 0;
		int num2 = 0;
		int oper = 0;
		int res = 0;
		char ch = ' '; // 每次的扫描结果保存到ch
		String keepNum = ""; // 用于拼接 多位数
		// k=开始用while语句循环扫描exception
		while (true) {
			// 依次得到exception 的每一个字符
			ch = exception.substring(index, index + 1).charAt(0);
			// 判断ch,做出相应的处理
			if (operS.isOper(ch)) {
				// 判断当前的符号栈是否为空,为空则入栈
				if (!operS.isEmpty()) {
					// 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,
					// 在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
					if (operS.priority(ch) <= operS.priority(operS.peek())) {
						num = numS.pop();
						num2 = numS.pop();
						oper = operS.pop();
						res = numS.cal(num, num2, oper);
						// 将运算结果入数栈
						numS.push(res);
						// 将当前的操作符入符号栈
						operS.push(ch);
					} else {
						// 如果当前的优先级大于栈中的操作符,就直接入符号栈
						operS.push(ch);
					}
				} else {
					// 如果为空,则直接入符号栈
					operS.push(ch); // 1 + 3
				}
			} else {// 如果是数,则直接入数栈
				// numS.push(ch); //错误的写法:读取的是字符不是数字
				// numS.push(ch - 48);
				// 分析思路如下:
				// 1.当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数
				// 2.在处理数,需要向exception的表达式的index 后再看一位,如果是数就进行扫描,如果是符号才入栈
				// 3.因此我们需要定义一个变量 字符串,用于拼接

				// 处理多位数
				keepNum += ch;

				// 如果ch已经是exception的最后一位,就直接入栈
				if (index == exception.length() - 1) {
					numS.push(Integer.parseInt(keepNum));
				} else {
					// 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
					// 注意是看后一位,不是index++
					if (operS.isOper(exception.substring(index + 1, index + 2).charAt(0))) {
						// 如果后一位是运算符,则入栈 keepNum = "1" 或者 "123"
						numS.push(Integer.parseInt(keepNum));
						// 注意!!!!!!,keepNum清空
						keepNum = "";

					}
				}
			}
			// 让index+1,并判断是否扫描到exception的最后
			index++;
			if (index >= exception.length()) {
				break;
			}
		}
		// 当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
		while (true) {
			// 如果符号栈为空,则计算到最后的结果,数栈中只有一个数字[结果]
			if (operS.isEmpty()) {
				break;
			}
			num = numS.pop();
			num2 = numS.pop();
			oper = operS.pop();
			res = numS.cal(num, num2, oper);
			numS.push(res); // 入栈
		}
		// 将数栈的最后数,pop出,就是结果
		int res2 = numS.pop();
		System.out.printf("表达式 %s = %d", exception, res2);
	}

}

// 先创建一个栈,直接使用前面的
// 定义一个类,表示栈
class ArrayStack2 {
	private int maxSize; // 栈的大小
	private int[] stack; // 数组,模拟栈,数据就放在该数组
	private int top = -1; // top表示栈,初始化为-1

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

	// 自定义一个方法,可以返回当前栈顶的值 ,但不是真的出栈
	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;
	}

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

	// 显示栈的情况[便利栈],遍历时,需要从栈顶开始显示数据
	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 oper) {
		if (oper == '*' || oper == '/') {
			return 1;
		} else if (oper == '+' || oper == '-') {
			return 0;
		} else {
			return -1; // 假设目前表达式只有+ - * /
		}
	}

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

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

前缀 中缀 后缀表达式规则

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

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

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

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

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

中缀表达式

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

后缀表达式

  1. 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
  2. 中举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
  3. 再比如:
正常的表达式 逆波兰表达式
a+b a b +
a+(b-c) a b c - +
a+(b-c)*d a b c – d * +
a+d*(b-c) a d b c - * +
a=1+3 a 1 3 + =
后缀表达式的计算机求值

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

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

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

逆波兰计算器分析和实现

完成一个逆波兰计算器,要求完成如下任务:

  1. 输入一个逆波兰表达式(后缀表达式),使用栈(Stack),计算其结果

  2. 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。

  3. 思路分析

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

1.从左至右扫描,将34压入堆栈;
2.遇到+运算符,因此弹出434为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
3.5入栈;
4.接下来是×运算符,因此弹出57,计算出7×5=35,将35入栈;
5.6入栈;
6.最后是-运算符,计算出35-6的值,即29,由此得出最终结果	
  1. 代码完成
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class PolandNotation {

	public static void main(String[] args) {
		//先定义一个逆波兰表达式
		//测试1:(3+4)x5-6 =》 3 4 + 5 x 6 -	=》 29
		//测试2:(30+4)x5-6 =》 30 4 + 5 x 6 - =》 164
		//中缀表达式:4 * 5 - 8 + 60 + 8 / 2 =》后缀表达式:4 5 * 8 - 60 + 8 2 / +
		//说明:为了方便,逆波兰表达式的数字和符号使用空格隔开
		String suffiException = "3 4 + 5 * 6 -";
		//具体思路:
		//1.先将“ 3 4 + 5 x 6 - ” =》 放到ArrayList中
		//2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
		
		List<String> rpnList = getListString(suffiException);
		System.out.println("rpnList = " + rpnList);
		
		int res = calculate(rpnList);
		System.out.println("计算的结果是:" + res);
	}

	//将一个逆波兰表达式,一次将数据和运算符放入ArrayList中
	public static List<String> getListString(String suffiException){
		//将suffiException分割
		String[] split = suffiException.split(" ");
		List<String> list = new ArrayList<String>();
		for(String ele:split){
			list.add(ele);
		}
		return list;
	}
	
	//完成对逆波兰表达式的计算
	/*
	 * 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,由此得出最终结果	
	 */
	public static int calculate(List<String> ls){
		//创建一个栈
		Stack<String> stack = new Stack<String>();
		//遍历
		for(String item:ls){
			//使用正则表达式取值
			if(item.matches("\\d+")){	//匹配多位数的正则表达式
				//入栈
				stack.push(item);
			}else{
				//pop出两个数,并运算,再入栈
				int num2 = Integer.parseInt(stack.pop());	//将字符串转成整数
				int num = Integer.parseInt(stack.pop());
				int res = 0;	//存放判断结果
				if(item.equals("+")){
					res = num + num2;
				}else if(item.equals("-")){
					res = num - num2;
				}else if(item.equals("*")){
					res = num * num2;
				}else if(item.equals("/")){
					res = num / num2;
				}else{
					throw new RuntimeException("运算符有问题!!!");
				}
				//将res入栈
				stack.push(res + "");	//整数转字符串
			}
		}
		//最后留在stack中的数据即为结果
		return Integer.parseInt(stack.pop());	//字符串转整数
 	}
}

中缀转后缀表达式思路分析及实现

大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式

举例说明:

将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下:

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

Java数据结构与算法 day03 栈_第14张图片

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

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

Java数据结构与算法 day03 栈_第15张图片

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

Java数据结构与算法 day03 栈_第16张图片

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

Java数据结构与算法 day03 栈_第17张图片
Java数据结构与算法 day03 栈_第18张图片

6. 重复步骤25,直到表达式的最右边
                           
7. 将s1中剩余的运算符依次弹出并压入s2
                           
8. 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式.

Java数据结构与算法 day03 栈_第19张图片
Java数据结构与算法 day03 栈_第20张图片
Java数据结构与算法 day03 栈_第21张图片
Java数据结构与算法 day03 栈_第22张图片
Java数据结构与算法 day03 栈_第23张图片
Java数据结构与算法 day03 栈_第24张图片

完整步骤:

扫描到的元素 s2(栈底->栈顶) s1 (栈底->栈顶) 说明
1 1 数字,直接入栈
+ 1 + s1为空,运算符直接入栈
( 1 + ( 左括号,直接入栈
( 1 + ( ( 同上
2 1 2 + ( ( 数字
+ 1 2 + ( ( + s1栈顶为左括号,运算符直接入栈
3 1 2 3 + ( ( + 数字
) 1 2 3 + + ( 右括号,弹出运算符直至遇到左括号
× 1 2 3 + + ( × s1栈顶为左括号,运算符直接入栈
4 1 2 3 + 4 + ( × 数字
) 1 2 3 + 4 × + 右括号,弹出运算符直至遇到左括号
- 1 2 3 + 4 × + - -与+优先级相同,因此弹出+,再压入-
5 1 2 3 + 4 × + 5 - 数字
到达最右端 1 2 3 + 4 × + 5 - s1中剩余的运算符

因此结果为

"1 2 3 + 4 × + 5 –"

  • 代码实现如下:
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.因为直接对str 进行操作,不方便,因此 先将 "1+((2+3)×4)-5" =》 中缀的表达式对应的List
		// 即 "1+((2+3)×4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]

		String exception = "1+((2+3)*4)-5";
		List<String> inList = toInList(exception);
		System.out.println("中缀表达式对应的List = " + inList);
		System.out.println("+++++++++++++++++++++++++++");

		// 3.将得到的中缀表达式对应的List => 后缀表达式对应的List
		// 即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList
		// [1,2,3,+,4,*,+,5,–]
		List<String> parList = parList(inList);
		System.out.println("后缀表达式对应的List = " + parList);
		
		System.out.printf("expression=%d", calculate(parList));

	}

	// 即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
	// 方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
	public static List<String> parList(List<String> ls) {
		// 定义两个栈
		Stack<String> s1 = new Stack<String>(); // 符号栈
		// 说明:因为s2 这个栈,在整个转换过程中,没有pop操作,而且后面还需要逆序输出
		// 因此比较麻烦,这里就不用 Stack 直接使用 List s2
		// Stack s2 = new Stack(); // 储存中间结果的栈s2
		List<String> s2 = new ArrayList<String>(); // 储存中间结果的Lists2

		// 遍历ls
		for (String item : ls) {
			// 如果是一个数,加入s2
			if (item.matches("\\d+")) {
				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();// 将 ( 弹出 s1栈,,消除小括号
			} else {
				// 当item的优先级小于等于s1栈顶运算符,
				// 将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
				// 问题:我们缺少一个比较优先级高低的方法
				while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
					s2.add(s1.pop());
				}
				// 还需要将item压入栈
				s1.push(item);
			}
		}
		// 将s1中剩余的运算符依次弹出并加入s2
		while (s1.size() != 0) {
			s2.add(s1.pop());
		}
		return s2; // 注意因为是存放到List, 因此按顺序输出就是对应的后缀表达式对应的List
	}

	// 自定义方法:将中缀表达式转成对应的List
	public static List<String> toInList(String s) {
		// 定义一个List,存放中缀表达式对应的内容
		List<String> ls = new ArrayList<String>();
		int i = 0; // 这是一个指针,用于遍历中缀表达式字符串
		String str; // 多位数拼接
		char c; // 每遍历一个字符,就存入c
		do {
			// 如果是非数字,需要加入ls
			if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
				ls.add("" + c);
				i++; // i后移
			} else { // 如果是一个数,需要考虑多位数
				str = ""; // 将str 制成 ""
				while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
					str += c; // 拼接
					i++; // 后移
				}
				ls.add(str);
			}
		} while (i < s.length());
		return ls;
	}

	// 将一个逆波兰表达式,一次将数据和运算符放入ArrayList中
	public static List<String> getListString(String suffiException) {
		// 将suffiException分割
		String[] split = suffiException.split(" ");
		List<String> list = new ArrayList<String>();
		for (String ele : split) {
			list.add(ele);
		}
		return list;
	}

	// 完成对逆波兰表达式的计算
	/*
	 * 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,由此得出最终结果
	 */
	public static int calculate(List<String> ls) {
		// 创建一个栈
		Stack<String> stack = new Stack<String>();
		// 遍历
		for (String item : ls) {
			// 使用正则表达式取值
			if (item.matches("\\d+")) { // 匹配多位数的正则表达式
				// 入栈
				stack.push(item);
			} else {
				// pop出两个数,并运算,再入栈
				int num2 = Integer.parseInt(stack.pop()); // 将字符串转成整数
				int num = Integer.parseInt(stack.pop());
				int res = 0; // 存放判断结果
				if (item.equals("+")) {
					res = num + num2;
				} else if (item.equals("-")) {
					res = num - num2;
				} else if (item.equals("*")) {
					res = num * num2;
				} else if (item.equals("/")) {
					res = num / num2;
				} else {
					throw new RuntimeException("运算符有问题!!!");
				}
				// 将res入栈
				stack.push(res + ""); // 整数转字符串
			}
		}
		// 最后留在stack中的数据即为结果
		return Integer.parseInt(stack.pop()); // 字符串转整数
	}
}

// 编写一个类 Operation 可以返回一个运算符 对应的优先级
class Operation {
	private static int ADD = 1;
	private static int SUB = 1;
	private static int MUL = 2;
	private static int DIV = 2;

	// 写一个方法,返回对应的优先级数字
	public static int getValue(String operation) {
		int result = 0;
		switch (operation) {
		case "+":
			result = ADD;
			break;
		case "-":
			result = SUB;
			break;
		case "*":
			result = MUL;
			break;
		case "/":
			result = DIV;
			break;
		default:
			System.out.println("不存在该运算符:" + operation);
			break;
		}
		return result;
	}
}

完整版逆波兰计算器

完整版的逆波兰计算器,功能包括

  1. 支持 + - * / ( )
  2. 多位数,支持小数,
  3. 兼容处理, 过滤任何空白字符,包括空格、制表符、换页符

注:逆波兰计算器完整版考虑的因素较多,下面给出完整版代码供参考学习,其基本思路和前面一样,也是使用到:中缀表达式转后缀表达式

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

public class ReversePolishMultiCalc {

	/**
	 * 匹配 + - * / ( ) 运算符
	 */
	static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

	static final String LEFT = "(";
	static final String RIGHT = ")";
	static final String ADD = "+";
	static final String MINUS = "-";
	static final String TIMES = "*";
	static final String DIVISION = "/";

	/**
	 * 加減 + -
	 */
	static final int LEVEL_01 = 1;
	/**
	 * 乘除 * /
	 */
	static final int LEVEL_02 = 2;

	/**
	 * 括号
	 */
	static final int LEVEL_HIGH = Integer.MAX_VALUE;

	static Stack<String> stack = new Stack<>();
	static List<String> data = Collections.synchronizedList(new ArrayList<String>());

	/**
	  * 
	  * @Description 去除所有空白符
	  */
	public static String replaceAllBlank(String s) {
		// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
		return s.replaceAll("\\s+", "");
	}

	/**
	  * 
	  * @Description 判断是不是数字 int double long float
	  */
	public static boolean isNumber(String s) {
		Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
		return pattern.matcher(s).matches();
	}

	/**
	  * 
	  * @Description 判断是不是运算符
	  */
	public static boolean isSymbol(String s) {
		return s.matches(SYMBOL);
	}

	/**
	  *
	  * @Description 匹配运算等级
	  */
	public static int calcLevel(String s) {
		if ("+".equals(s) || "-".equals(s)) {
			return LEVEL_01;
		} else if ("*".equals(s) || "/".equals(s)) {
			return LEVEL_02;
		}
		return LEVEL_HIGH;
	}

	/**
	 * 匹配
	 */
	public static List<String> doMatch(String s) throws Exception {
		if (s == null || "".equals(s.trim()))
			throw new RuntimeException("data is empty");
		if (!isNumber(s.charAt(0) + ""))
			throw new RuntimeException("data illeagle,start not with a number");

		s = replaceAllBlank(s);

		String each;
		int start = 0;

		for (int i = 0; i < s.length(); i++) {
			if (isSymbol(s.charAt(i) + "")) {
				each = s.charAt(i) + "";
				// 栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
				if (stack.isEmpty() || LEFT.equals(each)
						|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)) {
					stack.push(each);
				} else if (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
					// 栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
					while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
						if (calcLevel(stack.peek()) == LEVEL_HIGH) {
							break;
						}
						data.add(stack.pop());
					}
					stack.push(each);
				} else if (RIGHT.equals(each)) {
					// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
					while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())) {
						if (LEVEL_HIGH == calcLevel(stack.peek())) {
							stack.pop();
							break;
						}
						data.add(stack.pop());
					}
				}
				start = i; // 前一个运算符的位置
			} else if (i == s.length() - 1 || isSymbol(s.charAt(i + 1) + "")) {
				each = start == 0 ? s.substring(start, i + 1) : s.substring(start + 1, i + 1);
				if (isNumber(each)) {
					data.add(each);
					continue;
				}
				throw new RuntimeException("data not match number");
			}
		}
		// 如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
		Collections.reverse(stack);
		data.addAll(new ArrayList<>(stack));

		System.out.println(data);
		return data;
	}

	/**
	 * 算出结果
	 */
	public static Double doCalc(List<String> list) {
		Double d = 0d;
		if (list == null || list.isEmpty()) {
			return null;
		}
		if (list.size() == 1) {
			System.out.println(list);
			d = Double.valueOf(list.get(0));
			return d;
		}
		ArrayList<String> list1 = new ArrayList<>();
		for (int i = 0; i < list.size(); i++) {
			list1.add(list.get(i));
			if (isSymbol(list.get(i))) {
				Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
				list1.remove(i);
				list1.remove(i - 1);
				list1.set(i - 2, d1 + "");
				list1.addAll(list.subList(i + 1, list.size()));
				break;
			}
		}
		doCalc(list1);
		return d;
	}

	/**
	  * 
	  * @Description 运算
	  */
	public static Double doTheMath(String s1, String s2, String symbol) {
		Double result;
		switch (symbol) {
		case ADD:
			result = Double.valueOf(s1) + Double.valueOf(s2);
			break;
		case MINUS:
			result = Double.valueOf(s1) - Double.valueOf(s2);
			break;
		case TIMES:
			result = Double.valueOf(s1) * Double.valueOf(s2);
			break;
		case DIVISION:
			result = Double.valueOf(s1) / Double.valueOf(s2);
			break;
		default:
			result = null;
		}
		return result;
	}

	public static void main(String[] args) {
		// String math = "9+(3-1)*3+10/2";
		String math = "12.8 + (2 - 3.55)*4+10/5.0";
		try {
			doCalc(doMatch(math));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

本章导图总结

Java数据结构与算法 day03 栈_第25张图片

你可能感兴趣的:(数据结构与算法(Java版))