【JAVA数据结构系列】01_栈详解

一、栈

1、栈的定义与功能

  • 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
  • 栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
  • 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
  • 出栈:栈的删除操作叫做出栈。出数据在栈顶。

在这里插入图片描述

方法功能
Stack()构造一个空的栈
E.push(E e)将e入栈,并返回e
E.pop()将栈顶元素出栈并返回
E.peek()获取栈顶元素
int size()获取栈中有效元素个数
boolean empty()检测栈是否为空
public class TestDemo {
    public static void main(String[] args) {
        Stack<Integer> stack=new Stack<>();
        stack.push(12);
        stack.push(24);
        stack.push(48);
        stack.push(36);
        int val=stack.peek();//获取栈顶元素但不删除
        System.out.println("栈顶元素是:"+val);//36

        int val2=stack.pop();//删除栈顶元素
        System.out.println("被删除的栈顶元素是:"+val2);

        if(stack.empty()){
            System.out.println("栈空");
        }else{
            System.out.println("栈的有效元素个数是:"+stack.size());//获取栈中有效的元素个数
        }
    }
}

2、模拟实现一个栈

在这里插入图片描述
从上图中可以看到, Stack继承了Vector ,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安 全的。

【1】、源码中的capacityIncrement

扩容的源码中capacityIncrement的意思:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

【2】、初始化栈

  • 1、用一个数组代表栈,这里选择元素为基本数据类型的数组。
  • 2、用一个usedSize变量代表当前存储的有效的数据个数,也可以表示当前可以存放的数据元素的下标。
  • 3、设置一个栈的初始大小DEFAULT_CAPACITY
  • 4、设置一个构造方法可以初始化数组。
public class MyStack {
    /**
     * usedSize:表示当前存储的有效的数据个数,
     * 也可以表示当前可以存放的数据元素的下标
     */
    public int[] elem;
    public int usedSize;
    public static final int DEFAULT_CAPACITY=10;
    //初始栈的大小,类似于源码当中的capacityIncrement

    public MyStack(){
        elem=new int[DEFAULT_CAPACITY];
    }

}

【3】、压栈

  • 1、压栈之前要判断栈是否满了
  • 2、要设置一个栈的初始大小,才能比较栈是否满了
  • 3、栈满了,就要进行二倍扩容,使用Arrays.copyOf
    /**
     * 判断栈当前是否是满的
     * @return
     */
    public boolean isFull(){
        if(usedSize==elem.length){
            return true;
        }
        return false;
    }
    /**
     * 压栈
     * @param val 要入栈的元素值
     */
    public void push(int val){
        /**
        * 1、isFull()判断栈是否是满的
        * */
        if(isFull()){
            //栈满了,就进行二倍扩容
            elem=Arrays.copyOf(elem,2*elem.length);
        }
        //存放到当前下标,同时usedSize需要自增
        elem[usedSize]=val;
        usedSize++;
    }

【4】、出栈

如果元素是引用数据类型的出栈,则思路为:

elem[usedSize-1]=null;
usedSize--;

但由于我们定义的数组的元素是int类型,因此思路为:
直接让usedSize--,这样新的数据添加进来就会覆盖应该被删除的数据。

  • 1、在出栈之前也要判断栈是否为空
  • 2、如果栈为空,则抛出一个异常
/**
     * 判断是否为空
     * @return
     */
    public boolean isEmpty(){
        return usedSize==0;
    }

重新new一个class,写一个异常:

//运行时异常,则继承RuntimeException
public class EmptyStackException extends RuntimeException{
    //加上构造方法
    public EmptyStackException(){
        
    }    
    public EmptyStackException(String msg){
        super(msg);
    }
}
/**
     * 删除栈顶元素,
     * 分析:这里不是引用数据类型,而是基本数据类型
     * 删除就是直接让usedSize--,这样新的数据添加进来就会覆盖应该被删除的数据
     * @return
     */
    public int pop(){
        if(isEmpty()){
            throw new EmptyStackException("栈为空了!");
        }
        int oldVal=elem[usedSize-1];
        usedSize--;
        return oldVal;
    }

