1、栈的基本介绍
1.1、栈的实际需求
- 请计算表达式:[722-5+1-5+3-3] 的值
- 请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5,但是计算机怎么理解这个算式的
- 对计算机而言, 它接收到的就是一个字符串, 我们讨论的是这个问题:栈
1.2、栈的基本性质
1)栈的英文为(stack)
2)栈是一个先入后出(FILO-First In Last Out)的有序列表
3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。 允许插入和删除的一端, 为变化的一端, 称为栈顶(Top), 另一端为固定的一端, 称为栈底(Bottom)。
4)根据栈的定义可知, 最先放入栈中元素在栈底, 最后放入的元素在栈顶, 而删除元素刚好相反, 最后放入的元素最先删除, 最先放入的元素最后删除
图解方式说明出栈(pop)和入栈(push)的概念
1.3、栈的应用场景
子程序的调用: 在跳往子程序前, 会先将下个指令的地址存到堆栈中, 直到子程序执行完后再将地址取出, 以回到原来的程序中。
处理递归调用: 和子程序的调用类似, 只是除了储存下一个指令的地址外, 也将参数、 区域变量等数据存入栈中。
表达式的转换:[中缀表达式转后缀表达式]与求值(实际解决)。
二叉树的遍历。
图形的深度优先(depth 一 first)搜索法。
2、数组模拟栈
2.1、代码思路
maxSize :栈的大小(数组的大小)
arr :用来模拟栈的数组
top :指向当前栈顶元素,初始值为 -1 ,表示栈空
判断栈满:top == maxSize ,即已经到达数组最后一个位置
判断栈空:top == -1
入栈:arr[++top] = arr;
出栈:return arr[top–] ;
2.2、代码实现
- 栈的定义
//定义一个 ArrayStack 表示栈 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; } // 入栈-push 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 static void main(String[] args) { // 测试一下ArrayStack 是否正确 // 先创建一个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(); 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("程序退出~~~"); }
- 程序运行结果
show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 push 请输入一个数 1 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 push 请输入一个数 2 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 push 请输入一个数 3 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 push 请输入一个数 4 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 push 请输入一个数 5 栈满 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 show stack[3]=4 stack[2]=3 stack[1]=2 stack[0]=1 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 pop 出栈的数据是 4 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 pop 出栈的数据是 3 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 pop 出栈的数据是 2 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 pop 出栈的数据是 1 show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择 pop 栈空,没有数据~ show: 表示显示栈 exit: 退出程序 push: 表示添加数据到栈(入栈) pop: 表示从栈取出数据(出栈) 请输入你的选择
2.3、数组模拟栈全部代码
public class ArrayStackDemo { public static void main(String[] args) { // 测试一下ArrayStack 是否正确 // 先创建一个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(); 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("程序退出~~~"); } } //定义一个 ArrayStack 表示栈 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; } // 入栈-push 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]); } } }
2.4、课后练习
- 使用链表模拟栈
3、栈实现综合计算器(中缀表达式)
3.1、代码思路
- 栈分为两个栈:
- 数栈(numStack):存储表达式中的数字
- 符号栈(operStack):存储表达式中的符号
- 扫描表达式(这里并没有考虑括号):
- 对于数:扫描到数,则直接压入数栈
- 对于运算符:扫描到运算符,分为如下几种情况:
- 如果符号栈为空,则直接入栈
- 如果符号栈不为空:
- 如果当前扫描到的运算符的优先级 <= 符号栈栈顶的运算符的优先级,说明上次的运算符优先级较高,先执行优先级高的运算
- 从数栈中弹出两个数,根据符号栈栈顶的运算符进行运算(优先级高,就先算出来)
- 然后将计算的结果压入栈中
- 再将当前运算符压入符号栈
- 如果当前扫描到的运算符的优先级 > 符号栈栈顶的运算符的优先级,说明上次的运算符优先级较低,直接压入符号栈
- 何时停止循环?
- 处理完表达式,退出循环
- 即表达式下标(index)的值大于表达式(expression)的长度
- 代码:index >= expression.length()
- 表达式扫描完成:
- 此时符号栈中的运算符优先级都相同
- 从数栈中弹出两个数,再从符号栈中弹出一个运算符,进行运算,计算结果放回数栈中
- 何时停止循环?符号栈为空则停止:operStack.isEmpty()
- 表达式的值?符号栈为空时,数栈栈顶还有一个元素,这个元素的值就是表达式的值
举例:3+2*6-2
- 首先
- 将 3 压入数栈
- 将 + 压入符号栈
- 将 2 压入数栈
2
3 +
数栈 符号栈
- 由于 * 优先级大于 + ,所以将 * 压入 符号栈,然后将 6 压入数栈
6
2 *
3 +
数栈 符号栈
- 由于 - 优先级低于 * ,所以从数栈中弹出两个数(6 和 2),从符号栈中弹出一个运算符(*),进行运算,运算结果再压入数栈,然后将 - 压入符号栈
12 -
3 +
数栈 符号栈
- 将 2 压入数栈,表达式处理完毕
2
12 -
3 +
数栈 符号栈
- 重复此过程,直至符号栈为空:从数栈中弹出两个数,再从符号栈中弹出一个运算符,进行运算,计算结果放回数栈中
10
3 +
数栈 符号栈
13
数栈 符号栈
3.2、代码实现
- 栈的定义:专为计算器而生的栈
- 对于乘除法特别说明:由于栈先进后出的特点,num1 是运算符后面的数(减数、除数),num2 是运算符前的数(被减数、被除数),特别需要注意减法与除法的顺序
- res = num2 - num1;
- res = num2 / num1;
//先创建一个栈,直接使用前面创建好 //定义一个 CalcStack 表示栈, 需要扩展功能 class CalcStack { private int maxSize; // 栈的大小 private int[] stack; // 数组,数组模拟栈,数据就放在该数组 private int top = -1;// top表示栈顶,初始化为-1 // 构造器 public CalcStack(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; } // 入栈-push 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 num1, int num2, int oper) { int res = 0; // res 用于存放计算的结果 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; } }
- 综合计算器代码:对多位数进行了判断,使得程序可处理多位数的运算
public static void main(String[] args) { // 根据前面老师思路,完成表达式的运算 String expression = "7*2*2-5+1-5+3-4"; // 如何处理多位数的问题? // 创建两个栈,一个数栈,一个符号栈 CalcStack numStack = new CalcStack(10); CalcStack operStack = new CalcStack(10); // 定义需要的相关变量 int index = 0;// 用于扫描 int num1 = 0; int num2 = 0; int oper = 0; int res = 0; char ch = ' '; // 将每次扫描得到char保存到ch String keepNum = ""; // 用于拼接 多位数 // 开始while循环的扫描expression while (true) { // 依次得到expression 的每一个字符 ch = expression.substring(index, index + 1).charAt(0); // 判断ch是什么,然后做相应的处理 if (operStack.isOper(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); // 1 + 3 } } else { // 如果是数,则直接入数栈 // numStack.push(ch - 48); //? "1+3" '1' => 1 // 分析思路 // 1. 当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数 // 2. 在处理数,需要向expression的表达式的index 后再看一位,如果是数就进行扫描,如果是符号才入栈 // 3. 因此我们需要定义一个变量 字符串,用于拼接 // 处理多位数 keepNum += ch; // 如果ch已经是expression的最后一位,就直接入栈 if (index == expression.length() - 1) { numStack.push(Integer.parseInt(keepNum)); } else { // 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈 // 注意是看后一位,不是index++ if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) { // 如果后一位是运算符,则入栈 keepNum = "1" 或者 "123" numStack.push(Integer.parseInt(keepNum)); // 重要的!!!!!!, keepNum清空 keepNum = ""; } } } // 让index + 1, 并判断是否扫描到expression最后. 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出,就是结果 res = numStack.pop(); System.out.printf("表达式 %s = %d", expression, res); }
- 程序运行结果
表达式 7*2*2-5+1-5+3-4 = 18
3.3、综合计算器全部代码
public class Calculator { public static void main(String[] args) { // 根据前面老师思路,完成表达式的运算 String expression = "7*2*2-5+1-5+3-4"; // 如何处理多位数的问题? // 创建两个栈,一个数栈,一个符号栈 CalcStack numStack = new CalcStack(10); CalcStack operStack = new CalcStack(10); // 定义需要的相关变量 int index = 0;// 用于扫描 int num1 = 0; int num2 = 0; int oper = 0; int res = 0; char ch = ' '; // 将每次扫描得到char保存到ch String keepNum = ""; // 用于拼接 多位数 // 开始while循环的扫描expression while (true) { // 依次得到expression 的每一个字符 ch = expression.substring(index, index + 1).charAt(0); // 判断ch是什么,然后做相应的处理 if (operStack.isOper(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); // 1 + 3 } } else { // 如果是数,则直接入数栈 // numStack.push(ch - 48); //? "1+3" '1' => 1 // 分析思路 // 1. 当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数 // 2. 在处理数,需要向expression的表达式的index 后再看一位,如果是数就进行扫描,如果是符号才入栈 // 3. 因此我们需要定义一个变量 字符串,用于拼接 // 处理多位数 keepNum += ch; // 如果ch已经是expression的最后一位,就直接入栈 if (index == expression.length() - 1) { numStack.push(Integer.parseInt(keepNum)); } else { // 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈 // 注意是看后一位,不是index++ if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) { // 如果后一位是运算符,则入栈 keepNum = "1" 或者 "123" numStack.push(Integer.parseInt(keepNum)); // 重要的!!!!!!, keepNum清空 keepNum = ""; } } } // 让index + 1, 并判断是否扫描到expression最后. 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出,就是结果 res = numStack.pop(); System.out.printf("表达式 %s = %d", expression, res); } } //先创建一个栈,直接使用前面创建好 //定义一个 CalcStack 表示栈, 需要扩展功能 class CalcStack { private int maxSize; // 栈的大小 private int[] stack; // 数组,数组模拟栈,数据就放在该数组 private int top = -1;// top表示栈顶,初始化为-1 // 构造器 public CalcStack(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; } // 入栈-push 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 num1, int num2, int oper) { int res = 0; // res 用于存放计算的结果 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、课后练习
- 加入小括号的判断:小括号内的运算需要优先执行,也即小括号的优先级最高
- 我的代码思路如下:
- 但凡遇到左括号,直接入符号栈,之后的操作和之前的一样
- 直至遇到右括号,此时可以依次将符号栈中的符号弹出,与数栈中的数做运算
- 直至符号栈弹出左括号为止,此时,括号内的运算已执行完毕