跳转至

优先队列

概述

优先队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都有一个**优先级**。出队操作总是返回优先级最高(或最低)的元素,而不是按照"先进先出"的原则。

核心特征:优先队列的出队顺序由元素的优先级决定,而非入队顺序。通常使用堆(Heap)来实现,保证高效的插入和删除操作。

与普通队列的对比

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    普通队列 vs 优先队列                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  普通队列(FIFO):                                                   │
│  ┌───┬───┬───┬───┬───┐                                             │
│  │ 3 │ 1 │ 4 │ 1 │ 5 │  入队顺序: 3, 1, 4, 1, 5                     │
│  └───┴───┴───┴───┴───┘  出队顺序: 3, 1, 4, 1, 5  (先进先出)        │
│    ↑                                                               │
│   出队                                                              │
│                                                                     │
│  优先队列(按优先级):                                               │
│  ┌───┬───┬───┬───┬───┐                                             │
│  │ 5 │ 4 │ 3 │ 1 │ 1 │  入队顺序: 3, 1, 4, 1, 5                     │
│  └───┴───┴───┴───┴───┘  出队顺序: 5, 4, 3, 1, 1  (大根堆,优先出大)│
│    ↑                                                               │
│   出队(最大值)                                                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

优先队列特点

1. 优先级出队

元素按照优先级顺序出队,而非入队顺序:

  • 大根堆:优先级高的元素先出(最大值优先)
  • 小根堆:优先级低的元素先出(最小值优先)
Text Only
大根堆优先队列示例(最大值优先):

入队操作序列: push(3), push(1), push(4), push(1), push(5)

时间线:
─────────────────────────────────────────────────────────────
操作           堆状态                 堆顶(最大值)
─────────────────────────────────────────────────────────────
push(3)        [3]                    3
push(1)        [3, 1]                 3
push(4)        [4, 1, 3]              4
push(1)        [4, 1, 3, 1]           4
push(5)        [5, 4, 3, 1, 1]        5
─────────────────────────────────────────────────────────────

出队顺序: 5 → 4 → 3 → 1 → 1(从大到小)

2. 动态维护

优先队列支持动态插入和删除,始终维护堆的性质:

flowchart LR
    A[初始堆] --> B[插入新元素]
    B --> C[上浮调整]
    C --> D[恢复堆性质]
    D --> E[删除堆顶]
    E --> F[下沉调整]
    F --> G[恢复堆性质]

3. 堆实现

优先队列通常使用**二叉堆**实现,二叉堆是一棵完全二叉树:

Text Only
二叉堆的两种形式:

大根堆(Max Heap):              小根堆(Min Heap):
        10                              1
       /  \                            / \
      9    8                          2   3
     / \  /                          / \  /
    7  6 5                          4  5 6
    
特点: 父节点 ≥ 子节点          特点: 父节点 ≤ 子节点

4. 高效操作

操作 时间复杂度 说明
插入 O(log n) 添加到末尾后上浮
删除最值 O(log n) 交换后下沉
获取最值 O(1) 直接访问堆顶
建堆 O(n) 从下往上依次下沉

原理详解

二叉堆的数组表示

二叉堆使用数组存储,利用完全二叉树的性质:

Text Only
完全二叉树的数组表示:

逻辑视图(树形):
              [1]
             /    \
          [2]      [3]
          / \      / \
        [4] [5]  [6] [7]

物理视图(数组):
索引:    1    2    3    4    5    6    7
      ┌────┬────┬────┬────┬────┬────┬────┐
      │    │    │    │    │    │    │    │
      └────┴────┴────┴────┴────┴────┴────┘
       堆顶
      (索引1)

索引关系(从索引1开始):
- 父节点索引: parent(i) = i / 2
- 左子节点索引: leftChild(i) = 2 * i
- 右子节点索引: rightChild(i) = 2 * i + 1

索引关系(从索引0开始):
- 父节点索引: parent(i) = (i - 1) / 2
- 左子节点索引: leftChild(i) = 2 * i + 1
- 右子节点索引: rightChild(i) = 2 * i + 2

堆的性质

Text Only
大根堆性质: 对于任意节点 i,data[i] ≥ data[2i] 且 data[i] ≥ data[2i+1]

小根堆性质: 对于任意节点 i,data[i] ≤ data[2i] 且 data[i] ≤ data[2i+1]

