基本介绍
- 栈是一个先入后出(FILO-FirstInLastOut)的有序列表;
- 栈(stack)限制线性表中元素的插入和删除只能在线性表的同一端进行,允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom);
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除;
- 图解方式说明出栈(pop)和入栈(push)的概念:
数组模拟栈
实现思路
- 使用数组来模拟栈;
- 定义一个 top 来表示栈顶,初始化为 -1;
- 入栈的操作:当有数据加入到栈时:
top++; stack[top] = data;
- 出栈的操作:
int value = stack[top]; top--; return value;
实现逻辑
public class ArrayStack {
// 栈的最大容量
private int maxSize;
// 使用数组模拟栈,数据就放在该数组
private Integer[] stack;
// top表示栈顶,初始化为-1
private int top = -1;
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
this.stack = new Integer[maxSize];
}
// 栈空
public boolean isEmpty() {
return this.top == -1;
}
// 栈满
public boolean isFull() {
return this.top == this.maxSize - 1;
}
// 入栈
public void push(int value) {
if (isFull()) {
System.out.println("stack is full");
return;
}
this.top++;
this.stack[this.top] = value;
}
// 出栈,并回收该栈空间
public int pop() {
if (isEmpty()) {
System.out.println("stack is empty");
return -1;
}
int value = this.stack[this.top];
// 回收栈空间
this.stack[this.top] = null;
this.top--;
return value;
}
// 显示栈顶元素
public int peek() {
return this.stack[this.top];
}
// 显示栈当前空间大小
public int size() {
return this.top + 1;
}
// 遍历栈
public void show() {
if (isEmpty()) {
System.out.println("stack is empty");
return;
}
// 遍历时,需要从栈顶开始显示数据
for (int i = this.top; i >= 0; i--) {
System.out.println("stack element is " + stack[i]);
}
}
}
链表模拟栈
实现思路
实现逻辑
public class LinkStack {
// 栈顶元素
private Node top;
// 栈大小
private int size;
// 创建空栈
public LinkStack() {
this.top = null;
this.size = 0;
}
// 以value为元素创建栈
public LinkStack(Object value) {
this.top = new Node(value, null);
this.size++;
}
// 判断是否为空栈
public boolean isEmpty() {
return this.size == 0;
}
// 获取栈长度
public int size() {
return this.size;
}
// 压栈
public void push(Object value) {
// 让 top 指向新创建的元素,新元素的 next 引用指向原来的栈顶元素
this.top = new Node(value, this.top);
this.size++;
}
// 出栈
public Object pop() {
if (isEmpty()) {
// System.out.println("目前是空栈,没法进行出栈");
return "error";
}
Node temp = this.top;
// 更新头节点
this.top = this.top.getNext();
// 释放原栈顶元素的 next 引用,删除指针指向
temp.setNext(null);
this.size--;
return temp.getValue();
}
// 访问栈顶元素
public Object peek() {
if (isEmpty()) {
// System.out.println("目前是空栈,没法进行出栈");
return "error";
}
return this.top.getValue();
}
// 遍历栈
public void show() {
if (isEmpty()) {
// System.out.println("目前是空栈,没法进行出栈");
return;
}
while (null != this.top) {
System.out.println(this.top.getValue() + "\t");
this.top = this.top.getNext();
}
}
}
class Node {
private Object value;
private Node next;
public Node() {
}
public Node(Object value, Node next) {
this.value = value;
this.next = next;
}
...get/set
}
注意事项
如果在出栈前就已经对链栈中的栈内元素进行打印输出,那么在执行出栈操作的时候会产生空指针异常,因为在进行打印镇内元素时,栈顶指针发生了变化,整个栈内元素打印完毕,top指针为空,此时在进行出栈时,便会产生空指针异常问题。
栈实现计算器
实现思路
实现逻辑
计算表达式的结果
private static int calculator(String expression) {
// 创建数值栈
ArrayStack numStack = new ArrayStack(10);
// 创建符号栈
ArrayStack signStack = new ArrayStack(10);
// 定义在表达式扫描时变动的下标
int index = 0;
// 保存每次扫描到的字符
char temp = ' ';
// 用于拼接多个数字
String keepTemp = "";
int result = 0;
while (true) {
// 依次得到expression的每一个字符
temp = expression.substring(index, index + 1).charAt(0);
if (isSign(temp)) {
if (signStack.isEmpty() || temp == '(') {
// 如果当前符号栈为空,就直接入符号栈
signStack.push(temp);
} else if (temp == ')') {
// 如果为右括号
// 就需要从数值栈中pop出两个数,再从符号栈中pop出一个符号,进行运算并将结果入数值栈
// 直到符号栈栈顶元素为左括号,弹出左括号结束
while (signStack.peek() != '(') {
int num1 = numStack.pop();
int num2 = numStack.pop();
int sign = signStack.pop();
result = calResult(num1, num2, sign);
// 把运算的结果压入数值栈
numStack.push(result);
}
signStack.pop();
} else if (signStack.peek() == '(') {
// 如果当前符号栈栈顶元素为左括号,就直接入符号栈,[此逻辑须在右括号判断之后]
signStack.push(temp);
} else if (priority(temp) <= priority(signStack.peek())) {
// 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,
// 就需要从数值栈中pop出两个数,再从符号栈中pop出一个符号,进行运算并将结果压入数值栈
// 然后将当前的操作符入符号栈
int num1 = numStack.pop();
int num2 = numStack.pop();
int sign = signStack.pop();
result = calResult(num1, num2, sign);
// 把运算的中间结果压入数值栈
numStack.push(result);
// 将当前的操作符入符号栈
signStack.push(temp);
} else {
// 如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
signStack.push(temp);
}
} else {
// 1、当处理多位数时,不能发现是一个数就立即入栈,因为它可能是多位数
// 2、处理数值时,需要向expression的index后再看一位,如果是数就进行扫描,如果是符号才入栈
// 3、因此我们需要定义一个变量字符串,用于拼接
keepTemp = keepTemp + temp;
// 如果 temp 已经是 expression 的最后一位,就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepTemp));
} else if (isSign(expression.substring(index + 1, index + 2).charAt(0))) {
// 判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
// 注意是看后一位,不是index++
numStack.push(Integer.parseInt(keepTemp));
keepTemp = "";
}
}
// 让 index+1, 并判断是否扫描到expression最后
index++;
if (index >= expression.length()) {
break;
}
}
// 当表达式扫描完毕,顺序地从数栈和符号栈中 pop 出相应的数和符号,并运行
while (true) {
//如果符号栈为空,则计算到最后的结果, 数栈中只有一个数字【结果】
if (signStack.isEmpty()) {
break;
}
int num1 = numStack.pop();
int num2 = numStack.pop();
int sign = signStack.pop();
result = calResult(num1, num2, sign);
numStack.push(result);
}
// 将数栈的最后数,pop出,就是结果
return numStack.peek();
}
返回运算符的优先级,优先级是程序员来确定,优先级使用数字表示,数字越大,则优先级就越高。
private static int priority(int sign) {
if (sign == '(' || sign == ')') {
return 2;
}
if (sign == '*' || sign == '/') {
return 1;
}
if (sign == '+' || sign == '-') {
return 0;
}
return -1;
}
判断是不是一个运算符
private static boolean isSign(char sign) {
return sign == '*' || sign == '/' || sign == '+' || sign == '-' || sign == '(' || sign == ')';
}
算术运算
private static int calResult(int num1, int num2, int sign) {
switch (sign) {
case '+':
return num1 + num2;
case '-':
return num2 - num1;
case '*':
return num1 * num2;
case '/':
return num2 / num1;
default:
throw new RuntimeException("sign is error");
}
}
逆波兰表达式
逆波兰表达式又叫做后缀表达式,把运算量写在前面,把运算符写在后面。中缀表达式就是我们平时书写的数学表达式,例如:(2 + 3) * 5,转为逆波兰表达式为:2 3 5 + *。
转逆波兰表达式
实现思路
中缀表达式转逆波兰表达式,步骤如下:
-
初始化两个栈:运算符栈s1和储存中间结果的栈s2;
-
从左至右扫描中缀表达式;
-
遇到操作数时,将其压s2;
-
遇到运算符时,比较其与s1栈顶运算符的优先级:
4.1. 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
4.2. 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
4.3. 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;
-
遇到括号时:
5.1. 如果是左括号“(”,则直接压入s1;
5.2. 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
-
重复步骤2至5,直到表达式的最右边
-
将s1中剩余的运算符依次弹出并压入s2
-
依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。
将中缀表达式 1+((2+3)×4)-5 转换为后缀表达式的过程如下:
逆波兰表达式求值
实现思路
(3+4)×5-6 对应的后缀表达式就是:3 4 + 5 × 6 -,针对后缀表达式求值步骤如下:
-
从左至右扫描,将3和4压入堆栈;
-
遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
-
将5入栈;
-
接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
-
将6入栈;
-
最后是-运算符,计算出35-6的值,即29,由此得出最终结果。
实现逻辑
将中缀表达式拆分放入list集合
private static List toInfix(String str) {
List<String> result = new ArrayList<>();
int startIndex = 0;
int endIndex = 0;
while (endIndex != str.length()) {
// 遍历到符号前,把符号前的(endIndex-startIndex)之间的数值加入到结果集合中
// 否则将符号直接加入结果集合中
if (str.charAt(endIndex) < 48 || str.charAt(endIndex) > 57) {
String substring = str.substring(startIndex, endIndex);
if (!Strings.isNullOrEmpty(substring)) {
result.add(substring);
}
result.add(str.charAt(endIndex) + "");
startIndex = endIndex + 1;
}
endIndex++;
}
String substring = str.substring(startIndex, endIndex);
if (!Strings.isNullOrEmpty(substring)) {
result.add(substring);
}
return result;
}
将中缀表达式转为逆波兰表达式
private static List<String> toSuffix(List<String> list) {
// 存放操作符的栈
Stack<String> stack = new Stack();
// 储存结果的栈
List<String> result = new ArrayList<>();
for (String temp : list) {
if (temp.matches("\d+")) {
result.add(temp);
} else if (temp.equals("(")) {
stack.push(temp);
} else if (temp.equals(")")) {
// 如果是右括号“)”,则依次弹出操作符栈顶的运算符,并压入结果栈,直到遇到左括号为止,
// 此时将这一对括号丢弃
while (!stack.peek().equals("(")) {
result.add(stack.pop());
}
// 弹出操作符栈,消除小括号
stack.pop();
} else {
// 当temp的优先级小于等于符号栈栈顶运算符, 将符号栈栈顶的运算符弹出并加入到结果栈中,
// 再次转到(4.1步骤),与符号栈中新的栈顶运算符相比较
while (stack.size() != 0 && compare(stack.peek()) >= compare(temp)) {
result.add(stack.pop());
}
stack.push(temp);
}
}
// 将符号栈中剩余的运算符依次弹出并加入结果栈
while (stack.size() != 0) {
result.add(stack.pop());
}
return result;
}
逆波兰表达式运算
private static String calStack(List<String> toSuffix) {
Stack<String> stack = new Stack();
for (String temp : toSuffix) {
// // 匹配的是多位数,入栈
if (temp.matches("\d+")) {
stack.push(temp);
} else {
// pop出两个数,并运算,再入栈
int temp1 = Integer.parseInt(stack.pop());
int temp2 = Integer.parseInt(stack.pop());
stack.push(String.valueOf(cal(temp1, temp2, temp)));
}
}
return stack.pop();
}
应用场景
-
子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
-
处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
-
表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
-
二叉树的遍历。
-
图形的深度优先(depth-first)搜索法。
-
JDK 中的 Stack:blog.csdn.net/qq_42859864…