1 .概念及特性
栈
:是一种特殊
的线性表结构;
特殊性表现在:它只允许在固定的一端进行数据的插入与删除操作,将能进行数据操作的一端称为栈顶,而另一端称为栈底。
压栈
:栈的插入操作叫做进栈/压栈/入栈;
出栈
:栈的删除操作叫做出栈;
注: 出入数据都是在栈顶;
栈在生活中实例:
最后放的羽毛球最先会被拿出来
栈的特性:遵循 后进先出 的原则,LIFO(Last In First Out);
大神形容
:吃进去吐出来
2.常见方法
代码如下:
package day20211018;
import java.util.Stack;
public class TestStack {
// 测试栈
public static void main(String[] args) {
//Stack 是一个类
Stack<Integer> s=new Stack();
// 压栈:1 2 3 4
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s); //[1, 2, 3, 4]
// 获取栈顶元素
System.out.println(s.peek()); //4
//获取栈中有效元素的个数
System.out.println(s.size()); //4
// 出栈 :两个元素
s.pop();
s.pop();
System.out.println(s); //[1, 2]
//判断栈是否为空
if(s.empty()) {
System.out.println("栈为空");
}else{
System.out.println("栈不为空");
}
}
}
3.栈的应用
改变元素的序列
一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )。
A: 12345ABCDE
B: EDCBA54321
C: ABCDE12345
D: 54321EDCBA
解析:
根据 栈
的特性:后进先出,所以选择 D
将递归转化为循环
// 递归方式
public void reversePrintList(Node node){
if(null = node){
return ;
}
reversePrintList(node.next);
System.out.print(node.val + " ");
}
}
// 循环方式
public void reversePrintList(Node node){
if(null == node){
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = node;
while(null != cur){
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
System.out.print(s.pop().val + " ");
}
}
思考
:
为什么使用栈将递归转化为循环,而没有采用其他结构呢?
主要原因:递归调用遵循的是,后调用的先退出,先调用的后退出,这种特性正好与栈的特性相匹配;但并不是所有的递归转化为循环时都需要栈,比如:求阶乘;
-
括号匹配
题目描述:给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
解题思路:
(1)对所给字符串依次进行遍历,然后拿到其中的每个元素(括号);
(2)对拿到的元素进行判断:
如果是左括号:入栈(push)
如果是右括号:与栈顶元素进行比对
注意:要保证栈中有元素
比对结果:
匹配:将栈顶元素进行出栈返回;
不匹配:返回 false
代码如下:
class Solution {
public boolean isValid(String s) {
//构造一个栈
Stack<Character> st=new Stack<>();
//1.遍历字符串
for(int i=0;i<s.length();i++){
//判断第i个元素是左括号还是右括号
char c=s.charAt(i); //获取i个字符
if(c =='(' || c == '[' || c == '{'){
// 获取到的元素为左括号---入栈
st.push(c);
}else{
//获取到的元素为右括号
//得判断栈中是否有元素
if(st.empty()){
return false;
}
//获取栈顶元素
char top=st.peek();
if((top == '(' && c == ')') ||
(top == '[' && c == ']') ||
(top == '{' && c == '}')){
st.pop();
continue;
}else{
return false;
}
}
}
//最后判断栈是否为空
//不空说明栈中还有元素
if(!st.empty()){
return false;
}else{
return true;
}
}
}
逆波兰表达式求值
逆波兰表达式又称为后缀表达式,所谓后缀表达式就是将运算符放置在操作数之后的一种方式;
假设
:要求(4-2)*(5+3)
的值,对于我们而言,可以借助括号来进行辅助运算,而对于计算机而言,是看不到括号的,因此,为了使计算机能够帮助我们求解更加复杂的数学问题,就引入了后缀表达式;
对于上述算式转成后缀表达式就变成了如下所示:
4 2 - 5 3 + *
接下来看问题描述:
LeetCode逆波兰表达式求值
根据逆波兰表示法,求表达式的值。
有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
解题思路:
(1)依次遍历表达式中的每一项,然后进行判断;
(2)如果是数字就入栈,是运算符,就依次取出栈顶的两个元素进行运算,最后将计算结果入栈;
(3)将栈顶的最终计算结果返回;
代码如下:
class Solution {
public int evalRPN(String[] tokens) {
//构造一个栈
Stack<Integer> s=new Stack<>();
//遍历表达式中的每一项
for(String e: tokens){
//进行判断
if(!((e.equals("+")) || (e.equals("-")) || (e.equals("*")) || (e.equals("/")))){
//此时该项为数字---入栈
//e是数字字符串,需转成数字,才能入栈
s.push(Integer.parseInt(e));
}else{
//为字符串
//从栈顶取出两个操作数进行运算
int right=s.pop(); //该数字为右操作数
int left=s.pop(); //该数字为左操作数
switch(e){
case "+":
s.push(left + right);
break;
case "-":
s.push(left - right);
break;
case "*":
s.push(left * right);
break;
case "/":
s.push(left / right);
break;
default:
break;
}
}
}
return s.peek();
}
}
出栈入栈次序匹配
题目描述: 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列
代码如下:
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack <Integer> s = new Stack();
int pushIdx = 0, outIdx = 0;
while(outIdx < popped.length){
//当栈空或者栈顶元素与待出元素不相等时候入栈
while(s.empty() || s.peek() != popped[outIdx]){
//有元素时,继续入栈
if(pushIdx < pushed.length){
s.push(pushIdx);
pushIdx++;
}else {
return false;
}
}
s.pop();
outIdx++;
}
return true;
}
}
4.栈的模拟实现
package day20211023;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack <E>{
//模拟实现栈
//删减版的顺序表
E[] array;
int size; //用来表示栈中元素的个数
//构造方法
public Stack(){
array=(E[]) new Object[10] ;
size=0; //没有元素时
}
//压栈--push
public E push(E e){
//确保栈空间足够
ensureCapacity();
array[size]=e;
size++;
return e;
}
//出栈
public E pop(){
//判断栈是否为空
if(empty()){
throw new RuntimeException("栈为空异常");
}
E ret=peek();
size--;
return ret;
}
//获取栈顶元素
public E peek(){
if(empty()){
throw new RuntimeException("栈为空异常");
}
return array[size-1];
}
//判断栈是否为空
public boolean empty(){
return 0==size;
}
//获取栈的大小
public int size(){
return size;
}
//栈容量
private void ensureCapacity(){
if(size==array.length){
int newCapacity=size*2;
array=Arrays.copyOf(array,newCapacity);
}
}
public static void main(String[] args) {
Stack<Integer> s=new Stack<>();
//一般不进行遍历操作
//压栈:1 2 3 4
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();
s.pop();
System.out.println(s.peek()); //2
if(s.empty()){
System.out.println("栈为空");
}else{
System.out.println("栈不为空");
}
}
}
区分四个概念— 栈 & 虚拟机栈 & 栈帧 & 操作数栈
栈:
栈:指的是一种后进先出的数据结构,在
Java
集合中有对应的实现–就是Stack
,Stack
在实现时继承了vector
。
虚拟机栈:
虚拟机栈(Java Virtual Machine Stacks)是一块具有特殊作用内存空间,它的生命周期与线程相同;而
JVM
为了更便于管理数据,将内存按照不同的需求划分有栈区、
堆区
;
栈区:线程是私有的,栈区中存放的是函数调用相关的一些信息,主要是栈帧,栈帧也是按照数据结构中栈的特性来进行实现的;当栈区中内存空间不足时,会抛出StackOverFLowException
异常信息。
堆区:一般new
出来的空间都在堆区;
栈帧:
栈帧(Stack Frame)是用于支持
Java
虚拟机进行方法调用和执行的数据结构,每个方法在执行时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,当方法调用结束时,该方法对应的栈帧会从虚拟机中出栈。
操作数栈:
操作数栈(Operand Stack)也被称为操作栈,它也是一个后进先出的(Last In First out,LIFO)栈。同局部变量表一样,操作数栈的最大深度在编译时就被确定下来了,操作数栈的每一个元素可以是任意的
Java
数据类型,包括long
和double
。
注意:
每个方法中栈帧结构是一样的,大小可能不同,栈帧的大小在程序编译时就已经确定好了大小
;