示例大根堆:
索引:    1    2    3    4    5    6    7
      ┌────┬────┬────┬────┬────┬────┬────┐
      │ 10 │  9 │  8 │  7 │  6 │  5 │  4 │
      └────┴────┴────┴────┴────┴────┴────┘

验证:
- data[1]=10 ≥ data[2]=9 ✓  且 data[1]=10 ≥ data[3]=8 ✓
- data[2]=9  ≥ data[4]=7 ✓  且 data[2]=9  ≥ data[5]=6 ✓
- data[3]=8  ≥ data[6]=5 ✓  且 data[3]=8  ≥ data[7]=4 ✓

上浮操作(Swim)

当插入新元素或增大某元素的值时,需要上浮以恢复堆性质:

Text Only
上浮操作原理(大根堆):

如果当前节点 > 父节点,则交换,继续向上检查

示例: 在大根堆中插入 15

初始状态:
              10
             /  \
            9    8
           / \  / \
          7  6 5  4
         /
        15  ← 新插入的节点(索引8)

步骤1: 比较 15 与父节点 7
       15 > 7,交换

              10
             /  \
            9    8
           / \  / \
         15  6 5  4
         /
        7

步骤2: 比较 15 与父节点 9
       15 > 9,交换

              10
             /  \
           15    8
           / \  / \
          9  6 5  4
         /
        7

步骤3: 比较 15 与父节点 10
       15 > 10,交换

              15
             /  \
           10    8
           / \  / \
          9  6 5  4
         /
        7

上浮完成!15 已到达堆顶

下沉操作(Sink)

当删除堆顶或减小某元素的值时,需要下沉以恢复堆性质:

Text Only
下沉操作原理(大根堆):

如果当前节点 < 较大的子节点,则交换,继续向下检查

示例: 删除大根堆的堆顶 15

初始状态:
              15
             /  \
           10    8
           / \  / \
          9  6 5  4
         /
        7

步骤1: 将末尾元素 7 移到堆顶,删除末尾

              7
             /  \
           10    8
           / \  / \
          9  6 5  4

步骤2: 比较 7 与较大的子节点(10 vs 8,选10)
       7 < 10,交换

              10
             /  \
            7    8
           / \  / \
          9  6 5  4

步骤3: 比较 7 与较大的子节点(9 vs 6,选9)
       7 < 9,交换

              10
             /  \
            9    8
           / \  / \
          7  6 5  4

下沉完成!堆性质已恢复

建堆过程

从无序数组构建堆,有两种方法:

Text Only
方法1: 逐个插入(O(n log n))
- 对每个元素执行 push 操作

方法2: 自底向上下沉(O(n))← 更高效
- 从最后一个非叶子节点开始,依次下沉

示例: 将数组 [4, 1, 3, 2, 16, 9, 10] 构建大根堆

初始数组:
索引:    1    2    3    4    5    6    7
      ┌────┬────┬────┬────┬────┬────┬────┐
      │  4 │  1 │  3 │  2 │ 16 │  9 │ 10 │
      └────┴────┴────┴────┴────┴────┴────┘

初始树形:
              4
            /   \
          1       3
         / \     / \
        2  16   9  10

最后一个非叶子节点索引 = 7/2 = 3(即值为3的节点)

从索引3开始下沉:
─────────────────────────────────────────────────────────────────
sink(3): 节点值=3,子节点=9和10
         较大子节点是10,3 < 10,交换

              4
            /   \
          1      10
         / \     / \
        2  16   9   3

─────────────────────────────────────────────────────────────────
sink(2): 节点值=1,子节点=2和16
         较大子节点是16,1 < 16,交换

              4
            /   \
         16      10
         / \     / \
        2   1   9   3

─────────────────────────────────────────────────────────────────
sink(1): 节点值=4,子节点=16和10
         较大子节点是16,4 < 16,交换

              16
            /   \
          4      10
         / \     / \
        2   1   9   3

继续下沉,检查节点4(现在是索引2)
         子节点=2和1,较大子节点是2,4 > 2,无需交换

─────────────────────────────────────────────────────────────────
建堆完成!

最终大根堆:
              16
            /   \
          4      10
         / \     / \
        2   1   9   3

数组: [16, 4, 10, 2, 1, 9, 3]

可视化演示