【5】、获取栈顶元素

  • 1、判断是否为栈是否为空
  • 2、如果栈不空的话就返回栈顶元素的值
    public int peek(){
        if(isEmpty()){
            throw new EmptyStackException("栈为空了,无法获取栈顶元素");
        }
        return elem[usedSize-1];
    }

【6】获取元素个数

    public int getUsedSize(){
        return usedSize;
    }

【7】源码

源码一:

import java.util.*;
/**
 * @author Susie-Wen
 * @version 1.0
 * @description:模拟实现一个栈
 * @date 2022/7/6 19:38
 */
public class MyStack {
    /**
     * usedSize:表示当前存储的有效的数据个数,
     * 也可以表示当前可以存放的数据元素的下标
     */
    public int[] elem;
    public int usedSize;
    public static final int DEFAULT_CAPACITY=10;
    //初始栈的大小,类似于源码当中的capacityIncrement

    public MyStack(){
        elem=new int[DEFAULT_CAPACITY];
    }

    /**
     * 压栈
     * @param val 要入栈的元素值
     */
    public void push(int val){
        /**
        * 1、isFull()判断栈是否是满的
        * */
        if(isFull()){
            //栈满了,就进行二倍扩容
            elem=Arrays.copyOf(elem,2*elem.length);
        }
        //存放到当前下标,同时usedSize需要自增
        elem[usedSize]=val;
        usedSize++;
    }

    /**
     * 判断栈当前是否是满的
     * @return
     */
    public boolean isFull(){
        if(usedSize==elem.length){
            return true;
        }
        return false;
    }

    /**
     * 删除栈顶元素,
     * 分析:这里不是引用数据类型,而是基本数据类型
     * 删除就是直接让usedSize--,这样新的数据添加进来就会覆盖应该被删除的数据
     * @return
     */
    public int pop(){
        if(isEmpty()){
            throw new EmptyStackException("栈为空了!");
        }
        int oldVal=elem[usedSize-1];
        usedSize--;
        return oldVal;
    }
    /**
     * 判断是否为空
     * @return
     */
    public boolean isEmpty(){
        return usedSize==0;
    }
    public int peek(){
        if(isEmpty()){
            throw new EmptyStackException("栈为空了,无法获取栈顶元素");
        }
        return elem[usedSize-1];
    }
    public int getUsedSize(){
        return usedSize;
    }
}

源码二:

public 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); }
}
}

3、栈插入的时空复杂度

栈的底层就是一个数组。

1、对于栈的顺序存储:
入栈的时间复杂度:O(1)
出栈的时间复杂度:O(1)

2、假设单向链表尾插法插入数据:
入栈->尾插法:O(N)
出栈->尾插法:O(N)

3、假设单向链表头插法插入数据:
入栈->头插法:O(1)从头结点插入,不需要遍历列表
出栈->头插法:O(1)删除头结点
因此如果栈使用单向链表的形式存储,则需要使用头插法。

4、假设双向链表尾插法插入数据:
入栈->尾插法:O(1)
出栈->尾插法:O(1)

5、假设双向链表头插法插入数据:
入栈->尾插法:O(1)
出栈->尾插法:O(1)

4、例题

【1】、不可能的出栈顺序

有一道题目:一个栈的输入顺序是ABCDEF,那么不可能出现的出栈顺序是什么()

A、DCBAEF

B、ACBEDF

C、DEFBCA

D、CDBAFE

这道题的答案是C。分析:这些题一般都默认在进栈的过程当中可以出栈。

【2】、中缀转后缀表达式

1、中缀表达式转后缀表达式。

中缀表达式:a+b*c+(d*e+f)*g

整个后缀表达式当中没有括号,但是仍然可以区分优先级。

例题一:

中缀表达式:2+(3*5)
后缀表达式:235*+

例题二:

中缀表达式:(2+3)*5
后缀表达式:23+5*

做法请直接点击这个链接《《

在这里插入图片描述
在这里插入图片描述

