👑 优先队列(PriorityQueue):VIP插队的艺术!

118 阅读12分钟

"在优先队列的世界里,不是先来先得,而是优先级高的先走!" 🎯


📖 一、什么是优先队列?从医院急诊说起

1.1 生活中的场景

想象你在医院急诊室:

普通队列(FIFO):

张三(感冒)→ 李四(发烧)→ 王五(肚子疼)
按到达顺序就诊,谁先来谁先看

优先队列(Priority Queue):

患者A(轻微擦伤,优先级1)
患者B(骨折,优先级5)      ← B先看!(优先级高)
患者C(心脏病发,优先级10)  ← C最先看!(最高优先级)

即使C最后到,但因为优先级最高,会最先被处理!

1.2 专业定义

优先队列(Priority Queue) 是一种特殊的队列,每个元素都有一个优先级,出队时总是**优先级最高(或最低)**的元素先出队,而不管入队的先后顺序。

核心特点:

  • ✅ 不是先进先出(FIFO),而是优先级决定顺序
  • ✅ 底层通常用**堆(Heap)**实现
  • ✅ Java的PriorityQueue默认是最小堆(最小值优先)
  • ⚡ 入队和出队的时间复杂度都是 O(log n)

🏔️ 二、堆(Heap):优先队列的秘密武器

2.1 什么是堆?

是一种特殊的完全二叉树,满足以下性质:

🔻 最小堆(Min Heap)

  • 父节点的值 ≤ 子节点的值
  • 堆顶(根节点)是最小值
  • Java的PriorityQueue默认就是最小堆

🔺 最大堆(Max Heap)

  • 父节点的值 ≥ 子节点的值
  • 堆顶(根节点)是最大值

2.2 最小堆图解

         1           ← 堆顶(最小值)
        / \
       3   2         ← 第二层
      / \ / \
     5  4 6  7       ← 第三层

特点:
- 1 < 3, 1 < 2  (父 < 子)
- 3 < 5, 3 < 4
- 2 < 6, 2 < 7

2.3 堆用数组存储

堆虽然是树结构,但用数组存储超级方便!

数组索引:  0   1   2   3   4   5   6
数组值:   [1,  3,  2,  5,  4,  6,  7]

            1(0)
           /     \
         3(1)     2(2)
        /   \    /   \
      5(3) 4(4) 6(5) 7(6)

重要公式(索引从0开始):
- 父节点索引:(i - 1) / 2
- 左子节点索引:2 * i + 1
- 右子节点索引:2 * i + 2

2.4 堆的核心操作

🔼 上浮(Swim/Bubble Up)

插入新元素时,先放到数组末尾,然后不断与父节点比较,如果比父节点小就交换,直到满足堆性质。

插入元素 0 到堆中:

步骤1:先放到末尾
            1
           / \
          3   2
         / \ / \
        5  4 6  7
       /
      0  ← 新插入

步骤2:0 < 5,交换
            1
           / \
          3   2
         / \ / \
        0  4 6  7
       /
      5

步骤3:0 < 3,继续交换
            1
           / \
          0   2
         / \ / \
        3  4 6  7
       /
      5

步骤4:0 < 1,继续交换
            0  ← 最终成为堆顶
           / \
          1   2
         / \ / \
        3  4 6  7
       /
      5

🔽 下沉(Sink/Bubble Down)

删除堆顶元素时,用末尾元素替换堆顶,然后不断与子节点比较,与最小的子节点交换,直到满足堆性质。

删除堆顶元素 1:

步骤1:用末尾7替换堆顶1
            7  ← 末尾元素上来
           / \
          3   2
         / \ /
        5  4 6

步骤2:7 > 2(最小子节点),交换
            2
           / \
          3   7
         / \ /
        5  4 6

步骤3:7 > 6(最小子节点),交换
            2
           / \
          3   6
         / \ /
        5  4 7  ← 完成!

💻 三、Java中的PriorityQueue

3.1 基本使用

import java.util.*;

public class PriorityQueueDemo {
    public static void main(String[] args) {
        // 创建最小堆(默认)
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        
        // 添加元素
        minHeap.offer(5);
        minHeap.offer(3);
        minHeap.offer(7);
        minHeap.offer(1);
        minHeap.offer(9);
        
        System.out.println("=== 按优先级出队(从小到大)===");
        while (!minHeap.isEmpty()) {
            System.out.print(minHeap.poll() + " ");
        }
        // 输出:1 3 5 7 9
    }
}