插入操作流程

sequenceDiagram
    participant PQ as 优先队列
    participant Array as 数组
    participant Heap as 堆结构
    
    Note over PQ: 初始状态: [10, 9, 8, 7]
    
    PQ->>Array: push(12)
    Array->>Array: data[++size] = 12
    Note over Array: 数组: [10, 9, 8, 7, 12]
    
    Note over Heap: 开始上浮调整
    Heap->>Heap: 比较 12 与父节点 9
    Note over Heap: 12 > 9,交换
    Note over Array: 数组: [10, 12, 8, 7, 9]
    
    Heap->>Heap: 比较 12 与父节点 10
    Note over Heap: 12 > 10,交换
    Note over Array: 数组: [12, 10, 8, 7, 9]
    
    Note over PQ: 插入完成,堆顶为 12

删除操作流程

sequenceDiagram
    participant PQ as 优先队列
    participant Array as 数组
    participant Heap as 堆结构
    
    Note over PQ: 当前状态: [12, 10, 8, 7, 9]
    
    PQ->>Array: pop() 返回堆顶 12
    Array->>Array: data[1] = data[size--]
    Note over Array: 数组: [9, 10, 8, 7]
    
    Note over Heap: 开始下沉调整
    Heap->>Heap: 比较 9 与子节点 10, 8
    Note over Heap: 较大子节点是 10,9 < 10,交换
    Note over Array: 数组: [10, 9, 8, 7]
    
    Note over Heap: 检查 9 的子节点(无)
    Note over PQ: 删除完成,堆顶为 10

大根堆完整操作演示

Text Only
操作序列: push(10), push(20), push(15), push(30), pop(), pop()

═══════════════════════════════════════════════════════════════
初始状态: 空堆
═══════════════════════════════════════════════════════════════

数组: [空]
树形: (空)

═══════════════════════════════════════════════════════════════
push(10): 插入后上浮
═══════════════════════════════════════════════════════════════

数组: [10]
树形:    10

═══════════════════════════════════════════════════════════════
push(20): 插入后上浮
═══════════════════════════════════════════════════════════════

插入到末尾:
        10
       /
      20

上浮: 20 > 10,交换
        20
       /
      10

数组: [20, 10]

═══════════════════════════════════════════════════════════════
push(15): 插入后上浮
═══════════════════════════════════════════════════════════════

插入到末尾:
        20
       /  \
      10   15

上浮: 15 < 20,无需交换

数组: [20, 10, 15]

═══════════════════════════════════════════════════════════════
push(30): 插入后上浮
═══════════════════════════════════════════════════════════════

插入到末尾:
        20
       /  \
      10   15
     /
    30

上浮: 30 > 10,交换
        20
       /  \
      30   15
     /
    10

继续上浮: 30 > 20,交换
        30
       /  \
      20   15
     /
    10

数组: [30, 20, 15, 10]

═══════════════════════════════════════════════════════════════
pop(): 删除堆顶 30,返回 30
═══════════════════════════════════════════════════════════════

将末尾元素 10 移到堆顶:
        10
       /  \
      20   15

下沉: 10 与子节点 20, 15 比较
      较大子节点是 20,10 < 20,交换
        20
       /  \
      10   15

继续下沉: 10 无子节点,停止

数组: [20, 10, 15]

═══════════════════════════════════════════════════════════════
pop(): 删除堆顶 20,返回 20
═══════════════════════════════════════════════════════════════

将末尾元素 15 移到堆顶:
        15
       /
      10

下沉: 15 与子节点 10 比较
      15 > 10,无需交换

数组: [15, 10]

═══════════════════════════════════════════════════════════════
最终状态
═══════════════════════════════════════════════════════════════

数组: [15, 10]
树形:    15
        /
       10

出队顺序: 30, 20, ...(剩余: 15, 10)

代码实现

数组实现(大根堆)

C
typedef struct {
    int *data;       // 数据数组(从索引1开始存储)
    int size;        // 当前元素数量
    int capacity;    // 容量
} PriorityQueue;

// 创建优先队列
PriorityQueue* createPQ(int capacity) {
    PriorityQueue *pq = (PriorityQueue*)malloc(sizeof(PriorityQueue));
    pq->data = (int*)malloc(sizeof(int) * (capacity + 1));  // 索引0不使用
    pq->size = 0;
    pq->capacity = capacity;
    return pq;
}

