算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在用

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 栈是 后入先出(LIFO) 的数据结构,入栈通常使用 push 操作,往栈中插入数据到栈底,出栈使用 pop 操作,从栈顶删除数据。

image.png


公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。


在 LeetCode 上不知不觉已经刷了 210+ 题,总提交次数 1000+ 次,从这篇文章开始,每篇算法类型的文章,将会做成动画的形式,每篇文章都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度、空间复杂度和源代码,更多内容点击下方链接前去查看。



通过这篇文章你将学习到以下内容:


  • 栈的定义
  • 栈的实现
  • 为什么不推荐使用 Java 栈
  • 性能低
  • 破坏了原有的数据结构
  • 不推荐使用了,为什么现在还在用
  • 为什么推荐使用 Deque 接口替换栈
  • 效率比 Java 栈快
  • 屏蔽掉无关的方法
  • Stack 和 ArrayDeque 区别
  • 栈的时间复杂度
  • 栈的应用:有效的括号


栈的定义



栈是 后入先出(LIFO) 的数据结构,入栈通常使用 push 操作,往栈中插入数据到栈底,出栈使用 pop 操作,从栈顶删除数据。入栈和出栈操作动画如下所示。


image.png


栈的实现



栈常用的实现方式是通过动态数组来实现的,在 Java 和 Kotlin 中也内置了栈库 Stack,但是 Stack 已经不推荐使用了。


为什么不推荐使用


  • 性能低


性能低是因为 Stack 继承自 Vector, 而 Vector 在每个方法中都加了锁,如下所示:


......
public synchronized void trimToSize() { }
public synchronized void ensureCapacity(int minCapacity) {  }
public synchronized void setSize(int newSize) {  }
public synchronized int capacity() {  }
public synchronized int size() {  }
public synchronized boolean isEmpty() {  }
......


由于需要兼容老的项目,很难在原有的基础上进行优化,因此 Vector 就被淘汰掉了,使用 ArrayListCopyOnWriteArrayList 来代替,如果在非线程安全的情况下可以使用 ArrayList,线程安全的情况下可以使用 CopyOnWriteArrayList


  • 破坏了原有的数据结构


栈的定义是在一端进行 pushpop 操作,除此之外不应该包含其他 入栈和出栈 的方法,但是 Stack 继承自 Vector,使得 Stack 可以使用父类 Vector 公有的方法,如下所示。


val stack = Stack<Int>()
stack.push(6)
stack.add(1,10)
stack.removeAt(1)
stack.pop()
stack.addAll(arrayListOf())
......


正如你所见,除了调用 push() 和  pop() 方法之外,还可以调用 addXXX()removeXXX() 等等方法,但是这样会破坏栈原有的结构。所以对于栈的数据结构,不应该有可以在任何位置添加或者删除元素的能力。


为什么现在还在用


但是为什么在实际项目中还有很多小伙伴在使用 Stack。如果你经常刷 LeetCode 应该会见到很多小伙伴使用 Stack 做相关的算法题。总结了一下主要有两个原因。


  • JDK 官方是不推荐使用 Stack,之所以还有很多人在使用,是因为 JDK 并没有加 deprecation 注解,只是在文档和注释中声明不建议使用,但是很少有人会去关注其实现细节
  • 在做算法题的时候,关注点在解决问题的算法逻辑思路上,并不会关注在不同语言下 Stack 实现细节,但是对于使用 Java 语言的开发者,不仅需要关注算法逻辑本身,也需要关注它的实现细节


为什么推荐使用 Deque 接口替换栈


如果 JDK 不推荐使用 Stack,那应该使用什么集合类来替换栈,一起看看官方的文档。


image.png


正如图中标注部分所示,栈的相关操作应该由 Deque 接口来提供,推荐使用 Deque 这种数据结构, 以及它的子类,例如 ArrayDeque


val stack: Deque<Int> = ArrayDeque()


使用 Deque 接口来实现栈的功能有什么好处:


  • 速度比 Stack 快


image.png


这个类作为栈使用时可能比 Stack 快,作为队列使用时可能比 LinkedList 快。因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。


  • 屏蔽掉无关的方法


原来的 Java 的 Stack,包含了在任何位置添加或者删除元素的方法,这些不是栈应该有的方法,所以需要屏蔽掉这些无关的方法。


声明为 Deque 接口可以解决这个问题,在接口中声明栈需要用到的方法,无需管子类是如何是实现的,对于上层使用者来说,只可以调用和栈相关的方法。


Stack 和 ArrayDeque 区别如下所示。


集合类型 数据结构 是否线程安全
Stack 数组
ArrayDeque 数组


Stack 常用的方法如下所示。


