4.1 栈的介绍
栈(英语:stack)又称为堆栈或堆叠,是计算机科学中的一种抽象数据类型,只允许在有序的线性数据集合的一端(称为堆栈顶端,英语:top)进行加入数据(英语:push)和移除数据(英语:pop)的运算,因而按照先入后出(FILO-First In Last Out)的原理运作。常与另一种有序的线性数据集合队列相提并论,栈常用一维数组或链表来实现。
4.2 栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换:[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
4.3 栈的数组实现
因为栈是一种有序的线性列表,可以使用数组的结构来模拟栈的使用。
思路分析:
- 首先定义一个数组 stack 来模拟栈。
- 定义栈顶指针 top,初始化为 -1。
- 入栈操作,当有数据压入到栈时,stack[++top] = data。
- 出栈操作,当有数据出栈时,value = stack[top–]。
public class ArrayStackTest {
public static void main(String[] args) {
//创建一个 ArrayStack 对象表示栈
ArrayStack stack = new ArrayStack(4);
//控制是否退出菜单
boolean loop = true;
String key = "";
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) {
// TODO: handle exception
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;
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 IllegalArgumentException("栈已空,无法取出数据");
}
return stack[top--];
}
public void list() {
if (isEmpty()) {
System.out.println("栈已空,无法取出数据");
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d] = %d \n", i, stack[i]);
}
}
}
4.4 栈实现综合计算器
中缀表达式
与前缀表达式(例:+ 3 4)或后缀表达式(例:3 4 +)相比,中缀表达式不容易被电脑解析,但仍被许多程序语言使用,因为它符合人们的普遍用法。与前缀或后缀记法不同的是,中缀记法中括号是必需的,计算过程中必须用括号将操作符和对应的操作数括起来,用于指示运算的次序。
思路分析:
- 定义 index 值(索引)来遍历表达式。
- 如果对应的值是一个数字,则直接压入数字栈。
- 如果对应的值是一个符号,则根据如下规则入符号栈。
- 如果当前符号栈为空,或者当前操作符的优先级大于栈中的操作符,则直接压入符号栈。
- 如果当前操作符的优先级小于或等于栈中的操作符,则需要从数字栈中 pop 出两个数据和符号栈 pop 出一个符号进行运算,然后将计算结果压入数字栈,再将当前操作符压入符号栈。
- 当表达式扫描完毕,就顺序的从数字栈和符号栈中 pop 出响应的数字和符号进行运算。
- 最后在数字栈中只存在一个数字,就是表达式的结果。
代码实现:
package com.thundersoft.datastructure.stack;
public class Calculator {
public static void main(String[] args) {
// 表达式
String expression = "7*2*2-5+1-5+3-4";
// 定义 index
int index = 0;
// 用于拼接多位数
String keepNum = "";
// 创建数字栈
ArrayStack numberStack = new ArrayStack(10);
// 创建符号栈
ArrayStack operStack = new ArrayStack(10);
// 将所有字符入栈
while (true) {
// 获取表达式中的每个字符
char item = expression.substring(index, index + 1).charAt(0);
// 判断当前字符是否为符号
if (isOper(item)) {
// 如果当前符号栈为空
if (operStack.isEmpty()) {
operStack.push(item);
} else {
// 当前操作符的优先级小于或等于栈中的操作符
if (convertOper(item) <= convertOper(operStack.last())) {
// 从数字栈中 pop 出两个数据和符号栈 pop 出一个符号进行运算
int number1 = numberStack.pop();
int number2 = numberStack.pop();
int oper = operStack.pop();
// 进行运算
int result = cal(number1, number2, oper);
// 将计算结果压入数字栈
numberStack.push(result);
// 将当前操作符号要入符号栈
operStack.push(item);
} else {
// 当前操作符的优先级大于栈中的操作符,则直接压入符号栈
operStack.push(item);
}
}
} else {
// 判断当前字符是否为数字
keepNum += item;
//如果 item 已经是 expression 的最后一位,就直接入栈
if (index == expression.length() - 1) {
numberStack.push(Integer.parseInt(keepNum));
} else {
// 判断下一个字符是不是数字,如果是数字就继续扫描,如果是运算符则入栈
if (isOper(expression.substring(index + 1, index + 2).charAt(0))) {
numberStack.push(Integer.parseInt(keepNum));
// 将暂存值清空
keepNum = "";
}
}
}
// 判断是否扫描到 expression 最后
index++;
if (index >= expression.length()) {
break;
}
}
// 当表达式扫描完毕,就顺序的从数栈和符号栈中 pop 出相应的数和符号进行计算
while (true) {
if (operStack.isEmpty()) {
break;
}
int number1 = numberStack.pop();
int number2 = numberStack.pop();
int oper = operStack.pop();
// 进行运算
int result = cal(number1, number2, oper);
// 将计算结果压入数字栈
numberStack.push(result);
}
System.out.printf("表达式 %s = %d \n", expression, numberStack.pop());
}
private static boolean isOper(char item) {
return item == '+' || item == '-' || item == '*' || item == '/';
}
private static int convertOper(int item) {
if (item == '*' || item == '/') {
return 1;
} else if (item == '+' || item == '-') {
return 0;
} else {
return -1;
}
}
/**
* 计算结果
*
* @param number1
* @param number2
* @param oper
* @return
*/
public static int cal(int number1, int number2, int oper) {
// res 用于存放计算的结果
int result = 0;
switch (oper) {
case '+':
result = number1 + number2;
break;
case '-':
result = number2 - number1;
break;
case '*':
result = number1 * number2;
break;
case '/':
result = number2 / number1;
break;
default:
break;
}
return result;
}
}
4.5 栈实现逆波兰计算器
后缀表达式
后缀表达式,又称逆波兰式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则),例如常规中缀记法的 “3 - 4 + 5” 在逆波兰记法中写作 “3 4 - 5 +” 表示:先3减去4,再加上5。
思路分析:
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 * 6 -,针对后缀表达式求值步骤如下:
- 从左至右扫描,将 3 和 4 压入堆栈。
- 遇到 + 运算符弹出 4 和 3,计算出 3+4 的值,然后将结果 7 压入栈。
- 继续扫描将 5 入栈。
- 遇到 × 运算符弹出 5 和 7,计算出 7×5 的值,然后将结果 35 压入栈。
- 继续扫描将 6 入栈。
- 最后 - 运算符,计算出 35-6 的值,得出最终结果 29。
代码实现:
public class PolandNotation {
public static void main(String[] args) {
String suffixExpression = "3 4 + 5 * 6 -";
String[] split = suffixExpression.split(" ");
List<String> list = Arrays.asList(split);
System.out.println(list);
int result = calculateList(list);
System.out.println("计算的结果是:" + result);
}
private static int calculateList(List<String> list) {
// 创建栈结构
Stack<String> stack = new Stack<>();
for (String item : list) {
// 使用正则表达式去除整数
if (item.matches("\\d+")) {
stack.push(item);
} else {
String number1 = stack.pop();
String number2 = stack.pop();
int result = calculate(number1, number2, item);
stack.push(result + "");
}
}
// 最后留在 stack 中的数据是运算结果
return Integer.parseInt(stack.pop());
}
private static int calculate(String number1, String number2, String oper) {
int result;
int num1 = Integer.parseInt(number1);
int num2 = Integer.parseInt(number2);
if (oper.equals("+")) {
result = num1 + num2;
} else if (oper.equals("-")) {
result = num2 - num1;
} else if (oper.equals("*")) {
result = num1 * num2;
} else if (oper.equals("/")) {
result = num2 / num1;
} else {
throw new RuntimeException("运算符有误");
}
return result;
}
}
4.6 中缀表达式转换为后缀表达式
后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将中缀表达式转成后缀表达式。
思路分析:
- 1.初始化两个栈,运算符栈operStack和存储中间结果的栈numberStack。
- 2.从左至右扫描整个中缀表达式。
- 3.当遇到操作数时,将操作数压入到 numberStack。
- 4.当遇到运算符时,比较其与 operStack 栈顶运算符的优先级。
- 4.1如果 operStack 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈。
- 4.2.若当前运算符的优先级比栈顶运算符的高,也将运算符压入 operStack。
- 4.3若不满足上述条件,则将 operStack 栈顶的运算符弹出并压入到 numberStack 中,再次转到 4.1 步骤中与 operStack 中新的栈顶运算符相比较。
- 5.遇到括号时,如果是左括号 “(” 时,直接压入 operStack 栈中。
- 6.遇到括号时,如果是右括号 “)” 时,则依次弹出 operStack 栈顶的运算符并压入 numberStack 栈中,直到遇到左括号为止,此时将这对括号丢弃。
- 7.重复执行步骤 2 至 6,直到表达式的最右边。
- 8.将 operStack 栈中剩余的运算符依次弹出并压入 numberStack。
- 9.依次弹出 numberStack 栈中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。
代码实现
public class InfixToSufixExpressionTest {
public static void main(String[] args) {
String param = "122+((2+3)*4)-5";
List<String> list = toInfixExpressionList(param);
List<String> expressionList = toSufixExpressionList(list);
System.err.println(expressionList);
}
/**
* 中缀表达式转后缀表达式
*
* @param infixs
* @return
*/
private static List<String> toSufixExpressionList(List<String> infixs) {
List<String> resultList = new ArrayList<>();
// 初始化两个栈,运算符栈operStack和存储中间结果的栈numberStack。
Stack<String> operStack = new Stack<>();
Stack<String> numberStack = new Stack<>();
// 从左至右扫描整个中缀表达式。
for (String item : infixs) {
if (item.matches("\\d+")) {
// 当遇到操作数时,将操作数压入到 numberStack。
numberStack.push(item);
} else if (item.equals("(")) {
// 如果 operStack 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈。
operStack.push(item);
} else if (item.equals(")")) {
// 如果是右括号 “)” 时,则依次弹出 operStack 栈顶的运算符并压入 numberStack 栈中,直到遇到左括号为止
while (!operStack.peek().equals("(")) {
numberStack.push(operStack.pop());
}
// 此时将这对括号丢弃
operStack.pop();
} else {
// 若当前运算符的优先级比栈顶运算符的小或者相等
while (!operStack.isEmpty() && operationPriority(operStack.peek(), item)) {
// 将 operStack 栈顶的运算符弹出并压入到 numberStack 中,再次转到 4.1 步骤中与 operStack 中新的栈顶运算符相比较。
numberStack.push(operStack.pop());
}
operStack.push(item);
}
}
// 将 operStack 栈中剩余的运算符依次弹出并压入 numberStack。
while (!operStack.isEmpty()) {
numberStack.push(operStack.pop());
}
// 逆序
while (!numberStack.isEmpty()) {
resultList.add(numberStack.pop());
}
Collections.reverse(resultList);
return resultList;
}
/**
* 中缀表达式转集合
*
* @param param
* @return
*/
private static List<String> toInfixExpressionList(String param) {
ArrayList<String> list = new ArrayList<>();
int index = 0;
String keepNum;
while (index < param.length()) {
char ch = param.charAt(index);
if (isNotNumber(ch)) {
list.add(ch + "");
index++;
} else {
keepNum = "";
while (index < param.length() && isNumber(param.charAt(index))) {
keepNum += param.charAt(index);
index++;
}
list.add(keepNum);
}
}
return list;
}
private static boolean isNumber(char param) {
return param >= 48 && param <= 57;
}
private static boolean isNotNumber(char param) {
return param < 48 || param > 57;
}
/**
* 比较运算符的优先级
*
* @param operation1
* @param operation2
* @return
*/
private static boolean operationPriority(String operation1, String operation2) {
return operationValue(operation1) >= operationValue(operation2);
}
/**
* 获取运算符优先级的值
*
* @param operation
* @return
*/
private static int operationValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = 1;
break;
case "-":
result = 1;
break;
case "*":
result = 2;
break;
case "/":
result = 2;
break;
default:
result = 0;
break;
}
return result;
}
}