// 交换两个元素
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 上浮操作(大根堆)
void swim(int *data, int index) {
    // 当当前节点大于父节点时,向上交换
    while (index > 1 && data[index / 2] < data[index]) {
        swap(&data[index / 2], &data[index]);
        index = index / 2;  // 移动到父节点位置
    }
}

// 下沉操作(大根堆)
void sink(int *data, int size, int index) {
    // 当有子节点时
    while (2 * index <= size) {
        int j = 2 * index;  // 左子节点
        // 选择较大的子节点
        if (j < size && data[j] < data[j + 1]) j++;
        // 如果当前节点已经大于等于子节点,停止
        if (data[index] >= data[j]) break;
        // 交换并继续下沉
        swap(&data[index], &data[j]);
        index = j;
    }
}

// 插入元素
void push(PriorityQueue *pq, int value) {
    if (pq->size >= pq->capacity) return;
    
    pq->data[++pq->size] = value;  // 添加到末尾
    swim(pq->data, pq->size);      // 上浮调整
}

// 获取堆顶元素
int top(PriorityQueue *pq) {
    if (pq->size == 0) return -1;
    return pq->data[1];
}

// 删除并返回堆顶元素
int pop(PriorityQueue *pq) {
    if (pq->size == 0) return -1;
    
    int max = pq->data[1];              // 保存堆顶
    pq->data[1] = pq->data[pq->size--]; // 将末尾元素移到堆顶
    sink(pq->data, pq->size, 1);        // 下沉调整
    
    return max;
}

// 判空
int isEmpty(PriorityQueue *pq) {
    return pq->size == 0;
}
C++
#include <vector>
#include <algorithm>

template<typename T, typename Compare = std::less<T>>
class PriorityQueue {
private:
    std::vector<T> data;  // 从索引0开始
    Compare comp;         // 比较器
    
    // 上浮操作
    void swim(int index) {
        while (index > 0) {
            int parent = (index - 1) / 2;
            if (comp(data[parent], data[index])) {
                std::swap(data[parent], data[index]);
                index = parent;
            } else {
                break;
            }
        }
    }
    
    // 下沉操作
    void sink(int index) {
        int n = data.size();
        while (2 * index + 1 < n) {
            int left = 2 * index + 1;
            int right = 2 * index + 2;
            int target = index;
            
            // 选择优先级更高的子节点
            if (left < n && comp(data[target], data[left])) {
                target = left;
            }
            if (right < n && comp(data[target], data[right])) {
                target = right;
            }
            
            if (target == index) break;
            std::swap(data[index], data[target]);
            index = target;
        }
    }
    
public:
    PriorityQueue() = default;
    
    void push(const T& value) {
        data.push_back(value);
        swim(data.size() - 1);
    }
    
    void pop() {
        if (data.empty()) return;
        data[0] = data.back();
        data.pop_back();
        if (!data.empty()) sink(0);
    }
    
    T top() const { return data.front(); }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
};

// STL使用示例
#include <queue>
void demonstratePriorityQueue() {
    // 大根堆(默认)
    std::priority_queue<int> maxPQ;
    maxPQ.push(3);
    maxPQ.push(1);
    maxPQ.push(4);
    
    // 小根堆(使用 std::greater)
    std::priority_queue<int, std::vector<int>, std::greater<int>> minPQ;
    minPQ.push(3);
    minPQ.push(1);
    minPQ.push(4);
}
Python
import heapq

# 使用内置heapq(小根堆)
min_heap = []
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 4)

# 弹出最小值
print(heapq.heappop(min_heap))  # 输出: 1

# 大根堆实现(取负值)
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -4)
print(-heapq.heappop(max_heap))  # 输出: 4

# 完整实现
class PriorityQueue:
    def __init__(self, is_min_heap=True):
        self.data = []
        self.is_min_heap = is_min_heap
    
    def push(self, value):
        if self.is_min_heap:
            heapq.heappush(self.data, value)
        else:
            heapq.heappush(self.data, -value)
    
    def pop(self):
        if not self.data:
            return None
        if self.is_min_heap:
            return heapq.heappop(self.data)
        else:
            return -heapq.heappop(self.data)
    
    def top(self):
        if not self.data:
            return None
        return self.data[0] if self.is_min_heap else -self.data[0]
    
    def empty(self):
        return len(self.data) == 0
    
    def size(self):
        return len(self.data)
