🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔🦾🦾🦾
目录
1. 什么是栈?(Stack)
栈是一种数据结构,是操作受限制的线性表(先进后出),它只能在固定的一端进行插入和删除操作,进行数据插入和删除的一端为栈顶,另一端为栈底,按存储方式分为顺序栈(更好实现)和链式栈。
压栈:栈的插入操作叫做进栈/压栈/入栈。入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
2. 栈的使用
方法 | 功能说明 |
Stack() | 构造一个空栈 |
E push(E e) | 将e入栈并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素的个数 |
boolean empty() | 检测栈是否为空 |
●顺序栈:Deque接口+实现类ArrayDeque
Deque<Integer> deque = new ArrayDeque<>();//顺序栈
●链式栈:Deque接口+实现类LinkedList
Deque<Integer> deque1 = new LinkedList<>();//链式栈
栈的使用建议使用上面两种方式,不建议使用Stack
3. 模拟实现一个栈
3.1 构造方法和成员属性
public class MyStack {
private int[] elem; //存放数据的数组
private int size; //有效元素个数
private static final int DEFAULT_CAPACITY = 10; //约定好的默认容量
public MyStack() {
this.elem = new int[DEFAULT_CAPACITY];
this.size = 0;
}
}
有了顺序表和链表的学习,再次学习栈是很轻松的,但是在源码中,开辟空间是在 push 中开辟的,跟顺序表类似的。
3.2 push 方法
// 压栈
public int push(int data) {
// 判断是否需要增容
if (this.size == this.elem.length) {
this.elem = Arrays.copyOf(this.elem, this.size * 2);
}
// 压栈只能往栈顶压栈
this.elem[this.size++] = data;
return data;
}
在实现push方法要注意,栈扩容的问题,因为我们底层使用的是数组,所以可以用 Arrays.copyOf方法进行扩容。
3.3 pop 方法
// 出栈
public int pop() {
// 判断栈是否为null
if (this.size == 0) {
throw new MyStackEmptyException("栈为空!");// 自定义异常
}
return this.elem[--this.size];
}
在出栈方法中,如果栈为null的情况是不能进行出栈的,也就是有效元素个数size为0的情况,这里博主是直接抛出了一个自定义异常。在返回值的地方也要注意,出栈后栈的元素会减少一个,但我们只需要设置有效数据减一个即可,就像计算机中的删除一样,本质是将数据设置成无效,有新的数据可以直接覆盖。
3.4 peek 方法
// 查看栈顶元素
public int peek() {
// 判断栈是否为null
if (this.size == 0) {
throw new MyStackEmptyException("栈为空!"); //自定义异常
}
return this.elem[this.size - 1];
}
peek方法与pop方法相差无几,需要注意的是在返回栈顶元素的时候不要使有效数据个数减少了就像。
3.5 empty 方法
// 判断栈是否为空
public boolean empty() {
return this.size == 0;
}
4.栈相关的OJ题
4.1 有效的括号
●题目展示
● 写题链接:20. 有效的括号 - 力扣(LeetCode)
● 写题思路:
1) 新建一个栈存左括号,得到对应右括号出左括号,这次匹配成功,继续匹配
2) 只要左括号的栈为空,且给定字符串还没遍历完,那就代表右括号多了
3) 给定字符串遍历完了,但是存左括号的栈还有剩余的左括号,那么就是左括号多了
4) 只要给定字符串遍历完毕,且存左括号的栈无剩余,则为有效括号。
● 图示
● 实现代码:
class Solution {
public boolean isValid(String s) {
Deque<Character> leftStack = new ArrayDeque();
for(int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch == '(' || ch == '[' || ch == '{') {
leftStack.push(ch);
}else {
//如果栈中无左括号,又得到了一个右括号,则无效
if(leftStack.isEmpty()) { //多余的右括号
return false;
}
//ch为字符串中得到的右括号,ch2为栈中的左括号
char ch2 = leftStack.peek();
if(ch == ')' && ch2 == '(' || ch == ']' && ch2 == '[' || ch == '}' && ch2 == '{') {
//出栈
leftStack.pop();
}else {
return false;//不匹配
}
}
}
if(!leftStack.isEmpty()) { //还有多余的左括号
return false;
}
return true;
}
}
4.2 栈的压入弹出序列
● 题目展示
● 写题链接:栈的压入、弹出序列_牛客题霸_牛客网 (nowcoder.com)
● 写题思路:
1) 新建一个栈,根据给定的出栈序列依次进行匹配。
2) 只要不等于出栈序列的数字,就一直遍历入栈序列入栈
3) 等于出栈序列的数字后,直接出栈,只要等于出栈序列的数字,该栈就一直出栈,直到不等于结束出栈,继续遍历给定的入栈序列进行入栈。
4) 重复该操作直到遍历完入栈序列,最后这个栈如果为空,则匹配成功,反之则匹配失败
● 图示
● 实现代码:
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for(int i = 0;i < n;i++){
stack.push(pushA[j]);
//此处用equals是因为Integer值的取值范围在-128~127
while(j < pop.length && (stack.isEmpty()||stack.peek().equals(popA[i])){
stack.pop();
j++;
}
return stack.empty();
}
}
4.3 逆波兰(后缀)表达式求值
● 题目展示
● 写题链接:150. 逆波兰表达式求值 - 力扣(LeetCode)
● 写题思路:
1) 准备中间栈遍历给定后缀表达式,遇到操作数则入中间栈
2) 遇到操作符则出栈两个操作数进行运算,并将运算结果入栈
3) 直到后缀表达式遍历完毕,栈中最后一个元素值则为计算结果
● 知识普及
在讲解这个题之前给大家普及一点知识,我们平时的算术表达式中,运算符总是出现在两个操作数之间,例如 5 * (7 - 2 * 3) + 8 / 2 ,这种形式叫做中缀表达式,而我们的逆波兰表达式又叫后缀表达式,那什么是后缀表达式呢???
我们把中缀表达式的运算符移动到自己所在的括号的右括号的右边,然后再去括号,这就是逆波兰表达式,,,那这个东西该怎么计算呢???
● 实现代码:
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<Integer>(); //存操作数
for(int i = 0; i < tokens.length; i++) {
String ch = tokens[i];
if(!isOperators(ch)) {
stack.push(Integer.parseInt(ch));
}else {
int num1 = stack.pop();//出两个操作数
int num2 = stack.pop();
int result = 0; //计算结果
switch(ch) {
case "+":
result = num1 +num2;
break;
case "-":
result = num2 - num1;
break;
case "*":
result = num1 * num2;
break;
case "/":
result = num2 / num1;
break;
}
//结果入栈
stack.push(result);
}
}
return stack.pop();
}
//判断是否为操作符
public boolean isOperators(String s) {
return s.equals("+") || s.equals("*") || s.equals("-") || s.equals("/");
}
}
4.4 最小栈
● 题目展示
● 写题链接:力扣
● 写题思路:题目的目的是想让我们在O(1)时间复杂度内拿到栈的最小值,所以这里我们不能去遍历栈,而是需要用到两个栈,下面用画图的形式分析一下如何做到:
● 实现代码:
class MinStack {
private Stack<Integer> stack1;
private Stack<Integer> MinStack;
public MinStack() {
stack1 = new Stack<>();
MinStack = new Stack<>();
}
public void push(int val) {
stack1.push(val);
if(MinStack.empty()) {
MinStack.push(val);
} else {
if(val <= MinStack.peek()) {
MinStack.push(val);
}
}
}
//题目规定了栈不为空,所以这里没判断
public void pop() {
int tmp = stack1.pop();
if(tmp == MinStack.peek()) {
MinStack.pop();
}
}
//题目规定了栈不为空
public int top() {
return stack1.peek();
}
public int getMin() {
return MinStack.peek();
}
}
5. 栈,虚拟机栈,栈帧的区别
栈:一种后进先出的数据结构,继承自Vector
虚拟机栈:具有特殊作用的一块内存空间。栈区:是线程私有的,存放的是函数调用相关信息,主要是栈帧,它是按照数据结构中栈的特性来实现的
栈帧:一种结构,与函数调用相关。内部:局部变量表,操作数栈。每个方法运行时JVM会创建一个栈帧,然后将栈帧压到虚拟机栈中,调用结束时,该方法对应的栈帧会从虚拟机栈中出栈。