Stack遵循LIFO,Queue遵循FIFO;Java中推荐用ArrayDeque实现Stack,Queue常用LinkedList、ArrayDeque、PriorityQueue等,适用于表达式求值、BFS、任务调度等场景。
在Java中,
Stack和
Queue是两种核心的数据结构,它们提供了一种管理数据序列的特定方式。简单来说,
Stack遵循“后进先出”(LIFO)原则,就像一摞盘子,最后放上去的盘子最先被拿走;而
Queue则遵循“先进先出”(FIFO)原则,就像排队买票,最先排队的人最先买到票。理解并恰当使用它们,能显著优化我们处理数据流和特定算法的效率。
要在Java中使用
Stack和
Queue,我们需要了解它们的接口和常用实现。
使用Stack
java.util.Stack是Java集合框架中一个历史悠久的类,它继承自
Vector,因此是同步的(线程安全),这在单线程环境中反而会带来不必要的性能开销。它的核心操作包括:
push(E item): 将元素压入栈顶。
pop(): 移除并返回栈顶元素。如果栈为空,会抛出
EmptyStackException。
peek(): 返回栈顶元素,但不移除。如果栈为空,会抛出
EmptyStackException。
empty(): 检查栈是否为空。
search(Object o): 返回元素在栈中的1-based位置,如果不存在则返回-1。
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack browserHistory = new Stack<>();
// 访问页面
browserHistory.push("google.com");
browserHistory.push("baidu.com");
browserHistory.push("github.com");
System.out.println("当前页面: " + browserHistory.peek()); // github.com
// 后退
String lastVisited = browserHistory.pop();
System.out.println("后退到: " + browserHistory.peek()); // baidu.com
// 再次访问新页面
browserHistory.push("stackoverflow.com");
System.out.println("当前页面: " + browserHistory.peek()); // stackoverflow.com
System.out.println("栈是否为空: " + browserHistory.empty()); // false
}
} 使用Queue
java.util.Queue是一个接口,它定义了队列的基本行为。由于它是一个接口,我们需要选择一个具体的实现类。常用的实现包括
LinkedList和
ArrayDeque。
Queue的核心操作有两套,一套在操作失败时抛出异常,另一套返回特殊值(
null或
false),通常建议使用返回特殊值的方法:
add(E e): 将元素插入队列尾部。
remove(): 移除并返回队列头部元素。
element(): 返回队列头部元素,但不移除。
offer(E e): 将元素插入队列尾部,成功返回
true,失败返回
false。
poll(): 移除并返回队列头部元素。如果队列为空,返回
null。
peek(): 返回队列头部元素,但不移除。如果队列为空,返回
null。
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
Queue taskQueue = new LinkedList<>(); // LinkedList是Queue的一个常用实现
// 添加任务
taskQueue.offer("任务A");
taskQueue.offer("任务B");
taskQueue.offer("任务C");
System.out.println("队列头部任务: " + taskQueue.peek()); // 任务A
// 处理任务
String currentTask = taskQueue.poll();
System.out.println("正在处理: " + currentTask); // 任务A
System.out.println("下一个任务: " + taskQueue.peek()); // 任务B
// 继续处理
taskQueue.poll();
taskQueue.poll();
System.out.println("队列是否为空: " + taskQueue.isEmpty()); // true
System.out.println("尝试获取空队列头部: " + taskQueue.poll()); // null
}
} Stack和
Deque:我该如何选择,以及为什么?
这是个老生常谈但又很重要的问题。当我刚开始学习Java的时候,
Stack类似乎是理所当然的选择。然而,随着对集合框架的深入了解,我发现
java.util.Stack在现代Java开发中,通常不是实现栈行为的首选。
主要原因在于:
Stack类继承自
Vector,这意味着它是同步的。在多线程环境下,同步机制可以保证数据一致性,但在单线程环境下,这种同步开销是完全不必要的性能负担。此外,
Stack的API设计也略显陈旧,它直接暴露了
Vector的一些方法,比如
get(int index),这在语义上与LIFO的栈操作有些冲突,可能导致误用。
所以,更推荐的做法是使用
java.util.Deque(双端队列)接口的实现类来模拟栈的行为。
Deque,顾名思义,是一个可以在两端进行插入和删除操作的队列。它提供了与栈操作完全对应的API方法:
push(E e): 相当于
Stack的
push(),将元素压入双端队列的头部。
pop(): 相当于
Stack的
pop(),移除并返回双端队列头部的元素。
peek(): 相当于
Stack的
peek(),返回双端队列头部的元素,但不移除。
ArrayDeque和
LinkedList都是
Deque接口的优秀实现。其中,
ArrayDeque通常被认为是实现栈和队列的更优选择,因为它基于数组实现,没有同步开销,且在大多数操作上比
LinkedList更快。
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeAsStackExample {
public static void main(String[] args) {
Deque stack = new ArrayDeque<>(); // 使用ArrayDeque作为栈
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("栈顶元素: " + stack.peek()); // 30
System.out.println("弹出元素: " + stack.pop()); // 30
System.out.println("新的栈顶元素: " + stack.peek()); // 20
// 尝试使用LinkedList作为栈
Deque stringStack = new LinkedList<>();
stringStack.push("A");
stringStack.push("B");
System.out.println("String栈顶: " + stringStack.peek()); // B
}
} 总而言之,如果你需要一个栈,请优先考虑使用
Deque接口的实现,尤其是
ArrayDeque,它在性能和API设计上都更符合现代Java开发的最佳实践。
Queue接口的不同实现及其适用场景
Queue接口本身只定义了行为,具体的性能和特性则取决于其实现类。选择合适的
Queue实现对于程序的性能和健壮性至关重要。
LinkedList
:
offer和
poll)效率很高,时间复杂度为O(1)。它也可以作为
Deque使用,同时支持栈和队列操作。
LinkedList是一个不错的选择。例如,在实现一些简单的消息队列、任务调度器或者BFS算法时。
ArrayDeque
:
LinkedList和
Stack性能更好。它同样实现了
Deque接口,可以高效地作为栈或队列使用。
ArrayDeque在性能上会优于
LinkedList,因为它避免了链表节点的额外开销。尤其适合那些需要高性能、非阻塞的FIFO或LIFO操作的场景,比如解析器、算法中的状态管理。
PriorityQueue
:
Comparator来决定元素的优先级。每次
poll()操作都会移除优先级最高的元素。
PriorityQueue是唯一的选择。例如,任务调度器中需要优先处理紧急任务,或者在实现Dijkstra算法、Prim算法等图算法时。
ConcurrentLinkedQueue
(以及其他java.util.concurrent
包下的队列):
ConcurrentLinkedQueue是一个线程安全的非阻塞队列,基于链表实现。它通过CAS(Compare-And-Swap)操作来保证线程安全,而不是通过锁,因此在并发环境下性能通常优于
synchronized的队列。
ConcurrentLinkedQueue是非常合适的。例如,生产者-消费者模式、日志收集系统、事件处理系统等。
选择哪种实现,关键在于你的具体需求:是需要高性能的单线程操作?还是需要线程安全的并发操作?亦或是需要基于优先级的处理?明确这些,选择自然就清晰了。
Stack和
Queue有哪些常见应用场景?
在实际的软件开发中,
Stack和
Queue这两种看似简单的数据结构,却有着极其广泛且强大的应用。它们不仅仅是算法题的常客,更是许多复杂系统底层逻辑的基石。
Stack
的常见应用场景:
(),
{}, [])是否正确匹配,是栈的一个直接应用。每遇到一个左括号就入栈,遇到右括号就检查栈顶是否是对应的左括号,如果是则出栈。
Queue
的常见应用场景:
LinkedHashMap实现,但FIFO(先进先出)缓存淘汰策略可以直接用队列实现。最先进入缓存的数据,在缓存满时最先被淘汰。
状态的进程。例如,先来先服务(FCFS)调度算法就直接基于队列。无论是简单的算法题,还是复杂的系统架构,
Stack和
Queue都以它们独特的访问模式,为我们提供了高效、简洁的数据管理方案。理解它们的特性和适用场景,是成为一个优秀程序员的必经之路。