🎫 队列(Queue):排队买奶茶的艺术,先来先得的公平!

46 阅读8分钟

"队列就是排队,先到的先买,后到的等着!" 🧋


😊 什么是队列?奶茶店的故事

🧋 生活中的队列

想象你去网红奶茶店买奶茶:

出口 ←─────────────────────── 入口
 ↓                           ↑
[👨‍🦰][👩‍🦱][👨‍🦳][👧][👦]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、消息队列、调度🎯

🚀 下一步学习

掌握了队列,接下来可以学习:

  1. 哈希表(HashMap) - 快速查找的利器 #️⃣
  2. 堆(Heap) - 优先队列的底层实现 🏔️
  3. 广度优先搜索(BFS) - 队列的经典应用 🌲

恭喜你!🎉 你已经掌握了队列家族的各种技能!

记住:队列就是排队,先来先走,公平公正! 🎫

从普通队列到优先队列,从单端到双端,队列家族强大又实用!💪


📌 小练习:尝试用数组实现一个循环队列,支持入队、出队、判空、判满!

🤔 思考题:为什么BFS用队列,DFS用栈?

(答案:BFS是层层扩展(先进先出),DFS是深入探索(后进先出)!)