当年面试问的最多的那批 Java 集合框架现在问的还多么? 编程十年(手动狗头) , 我把这些集合框架做成盲盒, 无论你多难, 我都想去了解你, 今天来看-----pia~, Stack,一种线性数据结构,遵循后进先出(LIFO)原则。支持push(压入)、pop(弹出)和 peek(查看栈顶)等操作,常用于处理方法调用、表达式求值等场景。
Stack 基本用法
一. 概念
栈: 一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
- 出栈:栈的删除操作叫做出栈。出数据在栈顶。
栈在现实生活中的例子:往弹夹里面装子弹是入栈, 开枪发射时是出栈
二. 栈的使用
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s.size()); // 获取栈中有效元素个数---> 4
System.out.println(s.peek()); // 获取栈顶元素---> 4
s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
if(s.empty()){
System.out.println("栈空");
}else{
System.out.println(s.size());
}
}
三. 栈的模拟实现
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。
class MyStack {
int[] array;
int size;
public MyStack(){
array = new int[3];
}
public int push(int e){
ensureCapacity();
array[size++] = e;
return e;
}
public int pop(){
int e = peek();
size--;
return e;
}
public int peek(){
if(empty()){
throw new RuntimeException("栈为空,无法获取栈顶元素");
}
return array[size-1];
}
public int size(){
return size;
}
public boolean empty(){
return 0 == size;
}
private void ensureCapacity(){
if(size == array.length){
array = Arrays.copyOf(array, size*2);
}
}
}
如果要用单链表实现栈的话, 需要头插, 头删
如果用双向链表的话, 头插, 头删, 尾插, 尾删时间复杂度都是 O (1)
所以 LinkedList 可以当作 Stack 来使用
四. 栈的应用场景
1. 改变元素的序列
- 若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()
A: 1,4,3,2 B: 2,3,4,1 C: 3,1,4,2 D: 3,4,2,1
答案: C, 注意不是非得所有元素都进入之后才能出, 是随时可以出栈
- 一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )。
A: 12345ABCDE B: EDCBA54321 C: ABCDE12345 D: 54321EDCBA
答案: B
2. 将递归转化为循环
比如:逆序打印链表 (还有二叉树的前中后序遍历、快速排序等等的非递归方式都用到栈)
// 递归方式
void printList(Node head){
if(null != head){
printList(head.next);
System.out.print(head.val + " ");
}
}
// 循环方式
void printList(Node head){
if(null == head){
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while(null != cur){
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
System.out.print(s.pop().val + " ");
}
}
3. 括号匹配
思路:
遇到左括号入栈, 遇到右括号时将栈顶的对应左括号出栈, 如果两个括号不是一对的话就说明匹配不上
class Solution {
public boolean isValid(String s) {
Stack<Character> stack=new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch=s.charAt(i);
if(ch=='['||ch=='('||ch=='{'){
stack.push(ch);
}else{
if(stack.isEmpty()){
return false;
}
int flag=stack.pop();
if(!(flag=='['&&ch==']'
||flag=='('&&ch==')'
||flag=='{'&&ch=='}')){
return false;
}
}
}
return stack.isEmpty();
}
}
4. 逆波兰表达式求值
逆波兰表达式又叫后缀表达式, 我们一般使用的是中缀表达式, 后缀表达式是将 运算符号写在两个操作数的后面
比如: ((2 + 1) * 3) --> ((2 1 +) 3 *)
思路:
遇到数字就入栈, 遇到操作符就将栈中最上面的两个数字取出来根据运算符进行运算, 并将结果压入栈中
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack=new Stack<>();
for(int i=0;i<tokens.length;i++){
String str=tokens[i];
if(str.equals("+")){
int right=stack.pop();
int left=stack.pop();
stack.push(left+right);
}else if(str.equals("-")){
int right=stack.pop();
int left=stack.pop();
stack.push(left-right);
}else if(str.equals("*")){
int right=stack.pop();
int left=stack.pop();
stack.push(left*right);
}else if(str.equals("/")){
int right=stack.pop();
int left=stack.pop();
stack.push(left/right);
}else {
//是数字
stack.push(Integer.valueOf(str));
}
}
return stack.pop();
}
}
5. 出栈入栈次序匹配
思路:
一个指针指向出栈顺序数组, 先根据入栈顺序往里面压, 如果栈顶的元素与出栈
指针指向的值相同, 就出栈, 指针++, 直到栈顶元素与指针指向的值不相等, 此时继续入栈, 重复上次操作, 如果最后栈为空, 说明匹配上了, 否则说明匹配不上
import java.util.Stack;
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
Stack<Integer> stack = new Stack<>();
int pop = 0;
for (int x : pushA) {
stack.push(x);
while (!stack.isEmpty() && stack.peek() == popA[pop]) {
stack.pop();
pop++;
}
}
return stack.isEmpty();
}
}
6. 最小栈
思路:
使用两个栈, 一个是普通栈, 用来压入正常值, 一个是单调递减栈, 也就是说栈中元素从下往上, 值是递减的, 压入的规则是, 如果要压入的正常值, 比单调栈顶值小, 就压入正常值, 否则就压入单调栈顶值, 普通栈的话压入正常值就可以了, 出栈的时候, 两个栈同时 pop
class MinStack {
Deque<Integer> xStack;
Deque<Integer> minStack;
public MinStack() {
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
xStack.push(x);
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
xStack.pop();
minStack.pop();
}
public int top() {
return xStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
五. 概念区分
栈、虚拟机栈、栈帧有什么区别呢?
-
栈(Stack):
一种线性数据结构,遵循后进先出(Last In, First Out,LIFO)的原则。支持两种主要操作:入栈(Push)和出栈(Pop)。数据项只能从栈的顶部进行插入和删除。 -
虚拟机栈(Virtual Machine Stack):
虚拟机栈是在JVM 中的一块内存空间。每个运行的线程都会有一个独立的虚拟机栈。虚拟机栈中存储了方法的调用信息,包括方法的参数、局部变量和返回值等。每个方法在虚拟机栈上都会对应一个栈帧。 -
栈帧(Stack Frame):
栈帧是与函数调用相关的概念,它是虚拟机栈中的一个单元,包含了一个方法的调用信息和局部数据。当一个函数被调用时,会创建一个对应的栈帧并推入虚拟机栈,当函数执行完毕后,栈帧会被弹出。
栈帧通常包括以下几个部分:
- 方法参数:存储被调用方法的参数值。
- 方法返回地址:指向调用该方法的指令地址,用于在方法执行完毕后返回到正确的位置继续执行。
- 局部变量区:用于存储函数内部定义的局部变量。
- 操作数栈(Operand Stack):用于存储函数执行过程中的操作数和临时结果。
- 动态链接(Dynamic Link):指向调用该方法的上一个方法的栈帧,用于访问外部的变量和数据。
以上就是对 栈的讲解, 希望能帮到你 !
评论区欢迎指正 !