操作 方法
入栈 push(E item)
出栈 pop()
查看栈顶 peek() 为空时返回 null


ArrayDeque 常用的方法如下所示。


操作 方法
入栈 push(E item)
出栈 poll() 栈为空时返回 null
pop() 栈为空时会抛出异常
查看栈顶 peek() 为空时返回 null


栈的时间复杂度


栈的核心实现是通过动态数组来实现的,所以在扩容的时候,时间复杂度为 O(n),其他操作例如 push(E item) 和  pop()peek() 等等时间复杂度为 O(1)


栈的应用:有效的括号



题解已收藏于 github.com/hi-dhl/Leet…。每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度、空间复杂度和源代码,


题目描述


给定一个字符串, 只包括 '(',')','{','}','[',']',判断字符串是否有效


有效字符串需要满足以下条件:


  • 左括号必须用相同类型的右括号闭合
  • 左括号必须以正确的顺序闭合


注意空字符串可被认为是有效字符串。


Example 1:
    Input: "()"
    Output: true
Example 2:
    Input: "()[]{}"
    Output: true
Example 3:
    Input: "(]"
    Output: false
Example 4:
    Input: "([)]"
    Output: false
Example 5:
    Input: "{[]}"
    Output: true


算法流程


image.png


  1. 如果遇到左括号,将对应的右括号压入栈中
  2. 如果遇到右括号
  • 判断当前栈是否为空
  • 如果不为空,判断当前元素是否和栈顶元素相等
  • 如果不相等,发现了不符合的括号,提前返回 false,结束循环
  1. 重复执行「步骤 1」 和「步骤 2」
  2. 循环结束之后,通过判断栈是否为空,来检查是否是有效的括号


复杂度分析


假设字符串的长度为 N 则:


  • 时间复杂度:O(N)。正确有效的括号需要遍历了一次字符串,所需要的时间复杂度为 O(N)
  • 空间复杂度:O(N)。如果输入字符串全是左括号,例如 (((((((,栈的大小即为输入字符串的长度,所需要的空间复杂度为 O(N)


Kotlin 实现


class Solution {
    fun isValid(s: String): Boolean {
        val stack = ArrayDeque<Char>()
        // 开始遍历字符串
        for (c in s) {
            when (c) {
                // 遇到左括号,将对应的右括号压入栈中
                '(' -> stack.push(')')
                '[' -> stack.push(']')
                '{' -> stack.push('}')
                else -> {
                    // 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
                    if (stack.isEmpty() || stack.poll() != c) {
                        return false
                    }
                }
            }
        }
        // 通过判断栈是否为空,来检查是否是有效的括号
        return stack.isEmpty()
    }
}


Java 实现


class Solution {
    public boolean isValid(String s) {
        Deque<Character> stack = new ArrayDeque<Character>();
        // 开始遍历字符串
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // 遇到左括号,则将其对应的右括号压入栈中
            if (c == '(') {
                stack.push(')');
            } else if (c == '[') {
                stack.push(']');
            } else if (c == '{') {
                stack.push('}');
            } else {
                // 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
                if (stack.isEmpty() || stack.poll() != c) {
                    return false;
                }
            }
        }
        // 通过判断栈是否为空,来检查是否是有效的括号
        return stack.isEmpty();
    }
}


仓库 KtKit 是用 Kotlin 语言编写的小巧而实用的工具库,包含了项目中常用的一系列工具, 正在逐渐完善中。



如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR


如果有帮助 点个赞 就是对我最大的鼓励


代码不止,文章不停


最后推荐我一直在更新维护的项目和网站:


  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com
  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析


image.png


  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation


近期必读热门文章




目录
相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
69 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
31 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
20天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
24天前
|
存储 算法 Java
🧠Java零基础 - Java栈(Stack)详解
【10月更文挑战第17天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
17 2
|
1月前
|
算法
数据结构与算法二:栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式
这篇文章讲解了栈的基本概念及其应用,并详细介绍了中缀表达式转换为后缀表达式的算法和实现步骤。
46 3
|
1月前
|
算法 搜索推荐 Java
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
这篇文章介绍了如何使用Java后端技术,结合Graphics2D和Echarts等工具,生成包含个性化信息和图表的海报,并提供了详细的代码实现和GitHub项目链接。
105 0
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索栈和队列的无尽秘密
【用Java学习数据结构系列】探索栈和队列的无尽秘密
30 2
|
1月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
71 2
|
1月前
|
算法 Java Linux
java制作海报一:java使用Graphics2D 在图片上写字,文字换行算法详解
这篇文章介绍了如何在Java中使用Graphics2D在图片上绘制文字,并实现自动换行的功能。
100 0