Java
import java.util.PriorityQueue;
import java.util.Collections;

// 使用内置PriorityQueue(小根堆)
PriorityQueue<Integer> minPQ = new PriorityQueue<>();
minPQ.offer(3);
minPQ.offer(1);
minPQ.offer(4);

// 弹出最小值
System.out.println(minPQ.poll());  // 输出: 1

// 大根堆
PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Collections.reverseOrder());
maxPQ.offer(3);
maxPQ.offer(1);
maxPQ.offer(4);
System.out.println(maxPQ.poll());  // 输出: 4

// 数组实现版本
class BinaryHeap {
    private int[] data;
    private int size;
    private int capacity;
    
    public BinaryHeap(int capacity) {
        this.data = new int[capacity + 1];  // 索引0不使用
        this.size = 0;
        this.capacity = capacity;
    }
    
    private void swim(int index) {
        while (index > 1 && data[index / 2] < data[index]) {
            int temp = data[index / 2];
            data[index / 2] = data[index];
            data[index] = temp;
            index = index / 2;
        }
    }
    
    private void sink(int index) {
        while (2 * index <= size) {
            int j = 2 * index;
            if (j < size && data[j] < data[j + 1]) j++;
            if (data[index] >= data[j]) break;
            int temp = data[index];
            data[index] = data[j];
            data[j] = temp;
            index = j;
        }
    }
    
    public void push(int value) {
        data[++size] = value;
        swim(size);
    }
    
    public int pop() {
        int max = data[1];
        data[1] = data[size--];
        sink(1);
        return max;
    }
    
    public int top() {
        return data[1];
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
}
Go
package main

import "container/heap"

// 使用内置heap接口
type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

// 数组实现版本
type PriorityQueue struct {
    data     []int
    size     int
    capacity int
}

func NewPQ(capacity int) *PriorityQueue {
    return &PriorityQueue{
        data:     make([]int, capacity+1),
        size:     0,
        capacity: capacity,
    }
}

func (pq *PriorityQueue) swim(index int) {
    for index > 1 && pq.data[index/2] < pq.data[index] {
        pq.data[index/2], pq.data[index] = pq.data[index], pq.data[index/2]
        index = index / 2
    }
}

func (pq *PriorityQueue) sink(index int) {
    for 2*index <= pq.size {
        j := 2 * index
        if j < pq.size && pq.data[j] < pq.data[j+1] {
            j++
        }
        if pq.data[index] >= pq.data[j] {
            break
        }
        pq.data[index], pq.data[j] = pq.data[j], pq.data[index]
        index = j
    }
}

func (pq *PriorityQueue) Push(value int) {
    pq.size++
    pq.data[pq.size] = value
    pq.swim(pq.size)
}

func (pq *PriorityQueue) Pop() int {
    max := pq.data[1]
    pq.data[1] = pq.data[pq.size]
    pq.size--
    pq.sink(1)
    return max
}

func (pq *PriorityQueue) Top() int {
    return pq.data[1]
}

func (pq *PriorityQueue) Empty() bool {
    return pq.size == 0
}
Rust
use std::collections::BinaryHeap;

// 使用内置BinaryHeap(大根堆)
let mut max_heap: BinaryHeap<i32> = BinaryHeap::new();
max_heap.push(3);
max_heap.push(1);
max_heap.push(4);

// 弹出最大值
println!("{:?}", max_heap.pop());  // 输出: Some(4)

// 小根堆(使用Reverse)
use std::cmp::Reverse;
let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new();
min_heap.push(Reverse(3));
min_heap.push(Reverse(1));
min_heap.push(Reverse(4));

// 数组实现版本
struct PriorityQueue {
    data: Vec<i32>,
}

impl PriorityQueue {
    fn new(capacity: usize) -> Self {
        let mut data = Vec::with_capacity(capacity + 1);
        data.push(0);  // 索引0不使用
        PriorityQueue { data }
    }
    
    fn swim(&mut self, mut index: usize) {
        while index > 1 && self.data[index / 2] < self.data[index] {
            self.data.swap(index / 2, index);
            index = index / 2;
        }
    }
    