【3】、括号匹配

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

  • 有效字符串需满足:
    左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。

  • 示例 1:
    输入:s = “()” 输出:true

  • 示例 2:
    输入:s = “()[]{}” 输出:true

  • 示例 3:
    输入:s = “(]” 输出:false

  • 示例 4:
    输入:s = “([)]” 输出:false

  • 示例 5:
    输入:s = “{[]}” 输出:true

  • 提示:
    仅由括号 ‘()[]{}’ 组成

分析:
1、可能出现的情况

  • (【)】:左右字符不匹配
  • ()):右括号多
  • (():左括号多

2、使用栈来解决

如果是左括号就入栈,右括号就出栈。

当出栈的时候可能出现的情况:

【1】当匹配成功: 满足以下两点

  • 1、字符串遍历完成
  • 2、栈为空

【2】当不匹配的情况:

  • 1、右括号多:当前下标是右括号,但是栈为空
  • 2、左括号多:字符串遍历完成,但是栈仍然不为空
解法一
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();
                if(ch==')' && top=='('  || ch==']' && top=='[' || ch=='}' && top=='{'){
                    stack.pop();//说明当前的左右括号匹配,因此左括号出栈
                }else{
                        return false;//说明左右括号不匹配
                }
            }
        }

        if(stack.empty()){
            return true;
        }else{
            return false;//左括号多
        }
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

在这里插入图片描述

解法二

分析:解法一和解法二都是使用栈,但是解法二看上去更加简洁,因为解法二是将所有的右括号访入栈当中,而解法一是将所有的左括号进栈。

class Solution {
    public boolean isValid(String s) {
        Stack<Character>stack = new Stack<Character>();
        for(char c: s.toCharArray()){
            if(c=='(')stack.push(')');
            else if(c=='[')stack.push(']');
            else if(c=='{')stack.push('}');
            else if(stack.isEmpty()||c!=stack.pop())return false;
        }
        return stack.isEmpty();
    }
}

ToCharArray( )的用法,将字符串对象中的字符转换为一个字符数组。
详解释就是:字符串转换成字符数组后,每个字符的ASC码与字符T的ASC码进行二进制异或运算。

在这里插入图片描述

解法三
class Solution {
    public boolean isValid(String s) {
        int length = s.length() / 2;
		for (int i = 0; i < length; i++) {
			s = s.replace("()", "").replace("{}", "").replace("[]", "");
		}

		return s.length() == 0;
    }
}

在这里插入图片描述

分析:这个思路很好,不需要栈的复杂的解法,使用的是java当中的方法replace

为了搞清楚规律,我随便测试了一组数据:

public class Test01 {
    public static void main(String[] args) {
        String s="(}(([[[((]]]()[[[]]))){{{{}}}";
        System.out.println("原始的s:"+s);
        s=s.replace("()","");
        System.out.println("新的替换了()的s:"+s);
        s=s.replace("[]","");
        System.out.println("新的替换了[]的s:"+s);
        s=s.replace("{}","");
        System.out.println("新的替换了{}的s:"+s);
    }
}

在这里插入图片描述
在这里插入图片描述
从上面的执行结果可以看出,每次replace都会将一组满足条件的括号删除,因此可以考虑写一个循环。

  • 循环只要变量字符串的前一半字符就可以了,因为如果括号匹配的话,那么一半刚好是所有左括号。

【4】、递归实现单链表逆序打印

     /**
     * 递归实现单链表逆序打印
     * @param head
     */
    public void printList(ListNode head){
        if(head==null){
        //单链表无元素
            return;
        }
        if(head.next==null){
        //说明此时单链表只有一个元素
            System.out.println(head.val+" ");
            return;
        }
        //单链表至少两个结点
        printList(head.next);//循环结束之后从这里继续执行
        System.out.println(head.val+" ");
    }

在这里插入图片描述

【5】非递归实现单链表逆序打印