3.2 创建最大堆

// 方法1:使用Collections.reverseOrder()
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

// 方法2:使用Lambda表达式
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>((a, b) -> b - a);

// 测试
maxHeap.offer(5);
maxHeap.offer(3);
maxHeap.offer(7);
maxHeap.offer(1);

System.out.println(maxHeap.poll());  // 7(最大值先出)
System.out.println(maxHeap.poll());  // 5
System.out.println(maxHeap.poll());  // 3

3.3 自定义对象的优先队列

// 定义学生类
class Student {
    String name;
    int score;
    
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    
    @Override
    public String toString() {
        return name + "(" + score + "分)";
    }
}

public class CustomPriorityQueue {
    public static void main(String[] args) {
        // 按分数从高到低排序(分数高的优先)
        PriorityQueue<Student> pq = new PriorityQueue<>(
            (s1, s2) -> s2.score - s1.score
        );
        
        pq.offer(new Student("张三", 85));
        pq.offer(new Student("李四", 92));
        pq.offer(new Student("王五", 78));
        pq.offer(new Student("赵六", 95));
        
        System.out.println("=== 按分数从高到低出队 ===");
        while (!pq.isEmpty()) {
            System.out.println(pq.poll());
        }
        // 输出:
        // 赵六(95分)
        // 李四(92分)
        // 张三(85分)
        // 王五(78分)
    }
}

3.4 常用API总结

方法说明时间复杂度
offer(e) / add(e)插入元素O(log n)
poll() / remove()删除并返回堆顶O(log n)
peek() / element()查看堆顶(不删除)O(1)
size()返回元素个数O(1)
isEmpty()判断是否为空O(1)
clear()清空队列O(n)
contains(o)是否包含元素O(n)

🎯 四、经典应用场景

4.1 Top K 问题 🏆

问题: 从海量数据中找出最大的K个元素

解法: 用大小为K的最小堆

public class TopKElements {
    public static int[] findTopK(int[] nums, int k) {
        // 使用最小堆,保持堆大小为k
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        
        for (int num : nums) {
            minHeap.offer(num);
            if (minHeap.size() > k) {
                minHeap.poll();  // 移除最小的
            }
        }
        
        // 堆中剩下的就是Top K
        int[] result = new int[k];
        for (int i = k - 1; i >= 0; i--) {
            result[i] = minHeap.poll();
        }
        return result;
    }
    
    public static void main(String[] args) {
        int[] nums = {3, 2, 1, 5, 6, 4, 9, 7, 8};
        int k = 3;
        int[] topK = findTopK(nums, k);
        System.out.println("Top " + k + ": " + Arrays.toString(topK));
        // 输出:Top 3: [7, 8, 9]
    }
}

为什么用最小堆?

找最大的K个元素,用最小堆:
- 堆顶是K个元素中最小的
- 新元素如果比堆顶大,就替换堆顶
- 最终堆里都是大元素

找最小的K个元素,用最大堆:
- 堆顶是K个元素中最大的
- 新元素如果比堆顶小,就替换堆顂
- 最终堆里都是小元素

4.2 合并K个有序链表 🔗

LeetCode 23 - Hard题

public class MergeKSortedLists {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) return null;
        
        // 最小堆,按节点值排序
        PriorityQueue<ListNode> pq = new PriorityQueue<>(
            (a, b) -> a.val - b.val
        );
        
        // 将每个链表的头节点加入堆
        for (ListNode node : lists) {
            if (node != null) {
                pq.offer(node);
            }
        }
        
        ListNode dummy = new ListNode(0);
        ListNode curr = dummy;
        
        while (!pq.isEmpty()) {
            // 取出最小的节点
            ListNode minNode = pq.poll();
            curr.next = minNode;
            curr = curr.next;
            
            // 如果该链表还有下一个节点,加入堆
            if (minNode.next != null) {
                pq.offer(minNode.next);
            }
        }
        
        return dummy.next;
    }
}

过程演示:

链表1: 1 → 4 → 5
链表2: 1 → 3 → 4
链表3: 2 → 6

步骤1:堆中放入三个头节点
堆:[1(链表1), 1(链表2), 2(链表3)]

步骤2:取出1(链表1),加入4
堆:[1(链表2), 2(链表3), 4(链表1)]