    fn sink(&mut self, mut index: usize) {
        let size = self.data.len() - 1;
        while 2 * index <= size {
            let mut j = 2 * index;
            if j < size && self.data[j] < self.data[j + 1] {
                j += 1;
            }
            if self.data[index] >= self.data[j] {
                break;
            }
            self.data.swap(index, j);
            index = j;
        }
    }
    
    fn push(&mut self, value: i32) {
        self.data.push(value);
        self.swim(self.data.len() - 1);
    }
    
    fn pop(&mut self) -> Option<i32> {
        if self.data.len() <= 1 {
            return None;
        }
        let max = self.data[1];
        let last = self.data.pop().unwrap();
        if self.data.len() > 1 {
            self.data[1] = last;
            self.sink(1);
        }
        Some(max)
    }
    
    fn top(&self) -> Option<i32> {
        if self.data.len() <= 1 {
            None
        } else {
            Some(self.data[1])
        }
    }
    
    fn is_empty(&self) -> bool {
        self.data.len() <= 1
    }
}

复杂度分析

时间复杂度

操作 时间复杂度 说明
插入 push O(log n) 最多上浮树高次
删除堆顶 pop O(log n) 最多下沉树高次
获取堆顶 top O(1) 直接访问 data[1]
建堆 buildHeap O(n) 从下往上依次下沉

建堆复杂度推导

Text Only
建堆时间复杂度分析(自底向上方法):

设堆的高度为 h = log(n)

第 k 层的节点数量: 2^(h-k)
第 k 层节点最多下沉: k 次

总时间 T(n) = Σ(k=0 to h) 2^(h-k) * k
            = 2^h * Σ(k=0 to h) k / 2^k
            = n * Σ(k=0 to h) k / 2^k
            < n * Σ(k=0 to ∞) k / 2^k
            = n * 2
            = O(n)

结论: 建堆可以在 O(n) 时间内完成!

空间复杂度

