"队列就是排队,先到的先买,后到的等着!" 🧋
😊 什么是队列?奶茶店的故事
🧋 生活中的队列
想象你去网红奶茶店买奶茶:
出口 ←─────────────────────── 入口
↓ ↑
[👨🦰] ← [👩🦱] ← [👨🦳] ← [👧] ← [👦]
第1个 第2个 第3个 第4个 第5个
(先到) (后到)
规则:
- 先来的人先买到(出队)
- 新来的人排到队尾(入队)
- 不能插队!❌
这就是队列!先进先出(FIFO - First In First Out) ✨
栈 vs 队列:
栈:后进先出(LIFO)🍽️
像叠盘子,最后放的最先拿
队列:先进先出(FIFO)🎫
像排队,先来的先走
🏗️ 队列的原理
基本概念
队列(Queue) = 两端开口的容器
出队(Dequeue) 入队(Enqueue)
↓ ↑
┌────┬────┬────┬────┬────┐
Front │ 10 │ 20 │ 30 │ 40 │ 50 │ Rear
└────┴────┴────┴────┴────┘
↑ ↑
队头 队尾
(只能出) (只能进)
基本操作
| 操作 | 说明 | 时间复杂度 |
|---|---|---|
| enqueue(x) | 入队:将元素x加入队尾 | O(1) ⚡ |
| dequeue() | 出队:移除并返回队头元素 | O(1) ⚡ |
| peek() / front() | 查看队头元素(不删除) | O(1) ⚡ |
| isEmpty() | 判断队列是否为空 | O(1) ⚡ |
| size() | 返回队列中元素个数 | O(1) ⚡ |
📦 队列的实现方式
1️⃣ 用数组实现(普通队列)
问题:会有"假溢出"现象!
public class ArrayQueue {
private int[] arr;
private int front; // 队头指针
private int rear; // 队尾指针
private int capacity;
private int size; // 当前元素个数
public ArrayQueue(int capacity) {
this.capacity = capacity;
arr = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
// 入队
public void enqueue(int value) {
if (size == capacity) {
System.out.println("❌ 队列满了!");
return;
}
rear = (rear + 1) % capacity; // 循环利用
arr[rear] = value;
size++;
System.out.println("✅ 入队: " + value);
}
// 出队
public int dequeue() {
if (isEmpty()) {
System.out.println("❌ 队列为空!");
return -1;
}
int value = arr[front];
front = (front + 1) % capacity;
size--;
System.out.println("📤 出队: " + value);
return value;
}
// 查看队头
public int peek() {
if (isEmpty()) {
System.out.println("❌ 队列为空!");
return -1;
}
return arr[front];
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
}
2️⃣ 用链表实现
public class LinkedQueue {
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
private Node front; // 队头
private Node rear; // 队尾
private int size;
public LinkedQueue() {
front = null;
rear = null;
size = 0;
}
// 入队:在队尾添加
public void enqueue(int value) {
Node newNode = new Node(value);
if (rear == null) { // 队列为空
front = rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
size++;
System.out.println("✅ 入队: " + value);
}
// 出队:从队头移除
public int dequeue() {
if (isEmpty()) {
System.out.println("❌ 队列为空!");
return -1;
}
int value = front.data;
front = front.next;
if (front == null) { // 队列变空了
rear = null;
}
size--;
System.out.println("📤 出队: " + value);
return value;
}
// 查看队头
public int peek() {
if (isEmpty()) {
System.out.println("❌ 队列为空!");
return -1;
}
return front.data;
}
public boolean isEmpty() {
return front == null;
}
public int size() {
return size;
}
}
🔄 循环队列(Circular Queue)
为什么需要循环队列?
普通数组队列的问题:
初始: [10, 20, 30, 40, __]
front=0, rear=3
出队2次后:
[__, __, 30, 40, __]
front=2, rear=3
此时前面有空位,但rear已经在后面,会浪费空间!😰
循环队列的解决方案:
把数组想象成一个环!🔄
[4] [0]
╲ ╱
╲ ╱
[3] ╳ [1]
╱ ╲
╱ ╲
[2]
当rear到达末尾,下一个位置回到0!
循环队列实现
public class CircularQueue {
private int[] arr;
private int front;
private int rear;
private int capacity;
public CircularQueue(int capacity) {
this.capacity = capacity + 1; // 多留一个空位,用于区分空和满
arr = new int[this.capacity];
front = 0;
rear = 0;
}
// 入队
public boolean enqueue(int value) {
if (isFull()) {
System.out.println("❌ 队列满了!");
return false;
}
arr[rear] = value;
rear = (rear + 1) % capacity; // 循环
System.out.println("✅ 入队: " + value);
return true;
}
// 出队
public int dequeue() {
if (isEmpty()) {
System.out.println("❌ 队列为空!");
return -1;
}
int value = arr[front];
front = (front + 1) % capacity; // 循环
System.out.println("📤 出队: " + value);
return value;
}
// 判断空
public boolean isEmpty() {
return front == rear;
}
// 判断满
public boolean isFull() {
return (rear + 1) % capacity == front;
}
// 队列大小
public int size() {
return (rear - front + capacity) % capacity;
}
}
关键点:
- 用
(rear + 1) % capacity == front判断队满 - 用
front == rear判断队空 - 需要浪费一个位置来区分空和满
🔀 双端队列(Deque - Double Ended Queue)
概念
双端队列 = 两端都能进出的队列!
↕️ ↕️
入队/出队 入队/出队
↓ ↓
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘
↑ ↑
队头 队尾
支持的操作:
addFirst(x)/offerFirst(x)- 队头添加addLast(x)/offerLast(x)- 队尾添加removeFirst()/pollFirst()- 队头移除removeLast()/pollLast()- 队尾移除
Java的Deque
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeDemo {
public static void main(String[] args) {
Deque<Integer> deque = new ArrayDeque<>();
// 队尾添加
deque.addLast(10);
deque.addLast(20);
// 队头添加
deque.addFirst(5);
// 现在队列: 5 ← 10 ← 20
// 队头移除
System.out.println(deque.removeFirst()); // 5
// 队尾移除
System.out.println(deque.removeLast()); // 20
// 剩下: 10
}
}
双端队列可以当作:
- ✅ 栈使用:只用一端(addFirst + removeFirst)
- ✅ 队列使用:两端(addLast + removeFirst)
🏆 优先队列(Priority Queue)
概念
优先队列 = 不是先来先走,而是优先级高的先走!
普通队列:先进先出
👨🦰(1号) → 👩🦱(2号) → 👨🦳(3号) → 出口
优先队列:优先级高的先出
👦(优先级3)
👧(优先级1) → 出口(优先级最高的先走)
👨🦰(优先级5)
👩🦱(优先级2)
生活例子:
- 🏥 急诊室:病情严重的先看
- ✈️ 机场安检:头等舱优先
- 🎮 游戏任务:紧急任务先处理
Java的PriorityQueue
import java.util.PriorityQueue;
public class PriorityQueueDemo {
public static void main(String[] args) {
// 默认是最小堆(小的先出)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(30);
pq.offer(10);
pq.offer(50);
pq.offer(20);
// 输出顺序:10, 20, 30, 50(按从小到大)
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
}
}
最大堆(大的先出):
// 方式1:使用Collections.reverseOrder()
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 方式2:自定义Comparator
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(30);
maxHeap.offer(10);
maxHeap.offer(50);
// 输出:50, 30, 10
while (!maxHeap.isEmpty()) {
System.out.println(maxHeap.poll());
}
底层实现:堆(Heap) - 一种完全二叉树
最小堆示例:
10
/ \
20 30
/ \
40 50
性质:父节点 ≤ 子节点
🔐 阻塞队列(BlockingQueue)
概念
阻塞队列 = 线程安全的队列,支持阻塞操作
生产者-消费者模式:
生产者线程 →→→ [阻塞队列] →→→ 消费者线程
┌──────┐
│ 🍞🍞 │
└──────┘
队列满了 → 生产者阻塞(等待)
队列空了 → 消费者阻塞(等待)
Java的BlockingQueue
import java.util.concurrent.*;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// ArrayBlockingQueue - 有界阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 生产者线程
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
queue.put(i); // 队列满了会阻塞
System.out.println("生产: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
Thread.sleep(2000); // 延迟启动
while (true) {
int value = queue.take(); // 队列空了会阻塞
System.out.println("消费: " + value);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
常见的阻塞队列:
| 类型 | 说明 | 使用场景 |
|---|---|---|
ArrayBlockingQueue | 有界数组队列 | 线程池 |
LinkedBlockingQueue | 有界/无界链表队列 | 线程池 |
PriorityBlockingQueue | 无界优先级队列 | 任务调度 |
SynchronousQueue | 无缓冲队列 | 直接交付 |
DelayQueue | 延迟队列 | 定时任务 |
🎯 队列的应用场景
1️⃣ 广度优先搜索(BFS)
public void bfs(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.println(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
2️⃣ 消息队列
生产者1 →→→┐
生产者2 →→→┤
生产者3 →→→┼→→ [消息队列] →→→┬→→ 消费者1
│ ├→→ 消费者2
│ └→→ 消费者3
│
削峰填谷!
例子:
- Kafka
- RabbitMQ
- RocketMQ
3️⃣ 打印任务队列
打印任务队列:
┌─────────────┐
│ 文档1.pdf │ ← 正在打印
│ 文档2.doc │
│ 文档3.xlsx │
└─────────────┘
先提交的先打印!
4️⃣ CPU任务调度
进程调度:
┌───────┐ ┌───────┐ ┌───────┐
│进程1 │→ │进程2 │→ │进程3 │→ CPU
└───────┘ └───────┘ └───────┘
时间片轮转调度
🏆 队列的经典面试题
1. 用队列实现栈(LeetCode 225)
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
// 入栈:先加入队列,然后把前面的元素全部出队再入队
public void push(int x) {
int size = queue.size();
queue.offer(x);
// 把前面的元素移到后面
for (int i = 0; i < size; i++) {
queue.offer(queue.poll());
}
}
// 出栈:直接出队
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
演示:
Push(1): [1]
Push(2): [2] → [2, 1] (把1移到后面)
Push(3): [3] → [3, 2, 1] (把2,1移到后面)
Pop(): 返回3,剩下[2, 1]
2. 滑动窗口最大值(LeetCode 239)- 单调队列
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k <= 0) return new int[0];
int n = nums.length;
int[] result = new int[n - k + 1];
Deque<Integer> deque = new ArrayDeque<>(); // 存储索引
for (int i = 0; i < n; i++) {
// 移除超出窗口的元素
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除比当前元素小的元素(保持单调递减)
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 窗口形成后,记录最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
3. 二叉树的层序遍历(LeetCode 102)
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}
📊 队列对比总结
| 队列类型 | 特点 | 底层 | 时间复杂度 |
|---|---|---|---|
| 普通队列 | 先进先出 | 数组/链表 | O(1) |
| 循环队列 | 循环利用空间 | 数组 | O(1) |
| 双端队列 | 两端可进出 | 数组/链表 | O(1) |
| 优先队列 | 优先级排序 | 堆 | O(log n) |
| 阻塞队列 | 线程安全 | 数组/链表 | O(1) |
📝 总结
🎓 记忆口诀
队列就像排队买奶茶,
先来先走最公平。
循环队列省空间,
双端队列两头开。
优先队列看权重,
阻塞队列保线程。
BFS必用队列,
消息队列也靠它!
核心特点
| 特性 | 说明 | 符号 |
|---|---|---|
| 原则 | FIFO(先进先出) | 🎫 |
| 操作 | 队头出,队尾进 | ↔️ |
| 时间 | 入队出队O(1) | ⚡ |
| 应用 | BFS、消息队列、调度 | 🎯 |
🚀 下一步学习
掌握了队列,接下来可以学习:
- 哈希表(HashMap) - 快速查找的利器 #️⃣
- 堆(Heap) - 优先队列的底层实现 🏔️
- 广度优先搜索(BFS) - 队列的经典应用 🌲
恭喜你!🎉 你已经掌握了队列家族的各种技能!
记住:队列就是排队,先来先走,公平公正! 🎫
从普通队列到优先队列,从单端到双端,队列家族强大又实用!💪
📌 小练习:尝试用数组实现一个循环队列,支持入队、出队、判空、判满!
🤔 思考题:为什么BFS用队列,DFS用栈?
(答案:BFS是层层扩展(先进先出),DFS是深入探索(后进先出)!)