栈(stack) 先进后出
- 栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈(PUSH),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈(POP),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
- 栈的应用场景:
(1)子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
(2)处理递归调用:和子程序的调用类似,除了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。
(3)表达式的转换(中缀转后缀)与求值
(4)二叉树的遍历
(5)图形的深度优先搜索
1. 实现栈
思路分析:
- 数组模拟栈 stack
- 定义一个top来表示栈顶(相当于有个指针,进栈指针上移,出栈指针下移),初始化为-1
- 入栈操作,当有数据加入到栈时,top++ stack[top]= data
- 出栈操作,设置一个临时变量把栈顶元素拿到,int value = stack[top] top-- return value;
package 栈;
import java.util.Scanner;
public class Moni {
public static void main(String[] args) {
//创建一个ArrayStack类对象 表示栈
ArrayStack arrayStack = new ArrayStack(4);
String string = "";
boolean decide = true;//控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while(decide) {
System.out.println("show: 表示显示栈");
System.out.println("exit: 表示退出程序");
System.out.println("push: 表示入栈");
System.out.println("pop: 表示出栈");
System.out.println("请输入你的选择:");
//用key来接收用户输入的字符串
string = scanner.next();
switch (string) {
case "show": {
arrayStack.list();
break;
}
case "push": {
System.out.println("请输入一个数,放进栈");
int value = scanner.nextInt();
arrayStack.push(value);
break;
}
case "pop": {
//因为出栈有可能会有一个异常抛出,因此用try catch语句进行处理
try {
int result = arrayStack.pop();
System.out.printf("出栈的数据是%d\n",result);
} catch (Exception e) {
System.err.println(e.getMessage());
}
break;
}
case "exit": {
scanner.close();//因为scanner是个文件流
decide=false;
System.out.println("程序退出了");
break;
}
default:
break;
}
}
}
}
//定义一个ArrayStack类 表示栈
class ArrayStack{
private int maxSize;//栈的大小
private int[] stack;//数组 数组模拟栈 数据放在其中
private int top = -1;//栈顶 ,为-1表示栈空
//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];//数组初始化
}
//栈满
public boolean isFull() {
return top == maxSize-1; //进栈时 top从0开始
}
//栈空
public boolean isEmpty() {
return top == -1;
}
//入栈
public void push(int data) {
//先判断栈是否已经满
if(isFull()) {
System.out.println("栈已经满了");
return;
}
top++;
stack[top]=data;
}
//出栈 将栈顶的数据返回
public int pop() {
//先判断栈是否已经空
if(isEmpty()) {
//直接抛出一个 运行时异常 ,此异常非必须捕获 其后不用return
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]);//此处用的printf 前面是String的格式化
}
}
}
2. 栈实现中缀表达式计算器(了解)
思路分析:
建立两个栈,分别是数栈(存放数)和符号栈(存放操作符)
- 通过一个index值(索引),来遍历表达式
- 若扫描到一个数字就入数栈,
- 若扫描到一个运算符就入符号栈:若当前的符合栈为空就直接入栈;若当前的符合栈不为空,要进行比较,(1)如果当前的操作符的优先级小于或者等于栈中的栈顶的操作符,就需要从数栈中pop出两个数,再从符号栈中pop出一个运算符,进行运算,将运算结果入数栈,然后将当前的操作符入符号栈;(2)如果当前的操作符的优先级 大于栈中的栈顶的操作符,就直接入符号栈。
- 当表达式扫描完毕,就顺序地从数栈和符号栈中pop出相应的数和符号,并运算
- 最后,数栈只有一个数字,就是此表达式的结果。
//举例:3+2*6-2
//依次扫描表达式: 3入数栈, +入符号栈 2入数栈 *因为比栈中的-的优先级高,入符号栈 6入数栈
// -比栈顶的*优先级低,从数栈pop出两个数6 和2,从符号栈中pop出一个*,进行运算,6*2=12, 12入数栈,将当前的操作符- 入符号栈
//继续扫描2入数栈;
//扫描完毕 此时数栈 2 12 3 符号栈:- +
//之后顺序地从两个栈中pop, 先pop出 2 和 12,再pop出 -, 是后面弹出的数减去前面弹出的数 12-2=10
// 先pop出 10 和 3,再pop出 + 3+10=13 栈顶指向13
代码实现:(了解即可)
package 栈;
public class JiSuanQi {
public static void main(String[] args) {
String string = "70+2*6-2";
// 先创建数栈和符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
// 用于扫描的索引
int index = 0;
// 在弹数时需要两个变量保存
int num1 = 0;
int num2 = 0;
// 在弹符号时
int oper = 0;
// 结果
int result = 0;
// 将每次扫描得到的char保存到 ch中
char ch = ' ';
//用字符串变量用于拼接 多位数
String keepNumString = "";
// 开始循环扫描表达式
while (true) {
// 先依次得到表达式中的每一个字符, 就是从表达式字符串中取出各个字符 String.substring(起始下标,结束下标) 截取到前闭后开
ch = string.substring(index, index + 1).charAt(0);
// 判断取出的字符是数字 或者 运算符
if (operStack.isOper(ch)) {// 是运算符
// 判断当前的符号栈是否为空
if (!operStack.isEmpty()) {// 不为空
// 如果当前的操作符的优先级小于或者等于栈中的栈顶的操作符
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
// 需要从数栈中pop出两个数,,
num1 = numStack.pop();
num2 = numStack.pop();
// 再从符号栈中pop出一个运算符
oper = operStack.pop();
// 进行运算,将运算结果入数栈,
result = numStack.caculate(num1, num2, oper);
numStack.push(result);
// 然后将当前的操作符入符号栈
operStack.push(ch);
} else {// 如果当前的操作符的优先级 大于栈中的栈顶的操作符,就直接入符号栈。
operStack.push(ch);
}
} else {// 若当前的符合栈为空就直接入栈
operStack.push(ch);
}
} else {// 若是数字,直接入数栈
// 因为字符1 对应十进制值 是49 为了数值准确 用ch-48
// numStack.push(ch - 48);
//上式错误,因为当是多位数时,比如25,直接将2和5拆开了
//所以,处理数时,需要向string 的表达式的index后再看一位如果是数,需要拼接 如果是运算符,可以直接入栈
keepNumString += ch;
//如果 ch 已经是表达式最后一个字符
if(index == string.length()-1) {
numStack.push(Integer.parseInt(keepNumString));
}
else {
//判断下一个字符是运算符
if(operStack.isOper(string.substring(index+1,index+2).charAt(0))) {
numStack.push(Integer.parseInt(keepNumString));
//最重要 keepNumString要清空 否则会一直在此基础上拼接
keepNumString = "";
}
}
}
//让index+1 ,并判断是否扫描到这个表达式的最后
index++;
if(index >= string.length()) {//扫描结束
break;
}
}
//当表达式扫描完毕,就顺序地从数栈和符号栈中pop出相应的数和符号,并运算
while(true){
//如果符号栈为空,则计算到最后的结果,数栈中只有一个数字结果
if(operStack.isEmpty()) {
break;
}
else {
// 需要从数栈中pop出两个数,,
num1 = numStack.pop();
num2 = numStack.pop();
// 再从符号栈中pop出一个运算符
oper = operStack.pop();
// 进行运算,将运算结果入数栈,
result = numStack.caculate(num1, num2, oper);
numStack.push(result);
}
}
//将数栈的结果pop出来
System.out.printf("表达式%s = %d",string,numStack.pop());
}
//表达式70+2*6-2 = 80
}
//先创建一个栈
//定义一个ArrayStack2类 表示栈
class ArrayStack2 {
private int maxSize;// 栈的大小
private int[] stack;// 数组 数组模拟栈 数据放在其中
private int top = -1;// 栈顶 ,为-1表示栈空
// 构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];// 数组初始化
}
// 栈满
public boolean isFull() {
return top == maxSize - 1; // 进栈时 top从0开始
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int data) {
// 先判断栈是否已经满
if (isFull()) {
System.out.println("栈已经满了");
return;
}
top++;
stack[top] = data;
}
// 出栈 将栈顶的数据返回
public int pop() {
// 先判断栈是否已经空
if (isEmpty()) {
// 直接抛出一个 运行时异常 ,此异常非必须捕获 其后不用return
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]);// 此处用的printf 前面是String的格式化
}
}
// 返回运算符的优先级,优先级是程序员来确定的,假定优先级使用数字表示,数字越大,则优先级越高
public int priority(int oper) {// 传进一个操作符,因为字符型有一个先后,就是大小关系,可以用int
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '-' || oper == '+') {
return 0;
} else {
return -1;// 假定当前的表达式只有+,-,*,/
}
}
// 可以返回当前栈顶的值,但不是真正的pop
public int peek() {
return stack[top];
}
// 判断是否为一个运算符
public boolean isOper(char value) {
return value == '+' || value == '-' || value == '*' || value == '/';
}
// 计算方法
public int caculate(int n1, int n2, int oper) {
int result = 0;// 用于存放计算结果
switch (oper) {
case '+': {
result = n1 + n2;
break;
}
case '-': {
result = n2 - n1;// 注意顺序
break;
}
case '*': {
result = n1 * n2;
break;
}
case '/': {
result = n2 / n1;// 注意顺序
break;
}
default:
break;
}
return result;
}
}
3. 栈实现逆波兰表达式(后缀表达式)计算器
逆波兰表达式(后缀表达式):运算符位于操作数之后
/*逆波兰表达式(后缀表达式)的计算机求值:
*从左至右扫描表达式,遇到数字时,将数字压入栈堆,遇到运算符,弹出栈顶的两个数,用运算符对它们进行计算(栈顶元素和次顶元素),并将结果入栈
*重复上述过程,直到表达式的右端,最后计算的值即为表达式的值
*例如:(3+4)*5-6 对应的后缀表达式是 3 4 + 5 * 6 -
*步骤:
*1. 从左至右扫描表达式,将3和4压入栈堆
*2. 遇到+运算符,因此弹出4和3, 计算3+4=7 ,再将7入栈
*3. 将5入栈
*4. 遇到*运算符,因此弹出5和7, 计算7*5=35 再将35入栈
*5. 将6入栈
*6. 最后-运算符,因此弹出6和35 计算35-6=29 就是最终结果,*/
**次顶元素在运算的最前方,次顶元素 -栈顶元素 **
package 栈;
/*思路:
* 1. 先将String类型的逆波兰表达式,放到ArrayList中去
* 2. 将ArrayList 传递给一个方法,遍历ArrayList,(因为有下标,就不用另外设置扫描索引了) 配合栈Stack ,完成计算
* 3. 栈Stack直接使用系统的,不必再用数组模拟栈*/
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class NiBoLan {
public static void main(String[] args) {
String string = "3 4 + 5 * 6 -"; // 为了方便,逆波兰表达式的数字和运算符之间用空格隔开
List<String> list = getList(string);
System.out.println("list=" + list);
int re = calculate(list);
System.out.println("计算的结果="+re);
//输出:
//list=[3, 4, +, 5, *, 6, -]
//计算的结果=29
}
// 将String类型的逆波兰表达式,放到ArrayList中
public static List<String> getList(String string) {
// 将String类型的逆波兰表达式 分割
String[] splitString = string.split(" ");//将字符串分割的结果都放入到String数组中
List<String> list = new ArrayList<>();
for (String s : splitString) {
list.add(s);
}
return list;
}
// 完成对逆波兰表达式的计算
public static int calculate(List<String> list) {
// 创建栈,只需要一个栈
Stack<String> stack = new Stack<String>();
// 遍历ArrayList
for (String string : list) {
// 使用正则表达式 来识别数字 \d匹配一个数字,\d+ 匹配1个或多个数字,前面多一个\ ,是为了转义
if (string.matches("\\d+")) {
stack.push(string);// 入栈
} else {// 是运算符,弹出栈顶的两个数,用运算符对它们进行计算(栈顶元素和次顶元素),并将结果入栈
int num1 = Integer.parseInt(stack.pop());// pop出来是字符串,因此转为整形
int num2 = Integer.parseInt(stack.pop());
int result = 0;
// 接下来,判断是那个运算符
if (string.equals("+")) {
result = num2 + num1;
} else if (string.equals("-")) {
result = num2 - num1;
} else if (string.equals("*")) {
result = num2 * num1;
} else if (string.equals("/")) {
result = num2 / num1;
}else {
throw new RuntimeException("运算符有误!!!");
}
//最后结果入栈
stack.push(""+result);//因为结果是int ,而栈中要求String
}
}
//最后留在栈中的就是结果 String => int
return Integer.parseInt(stack.pop());
}
}
4.中缀表达式转为后缀表达式
/*思路:
* 1. 初始化两个栈:运算符栈s1和存储中间结果的栈s2
* 2. 从左到右扫描中缀表达式
* 3. 遇到操作数时,将其压入s2
* 4. 遇到运算符时,比较其与s1栈顶运算符的优先级:
* (1) 若s1为空,或栈顶运算符为左括号"(" ,则直接将其压入s1
* (2) 若优先级比栈顶运算符的高,也将其压入s1
* (3) 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到4.1 与s1中的新的栈顶运算符进行比较
* 5. 遇到括号时:
* (1) 若是左括号"(",则直接将其压入s1
* (2) 若是右括号")",则依次弹出s1的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢掉
* 6. 重复步骤2到5,直到表达式的最右边
* 7. 将s1中剩余的运算符依次弹出并压入s2
* 8. 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式所对应的后缀表达式。*/
/*举例子:1+((2+3)*4)-5
* (1) 1入s2
* (2) +入运算符栈s1, 两个左括号"(" 入运算符栈s1
* (3) 2入s2
* (4) +直接将其压入s1
* (5) 3入s2
* (6) 右括号")", 依次弹出s1的运算符,并压入s2,直到遇到左括号为止: 将+压入s2 此时将这一对括号丢掉
* (7) *直接将其压入s1 4入s2
* (8) 右括号")", 依次弹出s1的运算符,并压入s2,直到遇到左括号为止: 将*压入s2 此时将这一对括号丢掉
* (9) 因为- 和 + 是同级的优先级,将s1栈顶的运算符 + 弹出并压入到s2中,再次转到4.1 与s1中的新的栈顶运算符进行比较:
* 因为此时s1空栈,所以直接将 - 压入s1
* (10) 5入s2 到表达式的最右边 将s1中剩余的运算符 - 弹出并压入s2
* (11) 依次弹出s2中的元素并输出:- 5 * 4 + 3 2 1 结果的逆序即:123+4*5-
*
* */
代码实现:
package 栈;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/*思路:
* 1. 先将String类型的逆波兰表达式,放到ArrayList中去
* 2. 将ArrayList 传递给一个方法,遍历ArrayList,(因为有下标,就不用另外设置扫描索引了) 配合栈Stack ,完成计算
* 3. 栈Stack直接使用系统的,不必再用数组模拟栈*/
public class NiBoLan {
public static void main(String[] args) {
// String string = "3 4 + 5 * 6 -"; // 为了方便,逆波兰表达式的数字和运算符之间用空格隔开
// List<String> list = getList(string);
// System.out.println("list=" + list);
// int re = calculate(list);
// System.out.println("计算的结果="+re);
String string2 = "1+((2+3)*4)-5";
List<String> list2 = middleList(string2);
System.out.println("中缀表达式对应的List2 = " + list2);
List<String> list3 = changeList(list2);
System.out.println("后缀表达式对应的List = " + list3);
String string3 = "1 2 3 + 4 * + 5 -";
List<String> list1 = getList(string3);
int r = calculate(list1);
System.out.println("计算的结果="+r);
}
/*输出结果:
中缀表达式对应的List2 = [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
不存在该运算符
不存在该运算符
后缀表达式对应的List = [1, 2, 3, +, 4, *, +, 5, -]
计算的结果=16
*/
//因为小括号没有设置优先级,所以会出现“不存在该运算符”
// 将String类型的逆波兰表达式,放到ArrayList中
public static List<String> getList(String string) {
// 将String类型的逆波兰表达式 分割
String[] splitString = string.split(" ");// 将字符串分割的结果都放入到String数组中
List<String> list = new ArrayList<>();
for (String s : splitString) {
list.add(s);
}
return list;
}
// 完成对逆波兰表达式的计算
public static int calculate(List<String> list) {
// 创建栈,只需要一个栈
Stack<String> stack = new Stack<String>();
// 遍历ArrayList
for (String string : list) {
// 使用正则表达式 来识别数字 \d匹配一个数字,\d+ 匹配1个或多个数字,前面多一个\ ,是为了转义
if (string.matches("\\d+")) {
stack.push(string);// 入栈
} else {// 是运算符,弹出栈顶的两个数,用运算符对它们进行计算(栈顶元素和次顶元素),并将结果入栈
int num1 = Integer.parseInt(stack.pop());// pop出来是字符串,因此转为整形
int num2 = Integer.parseInt(stack.pop());
int result = 0;
// 接下来,判断是那个运算符
if (string.equals("+")) {
result = num2 + num1;
} else if (string.equals("-")) {
result = num2 - num1;
} else if (string.equals("*")) {
result = num2 * num1;
} else if (string.equals("/")) {
result = num2 / num1;
} else {
throw new RuntimeException("运算符有误!!!");
}
// 最后结果入栈
stack.push("" + result);// 因为结果是int ,而栈中要求String
}
}
// 最后留在栈中的就是结果 String => int
return Integer.parseInt(stack.pop());
}
// 将String类型的中缀表达式,放到ArrayList中
public static List<String> middleList(String string) {
List<String> list = new ArrayList<>();
int i = 0; // 类比于指针,用于遍历中缀表达式的字符串
String string2;// 用于多位数的拼接
char c;// 每遍历一个字符,就放到c中
do {
if ((c = string.charAt(i)) < 48 || (c = string.charAt(i)) > 57) {// 若扫描到的c是一个非数字,就将其添加到List集合中
list.add(c + "");
i++; // i要后移,依次扫描后面的字符
} else {// 若扫描到的c是一个数字,就要考虑多位数 字符'0'对应的ASCII码值是48 字符'9'对应的ASCII码值是57
string2 = "";// 必须在此处制空
while (i < string.length() && (c = string.charAt(i)) >= 48 && (c = string.charAt(i)) <= 57) {
string2 += c;// 用于多位数的拼接
i++;
}
list.add(string2);
}
} while (i < string.length());
return list;
}
// 将得到的中缀表达式的List => 对应的后缀表达式的List
public static List<String> changeList(List<String> list1) {
//本来要定义两个栈,因为s2这个栈,在整个转换的过程中,没有pop操作,而且后面还要逆序输出,较为麻烦,不用new Stack<String>
//可以用List<String> 来模拟,因为它有下标,有序(存进去的顺序和取出的顺序相同,正好与栈的特点相同)
Stack<String> s1 = new Stack<String>();
List<String> s2 = new ArrayList<>();
// 遍历中缀表达式的List
for (String it : list1) {
if (it.matches("\\d+")) {// 如果是一个数字,入s2
s2.add(it);
} else if (it.equals("(")) {// 左括号"(" ,则直接将其压入s1
s1.push(it);
} else if (it.equals(")")) {// 若是右括号")",则依次弹出s1的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢掉
while (!s1.peek().equals("(")) {// 关于栈的方法peek() :Looks at the object at the top of this stack without
// removing it from the stack.
s2.add(s1.pop());
}
s1.pop();// 将 ( 弹出s1栈,用于和右括号消除一对括号
} else {// 遇到运算符时,比较其与s1栈顶运算符的优先级 此处可以写一个方法来比较优先级
// 当it的优先级<=s1栈顶的运算符,将s1栈顶的运算符弹出并压入到s2中,再次与s1中的新的栈顶运算符进行比较
while (s1.size() != 0 && Oper.getoper(s1.peek()) >= Oper.getoper(it)) {
s2.add(s1.pop());
}
// 将it压入栈s1
s1.push(it);
}
} // for循环结束,扫描完毕
// 将s1中剩余的运算符依次弹出并压入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;
}
}
//编写一个类 处理运算符的优先级
class Oper {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
// 方法实现:返回对应的优先级的数字
public static int getoper(String stringOper) {
int result = 0;
switch (stringOper) {
case "+": {
result = ADD;
break;
}
case "-": {
result = SUB;
break;
}
case "*": {
result = MUL;
break;
}
case "/": {
result = DIV;
break;
}
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现