上面的递归其实就是模拟栈的实现,因此非递归的做法就是使用栈来逆序打印单链表:

  • 利用了栈的先进后出的特点
    /**
     * 非递归实现单链表逆序打印
     */
    public void printList2(){
        Stack<LinkNoed> Stack=new Stack<>();
        LinkNode cur=head;
        while(cur!=null){
            stack.push(cur);
            cur=cur.next;
        }
        while(!stack.empty()){
            LinkNode top=stack.pop();
            System.out.println(top.val+" ");
        }
    }

两种方式的源码:

// 递归方式
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 + " ");
	 }
}

【5】、逆波兰表达式求值

题目》》

根据 逆波兰表示法,求表达式的值。

  • 有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
    注意 两个整数之间的除法只保留整数部分。
    可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
  • 示例 1:
    输入:tokens = [“2”,“1”,“+”,“3”,“*”] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) *3) = 9
  • 示例 2:
    输入:tokens = [“4”,“13”,“5”,“/”,“+”] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13/ 5)) = 6
  • 示例 3:
    输入:tokens = [“10”,“6”,“9”,“3”,“+”,“-11”,““,”/“,””,“17”,“+”,“5”,“+”]
    输出:22
    解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
    = ((10 * (6 / (12 * -11))) + 17) + 5
    = ((10 * (6 / -132)) + 17) + 5
    = ((10 * 0) + 17) + 5
    = (0 + 17) + 5
    = 17 + 5
    = 22

思路:

  • 第一个数组是push数组,表示入栈次序。第二个数组是pop数组,表示出栈序列。
  • 定义两个变量i和j,分别遍历push和pop数组,push依次入栈,如果i和j对应的数字相同,则把栈当中的元素出栈,否则就让push入栈。
  • 结果:当j遍历完了数组,并且栈也是空的,则说明pop数组就是push数组的出栈序列。
  • 结果:当j遍历完了数组,但是栈里面仍然存在元素,则pop数组不是push数组的出栈序列。
  • 定义一个方法isOperation来判断是否是除了加减乘除之外的有效字符。
  • 由于是字符串比较因此必须使用equals
  • 最开始定义栈的时候,栈里面的元素要变成Integer,因为最后要把数字相加减,而不要定义成String

注意下列代码当中的num1和num2的顺序不能改变,因为num2先被pop出来。此外,自定义函数如果是加减乘除符号的话return的是true而非false。
在这里插入图片描述

class Solution {
    public int evalRPN(String[] tokens) {
            Stack<Integer> stack=new Stack<>();
            for(String x:tokens){
                //要把所有的数字全都放入数字当中
                if(!isOperation(x)){
                //!isOperation(x):表示字符不是加减乘除
                    stack.push(Integer.parseInt(x));//把字符串转变成整数再放到栈中
                }else{
                    //弹出两个元素
                    int num2=stack.pop();
                    int num1=stack.pop();
                    switch(x){
                        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();
    }
	//定义一个方法来判断是否是除了加减乘除之外的有效字符
	//由于是字符串比较因此必须使用equals
    private boolean isOperation(String opera){
        if(opera.equals("+") || opera.equals("-") || opera.equals("*") || opera.equals("/")){
            return true;
        }
        return false;
    }

}

【6】、栈的压入、弹出序列

题目《《

描述

  • 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
  1. 0<=pushV.length == popV.length <=1000
  2. -1000<=pushV[i]<=1000
  3. 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,3,5,1,2]
    返回值: false
    说明:
    由于是[1,2,3,4,5]的压入顺序,[4,3,5,1,2]的弹出顺序,要求4,3,5必须在1,2前压入,且1,2不能弹出,但是这样压入的顺序,1又不能在2之前弹出,所以无法形成的,返回false
import java.util.*;
import java.util.ArrayList;

public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        if(pushA.length==0 || popA.length==0){
            return false;
        }
            
        Stack<Integer> stack=new Stack<>();
        int j=0;
        for(int i=0;i<pushA.length;i++){
            stack.push(pushA[i]);
            while(j<popA.length && !stack.empty() &&
                  stack.peek()==popA[j]){//把栈里面一样的都弹出去
                //这里如果是引用类型则不能使用==,要使用equals
                stack.pop();
                j++;
            }
        }
        
        return stack.empty();
    }
}

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温欣2030

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值