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

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

栈的定义

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

算法动画图解 | 被_Android

栈的实现

栈常用的实现方式是通过动态数组来实现的,在 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​​​ 就被淘汰掉了,使用 ​​ArrayList​​​ 和 ​​CopyOnWriteArrayList​​​ 来代替,如果在非线程安全的情况下可以使用 ​​ArrayList​​​,线程安全的情况下可以使用 ​​CopyOnWriteArrayList​​ 。

  • 破坏了原有的数据结构

栈的定义是在一端进行 ​​push​​​ 和 ​​pop​​​ 操作,除此之外不应该包含其他 入栈和出栈 的方法,但是 ​​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​​,那应该使用什么集合类来替换栈,一起看看官方的文档。

算法动画图解 | 被_java_02

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

val stack: Deque<Int> = ArrayDeque()复制代码

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

  • 速度比 Stack 快

算法动画图解 | 被_java_03

这个类作为栈使用时可能比 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

算法流程

算法动画图解 | 被_算法_04

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

复杂度分析

假设字符串的长度为 ​​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 语言编写的小巧而实用的工具库,包含了项目中常用的一系列工具, 正在逐渐完善中。