数据结构与算法 - 基础:队列 (Queue) 全方位解析
一、队列------排队的哲学
如果说栈是"后来者居上",那么队列就是"先来者先走"------先进入队伍的人先被服务,后进入的人在后面等待。这种 FIFO(First In, First Out,先进先出) 的规则,构成了现实世界中最公平的服务模型。
从银行柜台排队、操作系统打印任务的调度,到消息中间件(Kafka、RabbitMQ)的消息传递,再到 Java 线程池的任务缓冲------队列的身影无处不在。
scss
入队(enqueue) ──→ [8] [3] [5] [1] [9] ──→ 出队(dequeue)
↑ ↑
队尾(rear) 队头(front)
栈和队列的核心差异在操作方向上:
| 特性 | 栈 (Stack) | 队列 (Queue) |
|---|---|---|
| 操作约束 | 仅一端操作(栈顶) | 两端各一种操作 |
| 入操作 | push(同一端) | offer / add(队尾) |
| 出操作 | pop(同一端) | poll / remove(队头) |
| 查看操作 | peek(同一端) | peek(队头) |
| 顺序语义 | LIFO(后进先出) | FIFO(先进先出) |
| 心理比喻 | 一摞盘子 | 排队买票 |
二、循环数组队列------用数组高效模拟环形结构
2.1 循环队列的设计思想
如果用普通数组实现队列而不做任何优化,会出现一个严重的问题:当队头的元素被移除后,前面的空间就"浪费"了,队尾不断向后移动,最终到达数组末尾就无法再入队------这就是"假溢出"。
循环队列巧妙地解决了这个问题:将数组视为一个首尾相连的环。当队尾指针到达数组末尾时,下一个位置"绕回"到数组的起始位置(下标 0)。
ini
普通队列的假溢出:
下标: [0] [1] [2] [3] [4]
状态: 空 空 空 C D
↑ ↑
front rear (到末尾了,无法再入队!)
循环队列:
下标: [0] [1] [2] [3] [4]
状态: E F 空 C D
↑ ↑
rear front
(rear 绕回到 index 0,空间得到复用)
2.2 完整实现
java
import java.util.NoSuchElementException;
public class CircularArrayQueue<E> {
private Object[] data; // 底层循环数组
private int front; // 队头指针(指向第一个有效元素)
private int rear; // 队尾指针(指向最后一个有效元素的下一个位置)
private int size; // 当前元素个数
private static final int DEFAULT_CAPACITY = 8;
public CircularArrayQueue() {
data = new Object[DEFAULT_CAPACITY];
front = 0;
rear = 0;
size = 0;
}
public CircularArrayQueue(int capacity) {
data = new Object[Math.max(1, capacity)];
front = 0;
rear = 0;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == data.length;
}
/**
* 入队------将元素添加到队尾
* 时间复杂度: 均摊 O(1),扩容时 O(n)
* 空间复杂度: O(1)(扩容除外)
*/
public boolean offer(E item) {
if (isFull()) {
resize(data.length * 2);
}
data[rear] = item;
rear = (rear + 1) % data.length; // 核心:取模实现环形
size++;
return true;
}
/**
* 出队------移除并返回队头元素
* 时间复杂度: O(1)
* 空间复杂度: O(1)
* @throws NoSuchElementException 队列为空时抛出
*/
@SuppressWarnings("unchecked")
public E poll() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空,无法出队");
}
E item = (E) data[front];
data[front] = null; // 帮助 GC
front = (front + 1) % data.length; // 核心:队头环绕前进
size--;
return item;
}
/**
* 查看队头元素(不移除)
*/
@SuppressWarnings("unchecked")
public E peek() {
if (isEmpty()) {
return null;
}
return (E) data[front];
}
/**
* 扩容:需要将循环数组"展开"成从 0 开始的线性排列
*/
private void resize(int newCapacity) {
Object[] newData = new Object[newCapacity];
// 从 front 开始,顺序拷贝 size 个元素到新数组的 [0..size-1]
for (int i = 0; i < size; i++) {
newData[i] = data[(front + i) % data.length];
}
data = newData;
front = 0;
rear = size; // 新数组中所有元素在 [0, size-1]
}
@Override
public String toString() {
if (isEmpty()) return "[] (空队列)";
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
sb.append(data[(front + i) % data.length]);
if (i < size - 1) sb.append(", ");
}
sb.append("]");
return sb.toString();
}
public String debugInfo() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("front=%d, rear=%d, size=%d/%d\n", front, rear, size, data.length));
sb.append("数组内容: [");
for (int i = 0; i < data.length; i++) {
sb.append(data[i] == null ? "_" : data[i]);
if (i < data.length - 1) sb.append(", ");
}
sb.append("]");
return sb.toString();
}
// ========== 测试主方法 ==========
public static void main(String[] args) {
System.out.println("========== 循环数组队列------完整演示 ==========\n");
CircularArrayQueue<Integer> queue = new CircularArrayQueue<>(5);
System.out.println("--- 入队(enqueue) ---");
for (int i = 1; i <= 8; i++) {
queue.offer(i * 10);
System.out.printf("入队 %2d → %s\n", i * 10, queue);
System.out.println(" " + queue.debugInfo());
}
System.out.println("(触发了一次扩容:5 → 10)");
System.out.println("\n--- 出队(dequeue) ---");
for (int i = 1; i <= 4; i++) {
int val = queue.poll();
System.out.printf("出队 %2d → %s\n", val, queue);
System.out.println(" " + queue.debugInfo());
}
System.out.println("\n--- 再次入队(验证循环复用) ---");
for (int i = 1; i <= 4; i++) {
queue.offer(i * 100);
System.out.printf("入队 %3d → %s\n", i * 100, queue);
System.out.println(" " + queue.debugInfo());
}
System.out.println("\n--- 清空队列 ---");
while (!queue.isEmpty()) {
System.out.printf("出队 %-3d → %s\n", queue.poll(), queue);
}
}
}
三、链式队列------无限容量的队列
链表实现队列不存在容量限制,也不需要扩容时的数组拷贝。核心思路是维护两个指针------队头指针 head(用于出队)和队尾指针 tail(用于入队)。
java
import java.util.NoSuchElementException;
public class LinkedQueue<E> {
/** 内部节点 */
private static class Node<E> {
E data;
Node<E> next;
Node(E data) {
this.data = data;
this.next = null;
}
}
private Node<E> head; // 队头(最早进入的元素)
private Node<E> tail; // 队尾(最新进入的元素)
private int size; // 元素个数
public LinkedQueue() {
head = null;
tail = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 入队:在链表尾部追加节点
* 时间复杂度: 严格 O(1)
*
* 图示 (enqueue "C"):
* head → [A] → [B] → null
* ↑ tail
* 插入后:
* head → [A] → [B] → [C] → null
* ↑ tail
*/
public boolean offer(E item) {
Node<E> newNode = new Node<>(item);
if (isEmpty()) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode; // 当前的 tail 后继指向新节点
tail = newNode; // tail 移动到新节点
}
size++;
return true;
}
/**
* 出队:移除链表头部节点
* 时间复杂度: 严格 O(1)
*/
public E poll() {
if (isEmpty()) {
return null;
}
E item = head.data;
head = head.next;
size--;
// 如果移除后队列为空,tail 也应置 null
if (isEmpty()) {
tail = null;
}
return item;
}
/**
* 查看队头
*/
public E peek() {
return isEmpty() ? null : head.data;
}
@Override
public String toString() {
if (isEmpty()) return "(空队列)";
StringBuilder sb = new StringBuilder("front → ");
Node<E> current = head;
while (current != null) {
sb.append("[").append(current.data).append("]");
if (current.next != null) sb.append(" → ");
current = current.next;
}
sb.append(" ← rear");
return sb.toString();
}
// ========== 测试主方法 ==========
public static void main(String[] args) {
System.out.println("========== 链式队列------完整演示 ==========\n");
LinkedQueue<String> queue = new LinkedQueue<>();
String[] tasks = {"编译源码", "运行测试", "打包部署", "发送通知", "清理缓存"};
for (String task : tasks) {
queue.offer(task);
System.out.println("入队: " + task);
}
System.out.println("队列: " + queue);
System.out.println("当前大小: " + queue.size());
System.out.println("\n--- 按顺序处理任务 ---");
while (!queue.isEmpty()) {
String task = queue.poll();
System.out.printf("正在处理: %-10s → 剩余: %s\n", task, queue);
}
}
}
四、双端队列 (Deque)------两端都能操作
java.util.Deque 是 Queue 的一个强大扩展:它打破了"一端进一端出"的限制,允许在两端都进行插入和删除。这使得它既能当队列用(FIFO),又能当栈用(LIFO)。
java
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeDemo {
public static void main(String[] args) {
System.out.println("========== 双端队列 (Deque) 全功能演示 ==========\n");
Deque<Integer> deque = new ArrayDeque<>();
// ======== 队列模式:尾部入、头部出 ========
System.out.println("--- 队列模式 (FIFO) ---");
deque.offerLast(10);
deque.offerLast(20);
deque.offerLast(30);
System.out.println("入队顺序: 10, 20, 30");
while (!deque.isEmpty()) {
System.out.println(" 出队: " + deque.pollFirst());
}
// ======== 栈模式:头部入、头部出 ========
System.out.println("\n--- 栈模式 (LIFO) ---");
deque.push(100); // 等价于 addFirst
deque.push(200);
deque.push(300);
System.out.println("入栈顺序: 100, 200, 300");
while (!deque.isEmpty()) {
System.out.println(" 出栈: " + deque.pop()); // 等价于 removeFirst
}
// ======== 双端操作 ========
System.out.println("\n--- 双端灵活操作 ---");
deque.addFirst(1); // 头部加 1
deque.addLast(5); // 尾部加 5
deque.addFirst(0); // 头部加 0
deque.addLast(9); // 尾部加 9
System.out.println("操作后: " + deque + " (大小=" + deque.size() + ")");
System.out.println("队头: " + deque.peekFirst());
System.out.println("队尾: " + deque.peekLast());
System.out.println("移除队头: " + deque.pollFirst());
System.out.println("移除队尾: " + deque.pollLast());
System.out.println("最终: " + deque);
}
}
五、优先队列------不按先来后到,按优先级
5.1 核心概念
普通队列是严格的 FIFO:先到先服务。但现实中有大量场景不适用这个规则------医院的急诊优先、VIP 用户的请求优先、操作系统中高优先级进程优先获得 CPU。
优先队列 的出队顺序不再取决于入队顺序,而是取决于元素的优先级 。底层通常使用二叉堆(Binary Heap) 实现,保证了插入和删除操作都是 O(log n)。
在 Java 中,java.util.PriorityQueue 默认是最小堆(堆顶元素最小),可以通过传入 Comparator 自定义排序规则。
5.2 完整实现
java
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Random;
public class PriorityQueueDemo {
/** 任务类:包含名称和优先级 */
static class Task {
String name;
int priority; // 数字越小,优先级越高
Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public String toString() {
return "[" + name + " p=" + priority + "]";
}
}
public static void main(String[] args) {
System.out.println("========== 优先队列------完整演示 ==========\n");
// 最小堆:priority 越小越靠近堆顶 → 越优先出队
PriorityQueue<Task> pq = new PriorityQueue<>(
Comparator.comparingInt(t -> t.priority)
);
System.out.println("--- 插入任务(乱序插入,按优先级出队) ---");
Task[] tasks = {
new Task("编译项目", 3),
new Task("紧急修复Bug", 1),
new Task("代码Review", 2),
new Task("写文档", 5),
new Task("合并代码", 2),
new Task("发布上线", 1),
new Task("清理日志", 4),
};
for (Task t : tasks) {
pq.offer(t);
System.out.printf(" 插入: %-14s → 当前队首: %s\n",
t, pq.peek());
}
System.out.println("\n--- 按优先级处理任务 ---");
while (!pq.isEmpty()) {
Task next = pq.poll();
System.out.printf(" 处理: %s\n", next);
}
System.out.println("(优先级1的先处理,优先级相同的按堆内部顺序出队)");
// 演示数字优先队列
System.out.println("\n--- 整数优先队列(最小堆) ---");
PriorityQueue<Integer> intPQ = new PriorityQueue<>();
int[] nums = {7, 2, 9, 1, 5, 3, 8};
for (int n : nums) {
intPQ.offer(n);
}
System.out.print("出队顺序: ");
while (!intPQ.isEmpty()) {
System.out.print(intPQ.poll() + " ");
}
System.out.println("(自动升序)");
// 最大堆演示
System.out.println("\n--- 整数优先队列(最大堆) ---");
PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Comparator.reverseOrder());
for (int n : nums) {
maxPQ.offer(n);
}
System.out.print("出队顺序: ");
while (!maxPQ.isEmpty()) {
System.out.print(maxPQ.poll() + " ");
}
System.out.println("(自动降序)");
}
}
六、队列在生产环境中的应用模拟
6.1 简易线程池中的任务队列
线程池的核心原理是:一个生产者(提交任务)和多个消费者(工作线程)通过一个共享队列进行解耦。任务是生产者提交的,由消费者------工作线程------按 FIFO 顺序取出执行。
java
import java.util.ArrayDeque;
import java.util.Deque;
public class SimpleThreadPoolDemo {
/** 模拟一个简单的线程池 */
static class SimpleThreadPool {
private final Deque<Runnable> taskQueue = new ArrayDeque<>();
private final WorkerThread[] workers;
private volatile boolean shutdown = false;
SimpleThreadPool(int threadCount) {
workers = new WorkerThread[threadCount];
for (int i = 0; i < threadCount; i++) {
workers[i] = new WorkerThread("工作线程-" + (i + 1));
workers[i].start();
}
}
/** 提交任务到队列------生产者 */
public void submit(Runnable task) {
synchronized (taskQueue) {
if (!shutdown) {
taskQueue.offerLast(task);
taskQueue.notify(); // 唤醒一个等待的消费者线程
}
}
}
/** 工作线程从队列取任务------消费者 */
private class WorkerThread extends Thread {
WorkerThread(String name) {
super(name);
}
@Override
public void run() {
while (!shutdown || !taskQueue.isEmpty()) {
Runnable task;
synchronized (taskQueue) {
while (taskQueue.isEmpty() && !shutdown) {
try {
taskQueue.wait(); // 队列空时等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (taskQueue.isEmpty()) break;
task = taskQueue.pollFirst(); // FIFO:取队头任务
}
task.run(); // 执行任务
}
}
}
void shutdown() {
shutdown = true;
synchronized (taskQueue) {
taskQueue.notifyAll(); // 唤醒所有等待线程以退出
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 线程池任务队列演示 ==========\n");
SimpleThreadPool pool = new SimpleThreadPool(3); // 3个工作线程
System.out.println("--- 提交 8 个任务 ---");
for (int i = 1; i <= 8; i++) {
final int taskId = i;
pool.submit(() -> {
System.out.printf(" [%s] 正在执行任务 #%d\n",
Thread.currentThread().getName(), taskId);
try {
Thread.sleep(100); // 模拟任务耗时
} catch (InterruptedException ignored) {}
});
System.out.printf(" 提交任务 #%d 到队列\n", taskId);
}
Thread.sleep(1500); // 等待所有任务完成
pool.shutdown();
System.out.println("\n所有任务执行完毕,线程池关闭。");
}
}
6.2 使用 JDK 阻塞队列实现生产者-消费者
Java 的 BlockingQueue 自带阻塞等待功能,省去了手动 wait/notify 的代码。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class ProducerConsumerDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 生产者-消费者模型 (BlockingQueue) ==========\n");
// 容量为 5 的有界阻塞队列
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// 生产者线程
Thread producer = new Thread(() -> {
String[] items = {"苹果", "香蕉", "橘子", "葡萄", "西瓜", "草莓", "芒果", "桃子"};
try {
for (String item : items) {
queue.put(item); // 队列满时阻塞等待
System.out.printf(" 生产者 → [%s] (队列大小=%d)\n", item, queue.size());
TimeUnit.MILLISECONDS.sleep(200); // 模拟生产速度
}
queue.put("EOF"); // 终止标记
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者");
// 消费者线程
Thread consumer = new Thread(() -> {
try {
while (true) {
String item = queue.take(); // 队列空时阻塞等待
if ("EOF".equals(item)) break;
System.out.printf(" 消费者 ← [%s] (队列大小=%d)\n", item, queue.size());
TimeUnit.MILLISECONDS.sleep(500); // 模拟消费速度(比生产慢)
}
System.out.println("\n消费者收到 EOF,处理完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("(消费速度 < 生产速度,队列会逐渐填满并阻塞生产者)");
}
}
七、JDK 队列生态全景图
Java 为队列提供了丰富的实现,覆盖了有界/无界、阻塞/非阻塞、单端/双端、优先等多种维度:
| 实现类 | 底层结构 | 边界 | 阻塞 | 特性 |
|---|---|---|---|---|
ArrayDeque |
循环数组 | 可扩展 | 否 | 双端,栈+队列首选 |
LinkedList |
双向链表 | 无界 | 否 | 支持 null 元素 |
PriorityQueue |
二叉小顶堆 | 可扩展 | 否 | 按优先级出队 |
ArrayBlockingQueue |
循环数组 | 有界 | 是 | 单锁实现,支持公平策略 |
LinkedBlockingQueue |
单向链表 | 可设界 | 是 | 双锁实现(putLock + takeLock),吞吐更高 |
PriorityBlockingQueue |
二叉堆 | 可扩展 | 是 | 并发优先队列 |
DelayQueue |
优先队列 | 可扩展 | 是 | 元素须到期后才能取出 |
SynchronousQueue |
无存储 | 无容量 | 是 | 每个 put 必须等一个 take,直接传递 |
ConcurrentLinkedQueue |
单向链表 | 无界 | 否 | CAS 实现,非阻塞并发队列 |
LinkedTransferQueue |
单向链表 | 无界 | 是 | 支持 transfer------生产者等待消费者直接取走 |
八、队列的常见应用场景速查
| 场景 | 推荐队列 | 理由 |
|---|---|---|
| 线程池任务缓冲 | LinkedBlockingQueue / ArrayBlockingQueue |
有界可控,阻塞语义天然适配生产者-消费者 |
| 消息中间件 | 持久化队列(磁盘 + 内存) | 支持消息堆积和重启恢复 |
| BFS 广度优先搜索 | ArrayDeque / LinkedList |
简单的 FIFO 需求 |
| 操作系统进程调度 | PriorityQueue |
高优先级进程优先获得 CPU |
| 延迟任务(定时重试) | DelayQueue |
元素需延迟到期后才消费 |
| 双端操作(LRU 缓存) | ArrayDeque |
头尾两端都可能操作 |
| Redis 的 List | 双端队列 | LPUSH/RPOP 实现消息队列 |
九、总结
队列是"公平"的数据结构------先来者先服务。它从最简单的 FIFO 语义出发,通过不同的变体,满足了现代软件系统中各类复杂的场景需求:
- 循环数组队列:空间高效,适用于容量可预估的场景
- 链式队列:无需关心容量上限,内存可动态增长
- 双端队列:灵活性最强,一端一规则,两端皆可为
- 优先队列:打破了时间公平,引入优先级维度
- 阻塞队列:多线程并发场景的基石,天然的生产者-消费者缓冲
理解队列,是理解"解耦"与"缓冲"两大架构思想的起点。消息队列解耦了服务的生产和消费速率;线程池的任务队列解耦了任务提交与任务执行的时机。在这些场景中,队列所扮演的角色远不只是"一个装数据的容器"------它是整个系统的流量调节器。
栈和队列联手构成了线性数据结构的双璧。当世界需要"后来者居上"时请找栈,当世界需要"先来者先走"时请找队列。而你的工作,就是判断当前这个世界要的到底是哪一种。