步骤3:取出1(链表2),加入3
堆:[2(链表3), 3(链表2), 4(链表1)]

...以此类推

最终结果:1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

4.3 任务调度器 ⏰

模拟操作系统的任务调度:

class Task {
    String name;
    int priority;  // 数字越大优先级越高
    
    public Task(String name, int priority) {
        this.name = name;
        this.priority = priority;
    }
    
    @Override
    public String toString() {
        return name + "(P" + priority + ")";
    }
}

public class TaskScheduler {
    public static void main(String[] args) {
        // 最大堆:优先级高的先执行
        PriorityQueue<Task> scheduler = new PriorityQueue<>(
            (t1, t2) -> t2.priority - t1.priority
        );
        
        // 添加任务
        scheduler.offer(new Task("打游戏", 1));
        scheduler.offer(new Task("写代码", 5));
        scheduler.offer(new Task("修复Bug", 10));
        scheduler.offer(new Task("开会", 7));
        scheduler.offer(new Task("吃饭", 8));
        
        System.out.println("=== 任务执行顺序 ===");
        int order = 1;
        while (!scheduler.isEmpty()) {
            Task task = scheduler.poll();
            System.out.println(order++ + ". " + task);
        }
    }
}

输出:

=== 任务执行顺序 ===
1. 修复Bug(P10)   ← 最高优先级
2. 吃饭(P8)
3. 开会(P7)
4. 写代码(P5)
5. 打游戏(P1)      ← 最低优先级

4.4 数据流中的中位数 📊

LeetCode 295 - Hard题

使用两个堆

  • 最大堆:存储较小的一半
  • 最小堆:存储较大的一半
class MedianFinder {
    // 最大堆:存储较小的一半数据
    private PriorityQueue<Integer> maxHeap;
    // 最小堆:存储较大的一半数据
    private PriorityQueue<Integer> minHeap;
    
    public MedianFinder() {
        maxHeap = new PriorityQueue<>((a, b) -> b - a);
        minHeap = new PriorityQueue<>();
    }
    
    public void addNum(int num) {
        // 先加入最大堆
        maxHeap.offer(num);
        // 将最大堆的堆顶移到最小堆
        minHeap.offer(maxHeap.poll());
        
        // 保持最大堆的大小 >= 最小堆
        if (maxHeap.size() < minHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
    }
    
    public double findMedian() {
        if (maxHeap.size() > minHeap.size()) {
            return maxHeap.peek();
        } else {
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        }
    }
}

// 测试
MedianFinder mf = new MedianFinder();
mf.addNum(1);
mf.addNum(2);
System.out.println(mf.findMedian());  // 1.5
mf.addNum(3);
System.out.println(mf.findMedian());  // 2.0

图解:

加入数据:1, 2, 3, 4, 5

最大堆(小的一半) | 最小堆(大的一半)
         3, 2, 1   |   4, 5
            ↑            ↑
         堆顶最大      堆顶最小

中位数 = 3(最大堆堆顶)

4.5 实时股票价格监控 📈

class StockPrice {
    String symbol;
    double price;
    
    public StockPrice(String symbol, double price) {
        this.symbol = symbol;
        this.price = price;
    }
}

public class StockMonitor {
    public static void main(String[] args) {
        // 最大堆:价格最高的股票
        PriorityQueue<StockPrice> highestPrices = new PriorityQueue<>(
            (s1, s2) -> Double.compare(s2.price, s1.price)
        );
        
        // 添加股票价格
        highestPrices.offer(new StockPrice("AAPL", 175.50));
        highestPrices.offer(new StockPrice("GOOGL", 140.30));
        highestPrices.offer(new StockPrice("MSFT", 380.20));
        highestPrices.offer(new StockPrice("AMZN", 155.80));
        
        System.out.println("价格最高的股票:");
        StockPrice highest = highestPrices.peek();
        System.out.println(highest.symbol + ": $" + highest.price);
        // 输出:MSFT: $380.20
    }
}

🎓 五、经典面试题

面试题1:PriorityQueue底层用什么实现?

答案:

  • 底层用数组存储
  • 逻辑结构是完全二叉树(堆)
  • Java的PriorityQueue默认是最小堆
  • 初始容量是11,扩容1.5倍或2倍(取决于当前容量)

面试题2:为什么堆的插入和删除都是O(log n)?

答案:

