栈与stack

前言

今天接着和大家分享数据结构中栈相关的知识,特别是与java集合框架相关的内容,如果有了顺序表与链表的基础,接触今天分享的会是相当容易上手的,话不多说,让我们开始吧


一、java集合框架与Stack

  • Java 集合框架是 Java 中用于存储和操作一组对象的体系,核心分为 Collection(单列集合)和Map(双列集合)

核心接口与分类

  • Collection(单列集合)
    • 是所有单列集合的根接口,定义了集合的基本操作(增删改查、遍历等)。
    • 子接口:List(有序可重复)、Set(无序不可重复)、Queue(队列)。
  • Map(双列集合)
    • 存储键值对(Key-Value),Key 唯一、Value 可重复。
    • 子接口:SortedMap(键有序)。

Stack

Stack 的继承关系(从提供的类图看)

从类图可知:

Stack 继承自 Vector(属于 List 分支),而Vector又继承自AbstractList、实现了List接口。

因此Stack本质是基于动态数组实现的,同时继承了Vector的线程安全特性(方法被synchronized修饰)。

Stack 的核心特性

  • LIFO(后进先出):
    • 新元素从 "栈顶" 加入,取出元素也只能从栈顶操作(最后加入的元素最先被取出)。
    • 动态扩容:
      底层基于Vector的动态数组,容量不足时会自动扩容(默认扩容为原容量的 2 倍)。
    • 线程安全:
      由于继承自Vector,Stack的方法(如push、pop)都被synchronized修饰,多线程环境下不会出现并发问题,但性能较低。

二、啥是栈

数据结构 的角度来说,栈(Stack) 是一种遵循 后进先出(Last In First Out,简称 LIFO) 原则的线性数据结构 ,只允许在一端 进行元素的插入和删除操作,这个操作端被称为栈顶 ,另一端则被称为栈底

  • 就是说栈就是一种线性的数据结构,比如数组,链表.后面会具体说一下这两个的实现形式.
  • 其中是满足先进后出的原则,大家可以想象一下手枪弹夹装弹的场景.

核心概念

  1. 栈顶(Top)
    栈的操作端,所有的插入(入栈)、删除(出栈)、查看操作都只能在这一端进行。
  2. 栈底(Bottom)
    栈的固定端,元素从栈顶入栈后,会依次向栈底方向排列,栈底元素是最先入栈且最后出栈的元素。
  3. 空栈
    没有任何元素的栈,此时进行出栈或查看栈顶操作通常会触发异常。

核心操作

栈的操作非常简洁,只有两个核心基础操作,以及两个常用辅助操作:

操作名称 功能描述 注意事项
入栈(Push) 将元素添加到栈顶 若栈已满(顺序栈),会触发"栈溢出"
出栈(Pop) 移除并返回栈顶元素 若栈为空,会触发"下溢"异常
查看栈顶(Peek/Top) 返回栈顶元素,但不移除 若栈为空,触发异常
判空(IsEmpty) 判断栈中是否有元素

说明:栈不支持在中间位置插入、删除或访问元素,这是它区别于数组、链表等线性结构的关键特征。

栈的两种实现方式

从数据结构的底层存储来看,栈有两种常见实现方式:

  1. 顺序栈(基于数组实现)

    • 原理 :用一段连续的内存空间(数组)存储栈元素,用一个变量标记栈顶的位置(如 top 指针,初始值为 -1 表示空栈)。
    • 入栈top 指针加 1,将元素存入数组对应下标位置。
    • 出栈 :返回数组 top 下标对应的元素,top 指针减 1。
    • 优缺点:访问速度快,但存在固定容量限制,满栈后扩容需要额外开销。
    • 典型例子 :Java 中遗留的 Stack 类、ArrayDeque 类。
  2. 链式栈(基于链表实现)

    • 原理 :用链表存储栈元素,一般选用带头结点的单链表,栈顶对应链表的头结点之后的第一个节点(避免复杂的尾操作)。
    • 入栈:将新节点插入到链表头部(栈顶)。
    • 出栈:删除链表头部的节点,返回其值。
    • 优缺点:容量可以动态增长,不存在溢出问题,但每个元素需要额外存储指针,空间开销略大。
    • 典型例子:自定义链式栈结构。

三、常用代码手动实现(数组类型实现)

  • 这一部分的逻辑是较为简单的,小伙伴们如果是第一次接触,非常建议大家上手实现一下~
    我就都分成一个一个小的代码块了 大家在学习的时候也可以分成基本成员变量 ,成员方法,**辅助方法(在成员方法中被调用的小方法)**进行学习

基本方法

java 复制代码
public class MyStack<E> {
    public Object[] elem ;
    //标记最后一个
    public int usedSize;
    public static final int DEFAULT_CAPACITY = 5;
    public MyStack(){
        this.elem = new Object[DEFAULT_CAPACITY];
    }

