系列文章目录
集合及数据结构第八节(上)————栈(Stack)、栈的模拟实现和应用
栈(Stack)、栈的模拟实现和应用(上)
- 栈的概念
- 栈的使用
- 栈的模拟实现
- 栈的应用场景
- 栈、虚拟机栈、栈帧的概念区分
文章目录
一、栈(Stack)
1.栈的概念
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈
顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
栈在现实生活中的例子:
最先打出来的子弹总是最后装的
最先吃到的羊肉一定是最后串进去的
2.栈的使用( * * )
import java.util.Stack;
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());
}
}
3.栈的模拟实现( * * * )
接口的定义
public interface IStack {
void push(int x);//存入数据
int pop ();//将e入栈,并返回e
int peek();//获取栈顶元素
int size();//获取栈中有效元素个数
boolean empty();//检测栈是否为空
boolean full();//检测栈是否放满了,不属于栈的方法;
}
栈的相关内容的定义
private int[] elem;
private int usedsize;
private static final int DEFAULT_CAPACITY = 10;//默认初始化栈的大小为10
public Mystack(int[] elem) {
this.elem = new int[DEFAULT_CAPACITY];
}
接口的实现
public class Mystack implements IStack{
@Override
public void push(int x) {//构造一个空的栈
}
@Override
public int pop() {//将e入栈,并返回e
return 0;
}
@Override
public int peek() {//获取栈顶元素
return 0;
}
@Override
public int size() {//获取栈中有效元素个数
return 0;
}
@Override
public boolean empty() {//检测栈是否为空
return false;
}
}
存入数据
public void push(int x) {//存入数据
if (full()){//如果存满了,自动扩容
elem = Arrays.copyOf(elem,2 * elem.length)
}
elem[usedsize] = x;
usedsize++;
}
将e入栈,并返回e
public int pop() {//将e入栈,并返回e
if(empty()){//如果栈为空,抛出异常
throw new StackEmptyException("栈是空的!!!");
}
int ret = elem[usedsize - 1];
usedsize--;//表示可以存放数据元素的下标向前移动,下次存放数时就会覆盖这个数据。
//usedsize = null;//当数据为引用类型时,要置空。
return ret;
}
获取栈顶元素
public int peek() {//获取栈顶元素
if(empty()){//如果栈为空,抛出异常
throw new StackEmptyException("栈是空的!!!");
}
return elem[usedsize - 1];
}
获取栈中有效元素个数
public int size() {//获取栈中有效元素个数
return usedsize;
}
检测栈是否为空
public boolean empty() {//检测栈是否为空
return usedsize == 0;
}
4.栈的应用场景
改变元素的序列
-
若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是( C )
A: 1,4,3,2
B: 2,3,4,1
C: 3,1,4,2
D: 3,4,2,1 -
一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( B )。
A: 12345ABCDE B: EDCBA54321 C: ABCDE12345 D: 54321EDCBA
将递归转化为循环
逆序打印链表
// 1. 递归方式
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 + " ");
}
}
括号匹配
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
思路:
只需要解决括号不匹配,左括号多,右括号多这三种情况。
- 左括号入栈
- 遇到右括号的时候判断与栈顶元素的左括号是不是匹配的
- 当栈里的元素为且字符串遍历完了,说明是匹配的
- 当字符串遍历完成,但是栈中仍然存在元素,那么说明是左括号多的情况。
- 当字符串还没有遍历完成,遇到了右括号,但是栈中没有元素了。这种情况是右括号多。
代码实现:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();//新建一个栈用来存放左括号
//1.遍历字符串
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);//ch表示右括号
//2.判断是不是左括号
if (ch == '{' || ch == '[' || ch == '(') {//是左括号
//3. 把左括号放到栈里
stack.push(ch);
} else {//不是左括号
if(stack.empty()){//如果栈为空说明没有左括号,不匹配或者栈的里没有元素了但是字符串还没有遍历完
return false;//6.右括号多,返回false
}
char ch1 = stack.peek();//ch1表示左括号
//4.判断左括号是否和右括号匹配(匹配就把左括号移出栈)
if((ch1 == '{' && ch == '}') || (ch1 == '[' && ch == ']') || (ch1 == '(' && ch == ')')){
stack.pop();//移出左括号
}else {//不匹配返回false
return false;
}
}
}
if (!stack.empty()){//当字符串遍历完了但是栈不为空
return false;//5.左括号多,返回false
}
return true;
}
}
逆波兰表达式求值
什么叫做逆波兰表达式:
逻辑提问式类似于算术表达式,对于检索而言,这种表达式并不是最优和最简洁的形式,需要进行必要的转换。
1929年波兰的逻辑学家卢卡西维兹(Jan Lucasiewicz)提出了将运算符放在运算项后面的逻辑表达式,又称“逆波兰表达式”。
采用这种表达式组织逻辑提问式非常方便检索运算,是日本的福岛先生最早将逆波兰表达式应用于情报检索的,故又称为“福岛方法”。
逆波兰表达式又叫做后缀表达式,是一种没有括号,并严格遵循“从左到右”运算的后缀式表达方法,如下表所示:
思考:
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
思路:
- 遍历这个后缀表达式遇到数字就存到栈中
- 遇到操纵符就依此取出栈中的两个数按照顺序将这两个数分别置于操纵符的右边和左边进行计算
- 计算结果再放进栈中,遍历完这个表达式后栈里的数就是这个后缀表达式的算术表达式的结果
代码实现
class Solution {
private boolean isOperation(String x){//判断是不是操作符
if(x.equals("+") || x.equals("-") || x.equals("*") || x.equals("/")){
return true;
}
return false;
}
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String x: tokens) {
if (!isOperation(x)){//字符串x是数字
stack.push(Integer.parseInt(x));//Integer.parseInt(x)把字符x转换从整形并存进stack栈中
}else {
int num2 = stack.pop();//从栈中取出一个数存进num2(右值)
int num1 = stack.pop();//从栈中再取出一个数存进num1(左值)
switch (x){
case "+":
stack.push(num1 + num2);//num1 + num2 的值再放入栈中
break;
case "-":
stack.push(num1 - num2);//num1 - num2 的值再放入栈中
break;
case "*" :
stack.push(num1 * num2);//num1 * num2 的值再放入栈中
break;
case "/":
stack.push(num1 / num2);//num1 / num2 的值再放入栈中
break;
}
}
}
return stack.pop();
}
}
出栈入栈次序匹配
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
- 0<=pushV.length == popV.length <=1000
- -1000<=pushV[i]<=1000
- pushV 的所有数字均不相同
示例1
输入:[1,2,3,4,5],[4,5,3,2,1]
返回值:true
说明:可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=>pop()=>pop()=>pop()=>pop()
这样的顺序得到[4,5,3,2,1]这个序列,返回true
示例2
输入:[1,2,3,4,5],[4,5,3,2,1]
返回值:true
说明:可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=>pop()=>pop()=>pop()=>pop()
这样的顺序得到[4,5,3,2,1]这个序列,返回true
思路:
- 遍历push数组,把元素放入栈中
- 每放一个元素,就 与pop数组中的元素比较
- 如果相等pop数组向后移动一位,并且让栈中的元素出栈
- 如果不相等,想办法入栈
代码实现:
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++) {//遍历push数组
stack.push(pushV[i]);//将push中的数组放进栈中
while (!stack.empty() && j < popV.length &&
stack.peek() == popV[j]) { //只有栈不为空且pop数组没有遍历完时,
stack.pop(); //如果栈顶的元素与pop数组j下标的元素相等,该元素出栈
j++;
}
}
return stack.empty();//栈为空时,说明pop数组是该压栈序列对应的一个弹出序列(不为空说明不是)
}
}
最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
示例 1:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
push思路
- push的时候普通的栈一定要放元素
- 最小栈如果为空也要放。如果不为空,当要放的元素小于最小栈栈顶的元素,那么也要放最小栈中
pop思路
- pop的时候要让普通栈栈顶元素与最小栈元素比较
- 如果pop的元素和栈顶的元素一样,那么两个栈的栈顶元素都要移出去
- 不一样只出普通栈的就可以
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);//push的时候普通的栈一定要放元素
if (minStack.empty()){//最小栈如果为空也要放
minStack.push(val);
}else {//最小栈不为空,判断要放的元素是否小于最小栈栈顶的元素
int peekval = minStack.peek();//用来存放最小栈栈顶的元素
if (val <= peekval){//当要放的元素小于等于最小栈栈顶的元素,那么也要放最小栈中
minStack.push(val);
}
}
}
public void pop() {
int val = stack.pop();//把要移出去普通栈的栈顶元素放进val用来与最小栈栈顶元素比较
if (!minStack.empty()){//当最小栈不为空时,把要移出去普通栈的栈顶元素与最小栈栈顶元素比较
if (val == minStack.peek() ){//要移出去普通栈的栈顶元素与最小栈栈顶元素相等时,移出最小栈栈顶元素
minStack.pop();
}
}
}
public int top() {//获取普通栈的栈顶元素
return stack.peek();
}
public int getMin() {//获取最小栈的栈顶元素(通过这个方法来获取最小值)
if (minStack != null){//当最小栈不为空时,返回最小的元素
return minStack.peek();
}
return -1;//否则返回-1
}
}
5. 栈、虚拟机栈、栈帧的概念区分( * * * )
栈、虚拟机栈、栈帧有什么区别呢?
栈:
栈(stack)是一种数据结构,具有“后进先出”(LIFO)的特点,也就是说,最后一个入栈的元素最先出栈。栈通常用于函数调用、表达式求值、括号匹配等场景中。
虚拟机栈:
虚拟机栈(virtual machine stack)是一种特殊的栈,是JVM划分的一块内存栈。用于存储方法调用的信息、局部变量表、操作数栈、返回值等。在Java虚拟机中,每个线程都有一个独立的虚拟机栈。
栈帧:(stack frame)是虚拟机栈中的一个元素,用于存储方法调用时所需的各种数据。每个方法调用都会在虚拟机栈中创建一个栈帧(开辟一块内存),并将其压入栈顶。