  • 堆是完全二叉树,高度为 log n
  • 插入:上浮操作最多交换 log n 次
  • 删除:下沉操作最多交换 log n 次

面试题3:如何在O(n)时间内建堆?

答案: 使用heapify(堆化)算法:

  • 从最后一个非叶子节点开始,从下往上执行下沉操作
  • 时间复杂度:O(n)(虽然看起来是O(n log n),但实际数学证明是O(n))
public void heapify(int[] arr) {
    int n = arr.length;
    // 从最后一个非叶子节点开始
    for (int i = n / 2 - 1; i >= 0; i--) {
        siftDown(arr, i, n);
    }
}

面试题4:PriorityQueue是线程安全的吗?

答案:

  • 不是线程安全的
  • 如需线程安全,使用PriorityBlockingQueue(并发包中的实现)

面试题5:如何找出数据流中的第K大元素?

答案: 维护一个大小为K的最小堆

class KthLargest {
    private PriorityQueue<Integer> minHeap;
    private int k;
    
    public KthLargest(int k, int[] nums) {
        this.k = k;
        this.minHeap = new PriorityQueue<>();
        for (int num : nums) {
            add(num);
        }
    }
    
    public int add(int val) {
        minHeap.offer(val);
        if (minHeap.size() > k) {
            minHeap.poll();
        }
        return minHeap.peek();
    }
}

🆚 六、优先队列 vs 普通队列

特性普通队列(Queue)优先队列(PriorityQueue)
出队顺序FIFO(先进先出)按优先级
底层实现数组/链表堆(数组)
插入时间O(1)O(log n)
删除时间O(1)O(log n)
查找最值O(n)O(1)
适用场景任务排队需要按优先级处理

🎨 七、手写最小堆

理解底层原理,手写一个简单的最小堆:

public class MinHeap {
    private int[] heap;
    private int size;
    private int capacity;
    
    public MinHeap(int capacity) {
        this.capacity = capacity;
        this.heap = new int[capacity];
        this.size = 0;
    }
    
    // 获取父节点索引
    private int parent(int i) {
        return (i - 1) / 2;
    }
    
    // 获取左子节点索引
    private int leftChild(int i) {
        return 2 * i + 1;
    }
    
    // 获取右子节点索引
    private int rightChild(int i) {
        return 2 * i + 2;
    }
    
    // 交换两个元素
    private void swap(int i, int j) {
        int temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }
    
    // 上浮操作
    private void siftUp(int i) {
        while (i > 0 && heap[i] < heap[parent(i)]) {
            swap(i, parent(i));
            i = parent(i);
        }
    }
    
    // 下沉操作
    private void siftDown(int i) {
        int minIndex = i;
        int left = leftChild(i);
        int right = rightChild(i);
        
        if (left < size && heap[left] < heap[minIndex]) {
            minIndex = left;
        }
        if (right < size && heap[right] < heap[minIndex]) {
            minIndex = right;
        }
        
        if (i != minIndex) {
            swap(i, minIndex);
            siftDown(minIndex);
        }
    }
    
    // 插入元素
    public void insert(int value) {
        if (size == capacity) {
            throw new IllegalStateException("Heap is full");
        }
        heap[size] = value;
        siftUp(size);
        size++;
    }
    
    // 删除并返回最小元素
    public int extractMin() {
        if (size == 0) {
            throw new IllegalStateException("Heap is empty");
        }
        int min = heap[0];
        heap[0] = heap[size - 1];
        size--;
        siftDown(0);
        return min;
    }
    
    // 查看最小元素
    public int peek() {
        if (size == 0) {
            throw new IllegalStateException("Heap is empty");
        }
        return heap[0];
    }
    
    // 测试
    public static void main(String[] args) {
        MinHeap heap = new MinHeap(10);
        heap.insert(5);
        heap.insert(3);
        heap.insert(7);
        heap.insert(1);
        heap.insert(9);
        
        System.out.println("最小值: " + heap.peek());  // 1
        System.out.println("出队: " + heap.extractMin());  // 1
        System.out.println("出队: " + heap.extractMin());  // 3
        System.out.println("出队: " + heap.extractMin());  // 5
    }
}

🎪 八、趣味小故事

故事:优先队列的VIP人生

我是优先队列PriorityQueue,江湖人称"VIP队列" 👑

我的工作地点是一家高级餐厅,专门负责安排客人入座顺序。

有一天,来了这些客人:

