Skip to content

优先队列

1. 概念与基本原理

优先队列是一种特殊的队列,其中每个元素都有一个优先级。在处理元素时,具有最高优先级的元素最先被处理。与普通队列(先进先出)和栈(后进先出)不同,优先队列按元素的优先级进行处理。

2. 实现方式

无序数组实现

  • 插入操作:将元素添加到数组末尾(O(1))
  • 删除最大元素:遍历找到最大元素,与最后一个元素交换后删除(O(N))

有序数组实现

  • 插入操作:找到合适位置插入,保持数组有序(O(N))
  • 删除最大元素:直接返回并删除数组末尾元素(O(1))

链表实现

与数组实现类似,可以选择维护有序或无序链表

2.2 各种实现的性能比较

数据结构 插入元素 删除最大元素
有序数组 N 1
无序数组 1 N
logN logN
理想情况 1 1

3. 堆的定义与结构

3.1 堆有序

当一棵二叉树的每个节点都大于等于它的两个子节点时,称为堆有序。在堆有序的二叉树中,从任意节点向上,会得到一列非递减的元素;从任意节点向下,会得到一列非递增的元素。

3.2 二叉堆

二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按层级顺序存储(通常不使用数组的第一个位置)。主要特点:

  • 位置k的节点的父节点位于k/2处
  • 位置k的节点的子节点位于2k和2k+1处
  • 大小为N的完全二叉树的高度为⌊lgN⌋

3.3 表示方法

数组索引:  0  1  2  3  4  5  6  7  8  9  10
数组内容:  -  T  S  R  P  N  O  A  E  I  H
堆表示:       T
           /   \
          S     R
         / \   / \
        P   N O   A
       / \  /
      E  I H

4. 堆的基本操作

4.1 上浮操作(由下至上的堆有序化)

当某个节点的优先级上升(比如插入新元素)时,为恢复堆的有序性,需要将该节点与其父节点比较,如果其大于父节点则交换位置,直到满足堆有序要求。

private void swim(int k) {
    while (k > 1 && less(k/2, k)) {
        exch(k/2, k);
        k = k/2;
    }
}

4.2 下沉操作(由上至下的堆有序化)

当某个节点的优先级下降(比如替换为一个较小的元素)时,为恢复堆的有序性,需要将该节点与其较大的子节点比较,如果其小于子节点则交换位置,直到满足堆有序要求。

private void sink(int k) {
    while (2*k <= N) {
        int j = 2*k;
        if (j < N && less(j, j+1)) j++;
        if (!less(k, j)) break;
        exch(k, j);
        k = j;
    }
}

5. 堆的核心操作实现

5.1 插入元素

将新元素加到数组末尾,增加堆的大小,然后让这个新元素上浮到正确位置。

public void insert(Key v) {
    pq[++N] = v;       // 将新元素加到数组末尾
    swim(N);           // 恢复堆的有序性
}

5.2 删除最大元素

从数组顶端删去最大元素,将数组的最后一个元素放到顶端,减小堆的大小,然后让这个元素下沉到正确位置。

public Key delMax() {
    Key max = pq[1];              // 获取最大元素
    exch(1, N--);                 // 将其与最后一个元素交换
    pq[N+1] = null;               // 防止对象游离
    sink(1);                      // 恢复堆的有序性
    return max;                   // 返回最大元素
}

5.3 性能分析

  • 在含有N个元素的基于堆的优先队列中:
    • 插入元素操作需要不超过(lgN+1)次比较
    • 删除最大元素操作需要不超过2lgN次比较

6. 完整的优先队列实现

public class MaxPQ<Key extends Comparable<Key>> {
    private Key[] pq;                    // 基于堆的完全二叉树
    private int N = 0;                   // 存储于pq[1..N]中,pq[0]未使用

    public MaxPQ(int maxN) { 
        pq = (Key[]) new Comparable[maxN+1]; 
    }

    public boolean isEmpty() { 
        return N == 0; 
    }

    public int size() { 
        return N; 
    }

    public void insert(Key v) {
        pq[++N] = v;
        swim(N);
    }

    public Key delMax() {
        Key max = pq[1];
        exch(1, N--);
        pq[N+1] = null;
        sink(1);
        return max;
    }

    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    private void exch(int i, int j) {
        Key t = pq[i]; pq[i] = pq[j]; pq[j] = t;
    }

    private void swim(int k) {
        while (k > 1 && less(k/2, k)) {
            exch(k/2, k);
            k = k/2;
        }
    }

    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && less(j, j+1)) j++;
            if (!less(k, j)) break;
            exch(k, j);
            k = j;
        }
    }
}

7. 堆排序算法

堆排序利用堆的性质进行排序,分为两个阶段:

  1. 堆的构造阶段:将无序数组重新组织安排成一个堆
  2. 下沉排序阶段:从堆中按递减顺序取出所有元素

7.1 堆的构造

从右至左使用sink()函数构造子堆,效率高于从左至右使用swim()。

// 堆的构造
for (int k = N/2; k >= 1; k--)
    sink(a, k, N);

堆的构造只需少于2N次比较以及少于N次交换。

7.2 下沉排序

重复删除最大元素并将其放入数组末尾空出的位置。

// 下沉排序
while (N > 1) {
    exch(a, 1, N--);
    sink(a, 1, N);
}

7.3 完整堆排序实现

public static void sort(Comparable[] a) {
    int N = a.length;
    // 堆的构造阶段
    for (int k = N/2; k >= 1; k--)
        sink(a, k, N);
    // 下沉排序阶段
    while (N > 1) {
        exch(a, 1, N--);
        sink(a, 1, N);
    }
}

private static void sink(Comparable[] a, int k, int N) {
    while (2*k <= N) {
        int j = 2*k;
        if (j < N && less(a, j, j+1)) j++;
        if (!less(a, k, j)) break;
        exch(a, k, j);
        k = j;
    }
}

private static boolean less(Comparable[] a, int i, int j) {
    return a[i-1].compareTo(a[j-1]) < 0;
}

private static void exch(Comparable[] a, int i, int j) {
    Comparable t = a[i-1];
    a[i-1] = a[j-1];
    a[j-1] = t;
}

7.4 堆排序性能分析

  • 将N个元素排序:
    • 堆排序需要少于(2NlgN+2N)次比较
    • 以及一半次数的交换
  • 空间复杂度:O(1),原地排序
  • 时间复杂度:O(N log N),最优化利用时间和空间的排序算法
  • 缺点:无法利用现代计算机的缓存机制,实际应用中可能不如快排和归并排序