栈
那么本篇,我们将介绍栈
栈其实是一种数据结构,用这么一个词可以来概括它:先进后出,后进先出
我们可以理解为:吃进去又吐出来🤣🤣🤣🤣
言归正传,我们来详细聊聊~
栈
概念
栈(Stack)是一种数据结构,它是一种线性数据结构,具有"先进后出"(Last In First Out, LIFO)的特性。栈可以看作是一种特殊的列表,只能在表的一端进行插入和删除操作,该一端称为栈顶。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
栈的使用
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
E push (E e) | 将e入栈,并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中的有效元素个数 |
boolean empty() | 检测栈是否为空 |
模拟实现
import java.util.Arrays;
public class MyStack {
private int[] elem;
private int usedSize;
private static final int DEFAULT_CAPACITY = 10;
public MyStack(){
this.elem=new int[DEFAULT_CAPACITY];
}
//判断是否为满
private boolean isFull(){
return usedSize == elem.length;
}
//判空
private boolean isEmpty(){
return usedSize == 0;
}
//入栈
public void push(int val){
if (isFull()){
System.out.println("栈已经满了,先进行扩容");
elem = Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize++] = val;
}
//出栈
public int pop(){
if (isEmpty()){
throw new EmptyException();
}
int val = this.elem[usedSize-1];
usedSize--;
return val;
}
//获取栈顶元素
public int peek(){
if (isEmpty()){
throw new EmptyException();
}
return this.elem[usedSize-1];
}
//栈的大小
public int size(){
return usedSize;
}
//清空
public void clear(){
for (int i = 0; i <= usedSize; i++) {
elem[i] = 0;
}
usedSize = 0;
}
}
应用场景
浏览器的前进和后退功能
浏览器的前进和后退功能可以使用两个栈来实现。其中,一个栈保存已访问的页面,当用户点击后退按钮时,将当前页面压入另一个栈中;当用户点击前进按钮时,将已经后退的页面从另一个栈中弹出并压入第一个栈中。
函数调用
栈可用于实现函数调用的调用栈。当一个函数调用另一个函数时,当前函数的状态(包括局部变量和返回地址)将被保存在栈中,直到被调用的函数执行完毕后,栈将弹出该函数的状态,从而返回到调用函数。
撤销操作
在文本编辑器中,可以使用栈来实现撤销操作。每当用户进行一次操作时,如插入、删除或替换文本,当前文本的状态将被保存在栈中,当用户点击撤销按钮时,将弹出最近的状态,从而恢复到之前的文本状态。
括号匹配
栈可以用于检查括号是否匹配。遍历字符串,当遇到左括号时,将其推入栈中,当遇到右括号时,与栈顶元素进行匹配。如果匹配成功,则将栈顶元素弹出,否则括号不匹配。如果最终栈为空,则表示所有括号都匹配。
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.empty()){
return false;
}
char top = stack.peek();
//此时top是左括号,ch是右括号
if(ch == ')' && top == '(' || ch =='}' && top == '{' || ch == ']' && top =='['){
stack.pop();
}else{
return false;
}
}
}
return stack.empty();
}
}
只要是左括号就入栈,只要是右括号,就开始匹配
- 和栈顶不匹配
- 栈为空
- 栈不为空,但是字符串遍历完了
表达式求值
栈常用于处理表达式求值。例如,当计算中缀表达式时,我们可以使用两个栈,一个用于操作符,一个用于操作数,通过遍历表达式并根据运算规则进行操作,最终得到表达式的结果。
逆波兰表达式求值
在此之前,我们必须知道什么叫做逆波兰表达式~
这里也不得不提一嘴关于计算机里加减乘除的栈的相关操作了
首先,我们可以将待计算的表达式从左到右逐个字符地读取。遇到数字时,我们将其转换为相应的数值,并将其压入栈中。例如,表达式"23+4",我们先将数字2和3分别转换成数值2和3,然后将它们依次压入栈中。
当遇到运算符号(如加号、减号、乘号或除号)时,我们需要从栈中连续弹出两个操作数进行运算,并将运算结果压入栈中。取出的第一个弹出的操作数是运算符号右侧的操作数,而第二个弹出的操作数是运算符号左侧的操作数。
例如,对于表达式"23+4",当遇到加号时,我们需要从栈中弹出操作数2和3,然后执行2+3的计算,将计算结果5压入栈中。
对于减号、乘号和除号,也是类似的操作。例如,减法运算时,从栈中弹出操作数2和3,计算2-3,并将结果(-1)压入栈中。
在处理完所有运算符之后,栈中最后剩下的元素即为表达式的最终计算结果。
总结起来,栈在计算器中的加减乘除运算过程中的作用主要是用来存储待计算的操作数,并在遇到运算符时执行相应的计算操作。通过栈的后进先出的特性,可以很方便地实现计算器中的运算功能。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String s : tokens){
if(!isOperation(s)){
//数字字符
stack.push(Integer.parseInt(s));
}else{
//有可能是加减乘除当中的其中一个运算符
int num2 = stack.pop();
int num1 = stack.pop();
switch(s){
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
}
}
}
return stack.pop();
}
private boolean isOperation(String s){
if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")){
return true;
}
return false;
}
}
出栈入栈次序匹配
思路:
- 定义一个辅助栈,用于模拟栈的压入和弹出操作。
- 遍历第一个序列(压入序列),依次将数字压入辅助栈中。
- 每次压入后,检查辅助栈栈顶元素是否与第二个序列(弹出序列)的当前元素相等。
- 如果相等,则将辅助栈栈顶元素弹出,同时将第二个序列的指针后移一位。
- 如果不相等,则继续将第一个序列的下一个元素压入辅助栈中,重复上述步骤。
- 最后,若辅助栈为空且第二个序列的指针已经遍历到末尾,则说明第二个序列是第一个序列的一个弹出顺序,返回true;否则,返回false。
import java.util.*;
public class Solution {
public boolean IsPopOrder (int[] pushV, int[] popV) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for(int i = 0; i < pushV.length; i++){
stack.push(pushV[i]);
while(!stack.empty() && j < popV.length && stack.peek() == popV[j]){
stack.pop();
j++;
}
}
return stack.empty();
}
}
最小栈
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if(minStack.empty()){
minStack.push(val);
}else{
if(val <= minStack.peek()){
minStack.push(val);
}
}
}
public void pop() {
if(!stack.empty()){
int ret = stack.pop();
if(minStack.peek() == ret){
minStack.pop();
}
}
}
//获取正常栈顶元素
public int top() {
if(stack.empty()){
return -1;
}
return stack.peek();
}
//获取最小栈
public int getMin() {
if(minStack.empty()){
return -1;
}
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
概念区分
- 栈(Stack):栈是一种数据结构,它是一种具有后进先出(Last-In-First-Out)特性的线性数据结构。栈的基本操作包括压栈(push)、弹栈(pop)、获取栈顶元素(peek)等。栈可以在内存中的任意位置实现,常用的实现方式有数组和链表。栈可以用于解决一些与顺序有关的问题,如函数调用、表达式求值等。
- 虚拟机栈(Java虚拟机栈):虚拟机栈是Java虚拟机在执行Java程序时使用的内存区域之一。每个线程在运行时都会创建一个对应的虚拟机栈,用于存储方法调用和方法执行过程中的局部变量表、操作数栈、方法返回值等信息。每个方法在虚拟机栈中对应一个栈帧(Stack Frame),方法的调用和返回都会对应着栈帧的入栈和出栈操作。
- 栈帧(Stack Frame):栈帧是虚拟机栈中的一个元素,它用于存储一个方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。每个方法在虚拟机栈中都会对应一个栈帧,栈帧的创建和销毁遵循方法的调用和返回过程。栈帧中的局部变量表和操作数栈是方法执行过程中需要用到的临时数据的存储区域。
至此,关于栈的介绍就到此为止,接下来会持续输出文章,敬请期待~