    //入栈方法
    public void push(E val){
        if (isFull()){
           elem = Arrays.copyOf(elem,2*elem.length);
        }

        elem[usedSize] = val;
        usedSize ++;


    }

    public boolean isFull(){
        return usedSize == DEFAULT_CAPACITY;
    }

    public E pop(){
        if(isEmpty()){
            return null;
        }

        E ret = (E)elem[usedSize-1];
        usedSize--;
        return ret;


    }

    public boolean isEmpty(){
        return usedSize == 0;
    }


    public E peek(){
        if(isEmpty()){
            return null;
        }
        return (E)elem[usedSize-1];
    }

四、逆波兰表达式

逆波兰表达式 (Reverse Polish Notation,简称 RPN),也叫后缀表达式 ,是一种不需要括号来标识运算优先级的算术表达式表示方法。

它的核心特点是:运算符位于其对应的所有操作数之后

与之相对的,我们日常使用的 3 + 4(2 + 3) × 5 这类表达式叫中缀表达式------运算符位于两个操作数中间。

  • 中缀转后缀

一、 中缀表达式 vs 逆波兰表达式

通过表格可以更直观地理解两者的转换关系:

中缀表达式 逆波兰表达式 计算逻辑
3 + 4 3 4 + 先取 3 和 4,再执行加法
(2 + 3) × 5 2 3 + 5 × 先算 2+3,结果再和 5 相乘
3 + 4 × 2 3 4 2 × + 先算 4×2,结果再和 3 相加
(3 + 4) × (5 - 2) 3 4 + 5 2 - × 先算 3+4 和 5-2,再将两个结果相乘

二、 逆波兰表达式的计算规则

逆波兰表达式的计算非常适合用来实现,步骤如下:

  1. 从左到右扫描表达式中的每个元素(数字或运算符)。
  2. 遇到数字:直接压入栈中。
  3. 遇到运算符 :从栈中弹出两个元素 (注意顺序:先弹出的是右操作数 ,后弹出的是左操作数),用该运算符对两个数进行计算,再将计算结果压入栈中。
  4. 扫描结束后,栈中剩余的唯一元素就是表达式的最终结果。

四、 逆波兰表达式的优势

  1. 无需括号:完全通过运算符的位置来体现运算优先级,没有歧义。
  2. 计算高效 :用栈实现的计算过程时间复杂度为 O(n)O(n)O(n),逻辑简单,适合计算机处理。
  3. 编译器常用:很多编译器在处理算术表达式时,会先将中缀表达式转为逆波兰表达式,再进行计算,避免了复杂的括号和优先级判断。

五、根据逆波兰表达式求值

java 复制代码
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();

        for (String x: tokens){
            //判断是运算符号还是数值
            if(!isOperations(x)){
                //不是运算符
                stack.push(Integer.parseInt(x)) ;
            }else {
                //是运算符
                int right = stack.pop();
                int left = stack.pop();
                //直到具体的操作符
               switch (x){
                   case "+" :
                      stack.push(left + right);
                       break;
                   case "-" :
                       stack.push(left - right);
                       break;
                   case "*" :
                       stack.push(left * right);
                       break;
                   case "/" :
                       stack.push(left / right);
                       break;
               }
            }
        }
        return stack.pop();
    }

    public boolean isOperations(String x){
        if (x.equals("+") || x.equals("-") ||
                x.equals("*") || x.equals("/") ){
            return true;
        }
        return false;
    }

五、 总结

  • 虽然栈的方法不是很多,但是栈这种独特的思想在编程题目中有着不少的应用,大家可以找两个感受感受~

  • 到这里我的分享就先结束了~,希望对你有帮助

  • 我是dylan 下次见~

    • 无限进步
相关推荐
董世昌417 小时前
break和continue的区别是什么?
java·jvm·算法
Chase_______7 小时前
【JAVA基础指南(一)】快速掌握基础语法
java·开发语言
陈逸轩*^_^*7 小时前
微服务常见八股(分布式seat, 网关,服务注册与发现、负载均衡、断路器、API 网关、分布式配置中心)
java·微服务
爱笑的眼睛117 小时前
MLflow Tracking API:超越实验记录,构建可复现的机器学习工作流
java·人工智能·python·ai
lxh01137 小时前
复原IP地址
前端·数据结构·算法
好学且牛逼的马7 小时前
Apache Commons DbUtils
java·设计模式·apache
榮十一7 小时前
100道Java面试SQL题及答案
java·sql·面试
专注于大数据技术栈7 小时前
java学习--String
java·开发语言·学习
LYFlied7 小时前
【每日算法】LeetCode 20. 有效的括号
数据结构·算法·leetcode·面试