  • 👨 普通客人张三(优先级1)
  • 👨‍💼 VIP客人李四(优先级5)
  • 👑 超级VIP王五(优先级10)
  • 👴 老年人赵六(优先级8)

虽然张三最先到,但我的规则是:优先级高的先服务!

所以实际的入座顺序是:

  1. 👑 王五(优先级10)- "超级VIP请进!"
  2. 👴 赵六(优先级8)- "尊老爱幼,请!"
  3. 👨‍💼 李四(优先级5)- "VIP客人请!"
  4. 👨 张三(优先级1)- "不好意思,请稍等..."

张三不服:"我明明先来的!"

我说:"在我的世界里,不是先来先得,而是优先级决定一切!这就是优先队列的规则!" 😎

后来,我还帮助了很多场景:

  • 🏥 医院急诊室(病情严重的先治)
  • 💻 操作系统任务调度(重要任务先执行)
  • 📊 数据分析(找Top K)
  • 🎮 游戏开发(技能冷却队列)

虽然有人说我"势利眼",但在很多场景下,按优先级处理才是最合理的!💪


📚 九、知识点总结

核心要点 ✨

  1. 定义:优先队列按优先级出队,不是FIFO
  2. 实现:底层用堆(数组存储的完全二叉树)
  3. 类型:最小堆(小值优先)、最大堆(大值优先)
  4. 时间复杂度:插入O(log n)、删除O(log n)、查看堆顶O(1)
  5. Java实现:PriorityQueue(非线程安全)
  6. 经典应用:Top K、任务调度、数据流中位数

记忆口诀 🎵

优先队列不排队,
优先级高先出队。
底层实现是个堆,
完全二叉树排队。
最小堆顶是最小,
最大堆顶是最大。
插入删除log n快,
Top K问题最常来!

堆的核心公式 📐

// 数组索引从0开始
父节点索引 = (i - 1) / 2
左子节点索引 = 2 * i + 1
右子节点索引 = 2 * i + 2

使用场景对照表 🎯

场景使用什么堆原因
找最大的K个最小堆(size=K)堆顶最小,新元素比堆顶大就替换
找最小的K个最大堆(size=K)堆顶最大,新元素比堆顶小就替换
任务调度最大堆优先级高的先执行
合并K个链表最小堆每次取最小的节点

🎯 十、练习题

练习1:数组中的第K个最大元素(LeetCode 215)

public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    for (int num : nums) {
        minHeap.offer(num);
        if (minHeap.size() > k) {
            minHeap.poll();
        }
    }
    return minHeap.peek();
}

练习2:前K个高频元素(LeetCode 347)

public int[] topKFrequent(int[] nums, int k) {
    // 统计频率
    Map<Integer, Integer> freqMap = new HashMap<>();
    for (int num : nums) {
        freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
    }
    
    // 最小堆,按频率排序
    PriorityQueue<Map.Entry<Integer, Integer>> minHeap = 
        new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());
    
    for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
        minHeap.offer(entry);
        if (minHeap.size() > k) {
            minHeap.poll();
        }
    }
    
    int[] result = new int[k];
    for (int i = k - 1; i >= 0; i--) {
        result[i] = minHeap.poll().getKey();
    }
    return result;
}

🌟 十一、总结彩蛋

恭喜你!🎉 你已经掌握了优先队列这个超级实用的数据结构!

记住:

  • 🎯 优先队列 = 按优先级处理,不是FIFO
  • 🏔️ 底层用堆,数组存储的完全二叉树
  • ⚡ O(log n) 的插入删除,O(1) 查看堆顶
  • 🔥 Top K问题的神器!

最后送你一张图

         👑
        /  \
       /    \
      VIP  普通
     /  \  /  \
    💎 ⭐ 🌟 ✨
    
  优先队列:让重要的先走!

下次见,继续加油! 💪😄


📖 参考资料

  1. Java官方文档:PriorityQueue
  2. 《算法导论》第6章 - 堆排序
  3. 《数据结构与算法分析》- 堆的应用
  4. LeetCode题单:堆/优先队列专题

作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐⭐ (中高级)
预计学习时间: 3-4小时

💡 温馨提示:理解堆的上浮和下沉操作是关键,建议画图模拟整个过程!