基本介绍

  • 栈的英文为(stack)
  • 栈是一个先入后出(FILO-First In Last Out)的有序列表。
  • 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
  • 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
  • 图解方式说明**出栈(pop)入栈(push)**的概念

出栈入栈操作

应用场景

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

快速入门

  • 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈,入栈等操作。

  • 思路分析

    image-20220718193728413

    1. 使用数组来模拟栈
    2. 定义一个top来表示栈顶,初始化为 -1
    3. 入柱的操作:当有数据加入到栈时,top++; stack[top] =data;
    4. 出栈的操作:int value = stack[top]; top–; return value;
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    package com.ysy.stack;

    import java.util.Scanner;

    public class ArrayStackDemo {
    public static void main(String[] args) {
    //测试栈
    ArrayStack stack = new ArrayStack(4);
    String key = "";
    boolean loop = true;//控制是否退出菜单
    Scanner sc = 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 = sc.next();
    switch (key){
    case "show":
    stack.list();
    break;
    case "push":
    System.out.println("请输入一个数~");
    int i = sc.nextInt();
    stack.push(i);
    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":
    sc.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[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("栈空");
    }
    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]);
    }
    }
    }

栈实现综合计算器(中缀表达式)

  • 就是最常见的运算表达式,如:(3 + 4) × 5 - 6
  • 中缀表达式求值对计算机来说不好操作,因此在计算时往往转换为后缀表达式

image-20220718202450431

  • 请问:计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式7×2×2-5+1-5×3-3,但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题

  • 思路分析

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

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

      • 当处理多位数时,需要向表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈
    3. 如果发现扫描到是一个符号,就分如下情况

      • 如果发现当前的符号栈为空,就直接入栈

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

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

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

  • 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.ysy.stack;

/**
* 一位数的加减乘除运算
*/
public class Calculator {
public static void main(String[] args) {
//完成表达式的运算
String expression = "70+2*6-4";
//创建两个栈:数栈和符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(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);
//判断是否是符号,然后做相应的处理
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);
}
}else {
//如果我们发现是一个数字,就直接入数栈
//numStack.push(ch - 48);

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

//如果ch已经是expression的最后一位,就直接入栈
if(index == expression.length() - 1){
numStack.push(Integer.parseInt(keepNum));
}else{
//当处理多位数时,需要向expression表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈
if(operStack.isOper(expression.substring(index+1, index+2).charAt(0))){
//后一位是运算符
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);
}
System.out.printf("表达式 %s = %d", expression, numStack.pop());
}
}

//定义一个ArrayStack,模拟出栈入栈操作
class ArrayStack2{
private int maxSize;//栈大小
private int[] stack;//定义一个数组用来模拟栈
private int top = -1;//定义一个**top**来表示栈顶,初始化为 -1

public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[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("栈空");
}
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;//用于存储计算结果
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,求值步骤如下
      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,由此得出最终结果

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

  • 后缀表达式的运算符位于操作数之后

    image-20220720200654526

  • 后缀表达式的计算机求值

    • 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素和栈顶元素〉,并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
    • 例如:(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,由此得出最终结果

逆波兰计算器

  • 输入一个逆波兰表达式,使用栈计算其结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.ysy.stack;

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

public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
//(3 + 4) × 5 - 6 对应的后缀表达式就是 3 4 + 5 × 6 -
//为了方便,逆波兰表达式中的数字和符号使用空格隔开
String suffixExpression = "3 4 + 5 * 6 -";
//思路
//1.先将suffixExpression=>放到ArrayList中
//2.将ArrayList传递给一个方法,遍历ArrayList,配合栈运算
List<String> rpnList = getListString(suffixExpression);
System.out.println(rpnList);

int res = calculate(rpnList);
System.out.println("(3 + 4) × 5 - 6 = " + res);
}

//一个逆波兰表达式,依次将数据和运算符 放入到ArrayList中
public static List<String> getListString(String suffixExpression){
//将suffixExpression分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : split) {
list.add(s);
}
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<>();
//遍历ls
for (String item : ls) {
//正则表达式取值
if(item.matches("\\d+")){//匹配的是多位数
//入栈
stack.push(item);
}else {
//pop出两个数,并运算,再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if(item.equals("+")){
res = num1 + num2;
}else if(item.equals("-")){
res = num1 - num2;
}else if(item.equals("*")){
res = num1 * num2;
}else if(item.equals("/")){
res = num1 / num2;
}else{
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中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
  • 实例

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

      image-20220720210413100

  • 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.ysy.stack;

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

public class InfixToSuffix {
public static void main(String[] args) {
//完成将中缀表达式转换为后缀表达式
//1 + ((2 + 3) × 4) - 5 =》1 2 3 + 4 * + 5 -
//直接对str进行操作,不方便,因此 先将中缀表达式转为对应的ArrayList
//将得到的中缀表达式对应的List => 后缀表达式对应的List
String expression = "1+((2+3)*4)-5";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List:" + infixExpressionList);//[1, +, (, (, 2, +, 3, ), ×, 4, ), -, 5]
List<String> suffixExpressionList = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的List:" + suffixExpressionList);
int result = calculate(suffixExpressionList);
System.out.println("1 + ((2 + 3) × 4) - 5 = " + result);
}

//将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> parseSuffixExpressionList(List<String> ls){
//定义两个栈
Stack<String> s1 = new Stack<String>();//符号栈
//因为s2在整个转换过程中没有pop操作,且后面还要逆序输出,因此直接使用List<String>代替
List<String> s2 = new ArrayList<>();
//遍历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栈顶的运算符弹出并压入到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> toInfixExpressionList(String s){
//定义一个List,存放中缀表达式对应的内容
List<String> ls = new ArrayList<>();
int i = 0;//指针,用来遍历中缀表达式的内容
String str;//对多位数进行拼接
char c;//每遍历到一个字符,就放入c
do{
//如果C是一个非数字,加入到ls
if((c=s.charAt(i)) < 48 || (c=s.charAt(i)) > 57){
ls.add(c + "");
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 suffixExpression){
//将suffixExpression分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : split) {
list.add(s);
}
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<>();
//遍历ls
for (String item : ls) {
//正则表达式取值
if(item.matches("\\d+")){//匹配的是多位数
//入栈
stack.push(item);
}else {
//pop出两个数,并运算,再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if(item.equals("+")){
res = num1 + num2;
}else if(item.equals("-")){
res = num1 - num2;
}else if(item.equals("*")){
res = num1 * num2;
}else if(item.equals("/")){
res = num1 / 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("不存在该运算符~");
break;
}
return result;
}
}

文章作者: Yang Shiyu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yang Shiyu !
  目录