堆与优先队列

Author Avatar
Brian Lee 8月 13, 2018
  • 在其它设备中阅读本文章

《Algorithm》(Sedgewick)笔记:堆与优先队列




定义

  • 堆的每个结点都大于等于两个子结点,这称为堆有序
  • 根结点是堆有序的二叉树中的最大结点
  • 一棵大小为 $N$ 的完全二叉树的高度为 $\lfloor lgN\rfloor+1$

实现

用数组pq[1..N]实现(不使用pq[0]),位置 $k$ 的结点的父节点位置为 $\lfloor k/2\rfloor$ ,两个子结点位置分别为 $2k$ 和 $2k+1$


图示

堆有序完全二叉树

堆表示


堆的算法

上浮

作用
  • 如果堆的有序状态因为某个结点变得比它父结点更大而打破,那么我们需要通过交换它和它的父节点来修复堆
  • 交换后继续比较,直到进入堆有序状态
代码
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;
        }
    }
图示

下沉

插入元素

作用
  • 将新元素加到数组末尾
  • 增加堆的大小
  • 让这个新元素上浮到合适的位置
代码
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;
    }
图示

删除



优先队列


定义

  • 可以删除最大元素(或最小)和插入元素的数据结构
  • 分为 最大优先队列最小优先队列

API

以最大优先队列为例

public class MaxPQ<Key extends Comparable<Key>>

MaxPQ() 创建一个优先队列
MaxPQ(int max) 创建一个最大容量为max的优先队列
MaxPQ(Key[] a) 用a[]中的元素创建一个优先队列
void Insert(Key v) 向优先队列中插入一个元素
Key max() 返回最大元素
Key delMax() 删除并返回最大元素
boolean isEmpty() 返回队列是否为空
int size() 返回优先队列中的元素个数

实现

可以使用无序数组与有序数组实现,但是时间复杂度不如使用 合理

数据结构 插入元素 删除最大元素
有序数组 $N$ $1$
无序数组 $1$ $N$
$logN$ $logN$

代码

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;
    }
    public void show() {
        for (int i = 1; i <= N; i++) {
            System.out.printf(pq[i] + " ");
        }
        System.out.println();
    }
    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;
        }
    }
}

时间复杂度

插入元素

$logN$

删除最大元素

$logN$

理由

两种操作都需要在根结点和堆底之间移动元素,而路径长度不超过 $logN$ 。


优先队列应用

描述

输入 $N$ 个数字,找出其中最小的 $M$ 个数字

思路

前 $M$ 个数字输入时构成大小为 $M$ 的基于堆的 最大 优先队列,之后的数字每来一个,将其插入优先队列,调整为堆有序后删除最大的元素。这样到最后剩下的就是最小的 $M$ 个元素。

复杂度

时间复杂度 :$O(MlogN)$

空间复杂度 :$O(M)$

代码

public class BottomM {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("Find the smallest M numbers in N numbers");
        System.out.println("Input M");
        int M = in.nextInt();
        System.out.println("Input N");
        int N = in.nextInt();
        MaxPQ<Integer> pq = new MaxPQ<Integer>(M + 1);  //多一个用于重排和删除
        for (int i = 0; i < N; i++) {
            Integer num = in.nextInt();
            pq.insert(num);
            //pq.show();
            if (pq.size() > M)
                pq.delMax();
        }
        Stack<Integer> stack = new Stack<Integer>();
        while (!pq.isEmpty())
            stack.push(pq.delMax());    //从大到小入栈
        for (int i = 0; i < M; i++) {
            System.out.println(stack.pop());    //从小到大出栈
        }
    }
}

源代码

https://github.com/XutongLi/Algorithm-Learn/tree/master/src/S2_Sorting/S2_4_4_2_MaxPriorityQueue


索引优先队列

在此以 最小索引优先队列 为例

定义

  • 给每个元素一个索引
  • 允许用例引用已经进入优先队列中的元素

API

public class IndexMinPQ<Item extends Comparable<Item>>

IndexMinPQ(int maxN) 创建一个最大容量为maxN的优先队列,索引的取值范围为0至maxN-1
void insert(int k, Item item) 插入一个元素,将它和索引k相关联
void change(int k, Item item) 将索引为k的元素设为item
boolean contains(int k) 是否存在索引为k的元素
void delete(int k) 删去索引k及其相关联的元素
Item min() 返回最小元素
int minIndex() 返回最小元素的索引
int delMin() 删除最小元素并返回它的索引
boolean isEmpty() 优先队列是否为空
int size() 优先队列中的元素数量

代码

public class IndexMinPQ<Key extends Comparable<Key>> {
    private int N = 0;      //PQ中的元素数量
    private int[] pq;   //索引二叉堆,从1开始(pq[顺序号]=索引号)
    private int[] qp;   //逆序:qp[pq[i]]=pq[qp[i]]=i(qp[索引号]=顺序号)
    private Key[] keys; //有优先级之分的元素

    public IndexMinPQ(int maxN) {
        keys = (Key[]) new Comparable[maxN + 1];
        pq = new int[maxN + 1];
        qp = new int[maxN + 1];
        for (int i = 0; i <= maxN; i++)
            qp[i] = -1;
    }
    public boolean isEmpty() {
        return N == 0;
    }
    public int size() {
        return N;
    }
    public boolean contains(int k) {
        return qp[k] != -1;
    }
    public void show() {
        for (int i = 1; i <= N; i++) {
            System.out.printf(keys[pq[i]] + " ");
        }
        System.out.println();
    }
    public void insert(int k, Key key) {
        N++;
        qp[k] = N;
        pq[N] = k;
        keys[k] = key;
        swim(N);
    }
    public Key min() {
        return keys[pq[1]];
    }
    public int delMin() {
        int idxOfMin = pq[1];
        exch(1, N--);
        sink(1);
        keys[pq[N + 1]] = null;
        qp[pq[N + 1]] = -1;
        return idxOfMin;
    }
    public int minIndex() {
        return pq[1];
    }
    public void change(int k, Key key) {
        keys[k] = key;
        swim(qp[k]);
        sink(qp[k]);
    }
    public void delete(int k) {
        exch(k, N--);
        swim(qp[k]);
        sink(qp[k]);
        keys[pq[N + 1]] = null;
        qp[pq[N + 1]] = -1;
    }
    private boolean less(int i, int j) {
        return keys[pq[i]].compareTo(keys[pq[j]]) < 0;
    }
    private void exch(int i, int j) {
        Key t = keys[pq[i]];
        keys[pq[i]] = keys[pq[j]];
        keys[pq[j]] = t;
    }
    private void swim(int k) {
        while (k > 1 && less(k, k / 2)) {
            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 + 1, j))
                j++;
            if (less(k, j))
                break;
            exch(k, j);
            k = j;
        }
    }
}

时间复杂度

操作 比较次数的增长数量级
insert() $logN$
change() $log N$
contains() $1$
delete() $logN$
min() $1$
minIndex() $1$
delMin() $logN$

源代码

https://github.com/XutongLi/Algorithm-Learn/tree/master/src/S2_Sorting/S2_4_4_6_IndexMinPQ


本博客采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!
本文链接:http://brianleelxt.top/2018/08/13/heapandPQ/