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

简介: 栈是 后入先出(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


近期必读热门文章




目录
相关文章
|
9月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
2369 35
|
9月前
|
存储 算法 搜索推荐
《数据之美》:Java数据结构与算法精要
本系列深入探讨数据结构与算法的核心原理及Java实现,涵盖线性与非线性结构、常用算法分类、复杂度分析及集合框架应用,助你提升程序效率,掌握编程底层逻辑。
|
9月前
|
存储 人工智能 算法
从零掌握贪心算法Java版:LeetCode 10题实战解析(上)
在算法世界里,有一种思想如同生活中的"见好就收"——每次做出当前看来最优的选择,寄希望于通过局部最优达成全局最优。这种思想就是贪心算法,它以其简洁高效的特点,成为解决最优问题的利器。今天我们就来系统学习贪心算法的核心思想,并通过10道LeetCode经典题目实战演练,带你掌握这种"步步为营"的解题思维。
|
12月前
|
机器学习/深度学习 算法 Java
Java实现林火蔓延路径算法
记录正在进行的森林防火项目中林火蔓延功能,本篇文章可以较好的实现森林防火蔓延,但还存在很多不足,如:很多参数只能使用默认值,所以蔓延范围仅供参考。(如果底层设备获取的数据充足,那当我没说)。注:因林火蔓延涉及因素太多,如静可燃物载量、矿质阻尼系数等存在估值,所以得出的结果仅供参考。
536 5
|
12月前
|
存储 负载均衡 算法
我们来说一说 Java 的一致性 Hash 算法
我是小假 期待与你的下一次相遇 ~
648 1
|
11月前
|
运维 监控 算法
基于 Java 滑动窗口算法的局域网内部监控软件流量异常检测技术研究
本文探讨了滑动窗口算法在局域网流量监控中的应用,分析其在实时性、资源控制和多维分析等方面的优势,并提出优化策略,结合Java编程实现高效流量异常检测。
432 0
|
12月前
|
存储 监控 算法
企业上网监控场景下布隆过滤器的 Java 算法构建及其性能优化研究
布隆过滤器是一种高效的数据结构,广泛应用于企业上网监控系统中,用于快速判断员工访问的网址是否为违规站点。相比传统哈希表,它具有更低的内存占用和更快的查询速度,支持实时拦截、动态更新和资源压缩,有效提升系统性能并降低成本。
553 0
|
9月前
|
机器学习/深度学习 算法 机器人
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
754 0
|
9月前
|
数据采集 分布式计算 并行计算
mRMR算法实现特征选择-MATLAB
mRMR算法实现特征选择-MATLAB
480 2
|
10月前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
385 3