  • 数组实现: O(n),存储所有元素
  • 链式实现: O(n),存储节点和指针

应用场景

1. 合并K个有序链表

使用小根堆维护K个链表的当前节点:

C
typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;

typedef struct {
    int val;           // 节点值
    int listIndex;     // 来自哪个链表
} HeapNode;

ListNode* mergeKLists(ListNode **lists, int k) {
    MinPQ *pq = createMinPQ(k);
    
    // 将各链表头节点入堆
    for (int i = 0; i < k; i++) {
        if (lists[i]) {
            pushMin(pq, lists[i]->val);
        }
    }
    
    ListNode dummy = {0, NULL};
    ListNode *tail = &dummy;
    
    while (!isEmpty(pq)) {
        tail->next = (ListNode*)malloc(sizeof(ListNode));
        tail->next->val = popMin(pq);
        tail->next->next = NULL;
        tail = tail->next;
    }
    
    return dummy.next;
}

算法图解:

Text Only
输入: 3个有序链表
list1: 1 → 4 → 5
list2: 1 → 3 → 4
list3: 2 → 6

初始堆状态(各链表头节点):
          1
         / \
        1   2

合并过程:
───────────────────────────────────────────────────────────────
步骤   操作           堆状态        输出链表
───────────────────────────────────────────────────────────────
 1    pop() → 1      [1, 2]        1 →
 2    pop() → 1      [2, 3]        1 → 1 →
 3    pop() → 2      [3, 4]        1 → 1 → 2 →
 4    pop() → 3      [4, 4]        1 → 1 → 2 → 3 →
 5    pop() → 4      [4, 5]        1 → 1 → 2 → 3 → 4 →
 6    pop() → 4      [5, 6]        1 → 1 → 2 → 3 → 4 → 4 →
 7    pop() → 5      [6]           1 → 1 → 2 → 3 → 4 → 4 → 5 →
 8    pop() → 6      []            1 → 1 → 2 → 3 → 4 → 4 → 5 → 6
───────────────────────────────────────────────────────────────

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

2. Top K 问题

找出数组中出现频率最高的K个元素:

C
int* topKFrequent(int nums[], int n, int k, int *returnSize) {
    // 统计频率
    int freq[10000] = {0};
    for (int i = 0; i < n; i++) {
        freq[nums[i] + 5000]++;
    }
    
    // 使用小根堆维护Top K
    MinPQ *pq = createMinPQ(k + 1);
    int *result = (int*)malloc(sizeof(int) * k);
    *returnSize = 0;
    
    for (int i = 0; i < 10000; i++) {
        if (freq[i] > 0) {
            pushMin(pq, freq[i]);
            // 保持堆大小为K
            if (pq->size > k) {
                popMin(pq);
            }
        }
    }
    
    while (!isEmpty(pq)) {
        result[(*returnSize)++] = popMin(pq);
    }
    
    return result;
}

算法思路:

Text Only
使用小根堆维护Top K的原理:

───────────────────────────────────────────────────────────────
输入: nums = [1,1,1,2,2,3], k = 2

统计频率:
  1 → 3次
  2 → 2次
  3 → 1次

处理过程:
───────────────────────────────────────────────────────────────
步骤   入堆元素    堆状态(小根堆)    说明
───────────────────────────────────────────────────────────────
 1     (1, 3)     [3]                入堆频率3
 2     (2, 2)     [2, 3]             入堆频率2
 3     (3, 1)     [1, 3]             入堆频率1
                  ↓ pop() → 1        堆大小超过k,弹出最小
        [2, 3]                       堆大小恢复为k
───────────────────────────────────────────────────────────────

结果: 频率最高的2个元素是 1(3次) 和 2(2次)

3. 任务调度

优先级调度算法:

C
typedef struct {
    int id;        // 任务ID
    int priority;  // 优先级
    int arrival;   // 到达时间
    int burst;     // 执行时间
} Task;

void scheduleTasks(Task tasks[], int n) {
    PriorityQueue *pq = createPQ(n);
    
    int time = 0;
    int completed = 0;
    
    while (completed < n) {
        // 将已到达的任务加入队列
        for (int i = 0; i < n; i++) {
            if (tasks[i].arrival <= time && tasks[i].burst > 0) {
                push(pq, tasks[i].priority);
            }
        }
        
        // 执行优先级最高的任务
        if (!isEmpty(pq)) {
            int priority = pop(pq);
            printf("Time %d: Execute task with priority %d\n", time, priority);
            completed++;
        }
        
        time++;
    }
}

调度示例:

Text Only
任务列表:
┌────────┬──────────┬───────────┬───────────┐
│ Task ID │ Priority │ Arrival   │ Burst     │
├────────┼──────────┼───────────┼───────────┤
│   T1   │    3     │    0      │    2      │
│   T2   │    5     │    1      │    3      │
│   T3   │    1     │    2      │    1      │
│   T4   │    4     │    3      │    2      │
└────────┴──────────┴───────────┴───────────┘

调度过程:
───────────────────────────────────────────────────────────────
时间    就绪队列         执行任务      说明
───────────────────────────────────────────────────────────────
 0     [T1]            T1(pri=3)    T1到达,执行
 1     [T1, T2]        T2(pri=5)    T2到达,优先级更高
 2     [T1, T2, T3]    T2(pri=5)    继续执行T2
 3     [T1, T3, T4]    T4(pri=4)    T4到达,优先级最高
 4     [T1, T3, T4]    T4(pri=4)    继续执行T4
 5     [T1, T3]        T1(pri=3)    T1优先级最高
 6     [T3]            T3(pri=1)    执行T3
───────────────────────────────────────────────────────────────

执行顺序: T1 → T2 → T2 → T4 → T4 → T1 → T3

4. Dijkstra 最短路径

使用优先队列优化Dijkstra算法:

flowchart LR
    A[起点入堆<br/>dist=0] --> B[取出最小距离节点]
    B --> C[更新邻接节点距离]
    C --> D{堆是否为空?}
    D -->|否| B
    D -->|是| E[算法结束]

5. Huffman 编码

构建最优前缀编码树:

Text Only
Huffman编码建树过程:
输入字符频率: a:5, b:9, c:12, d:13, e:16, f:45

步骤1: 初始小根堆
          5
         / \
        9  12
       / \  /
      13 16 45

步骤2: 合并最小的两个节点 (5, 9) → 14
          12
         /  \
       13    14
       /    /  \
      16   45   (5,9)

步骤3: 合并 (12, 13) → 25
          14
         /  \
       16    25
       /    /  \
      45  (12,13) (5,9)

...继续合并直到只剩一棵树

最终编码:
a: 1100    b: 1101    c: 100    d: 101    e: 111    f: 0

参考资料