優先隊列實現原理分析

引言

優先隊列是在實際工程中被普遍應用的一種數據結構,無論是在操做系統的進程調度中,仍是在相關的圖算法好比Prim算法和Dijkstra算法中,咱們均可以看到優先隊列的身影,本文咱們就來分析一下優先隊列的實現原理。java

優先隊列

以操做系統的進程調度爲例,好比咱們在使用手機的過程當中,手機分配給來電的優先級都會比其它程序高,在這個業務場景中,咱們不要求全部元素所有有序,由於咱們須要處理的只是當前鍵值最大的元素(優先級最高的進程)。在這種狀況下,咱們須要實現的只是刪除最大的元素(獲取優先級最高的進程)和插入新的元素(插入新的進程),這種數據結構就叫作優先隊列。git

咱們先來定義一個優先隊列,下面咱們將使用pq[]來保存相關的元素,在構造函數中能夠指定堆的初始化大小,若是不指定初始化大小值,默認初始化值爲1。p.s: 在下面咱們會實現相關的resize()方法用來動態調整數組的大小。github

public class MaxPQ<Key> implements Iterable<Key> {
    private Key[] pq;                    // store items at indices 1 to n
    private int n;                       // number of items on priority queue
    private Comparator<Key> comparator;  // optional Comparator

    /**
     * Initializes an empty priority queue with the given initial capacity.
     *
     * @param  initCapacity the initial capacity of this priority queue
     */
    public MaxPQ(int initCapacity) {
        pq = (Key[]) new Object[initCapacity + 1];
        n = 0;
    }

    /**
     * Initializes an empty priority queue.
     */
    public MaxPQ() {
        this(1);
    }
}

堆的基本概念

在正式進入優先隊列分析以前,咱們有必要先了解一下對於堆的相關操做。咱們定義當一棵二叉樹的每一個結點都要大於等於它的兩個子結點的時候,稱這棵二叉樹堆有序。以下圖就是一棵典型的堆有序的徹底二叉樹。算法

堆有序的徹底二叉樹

堆上浮和下沉操做

對了保證堆有序,對於堆咱們要對它進行上浮和下沉操做,咱們先來實現兩個經常使用的工具方法,其中less()用於比較兩個元素的大小,exch()用於交換數組的兩個元素:數組

private boolean less(int i, int j) {
    if (comparator == null) {
        return ((Comparable<Key>) pq[i]).compareTo(pq[j]) < 0;
    }
    else {
        return comparator.compare(pq[i], pq[j]) < 0;
    }
}

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

上浮操做

根據下圖咱們首先來分析一下上浮操做,以swim(5)爲例子,咱們來看一下上浮的過程。對於堆咱們進行上浮的目的是保持堆有序性,即一個結點的值大於它的子結點的值,因此咱們將a[5]和它的父結點a[2]相比較,若是它大於父結點的值,咱們就交換二者,而後繼續swim(2)數據結構

上浮操做

具體的實現代碼以下:less

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

下沉操做

根據下圖咱們來分析一下下沉操做,以sink(2)爲例子,咱們先將結點a[2]和它兩個子結點中較小的結點相比較,若是小於子結點,咱們就交換二者,而後繼續sink(5)函數

下沉操做

具體的實現代碼以下:工具

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;
    }
}

實現

咱們來分析一下插入一個元素的過程,若是咱們要在堆中新插入一個元素S的話,首先咱們默認將這個元素插入到數組中pq[++n] 中(數組是從1開始計數的)。當咱們插入S後,打破了堆的有序性,因此咱們採用上浮操做來維持堆的有序性,當上浮操做結束以後,咱們依然能夠保證根結點的元素是數組中最大的元素。this

接下來咱們來看一下刪除最大元素的過程,咱們首先將最大的元素a[1]a[n]交換,而後咱們刪除最大元素a[n],這個時候堆的有序性已經被打破了,因此咱們繼續經過下沉操做來從新維持堆的有序性,保持根結點元素是全部元素中最大的元素。

插入元素和刪除最大元素

插入的實現代碼以下:

/**
* Adds a new key to this priority queue.
*
* @param  x the new key to add to this priority queue
*/
public void insert(Key x) {

   // double size of array if necessary
   if (n >= pq.length - 1) resize(2 * pq.length);

   // add x, and percolate it up to maintain heap invariant
   pq[++n] = x;
   swim(n);
   assert isMaxHeap();
}

刪除的實現代碼以下:

/**
 * Removes a maximum key and returns its associated index.
 *
 * @return an index associated with a maximum key
 * @throws NoSuchElementException if this priority queue is empty
 */
public Key delMax() {
    if (isEmpty()) throw new NoSuchElementException("Priority queue underflow");
    Key max = pq[1];
    exch(1, n);
    n--;
    sink(1);
    pq[n+1] = null;     // to avoid loiterig and help with garbage collection
    if ((n > 0) && (n == (pq.length - 1) / 4)) resize(pq.length / 2);
    assert isMaxHeap();
    return max;
}

上面咱們在insert()過程當中用到了resize()函數,它用於動態數組的大小,具體的實現代碼以下:

// helper function to double the size of the heap array
private void resize(int capacity) {
    assert capacity > n;
    Key[] temp = (Key[]) new Object[capacity];
    for (int i = 1; i <= n; i++) {
        temp[i] = pq[i];
    }
    pq = temp;
}


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

isMaxHeap()則用於判斷當前數組是否知足堆有序原則,這在debug的時候很是的有用,具體的實現代碼以下:

// is pq[1..N] a max heap?
private boolean isMaxHeap() {
    return isMaxHeap(1);
}

// is subtree of pq[1..n] rooted at k a max heap?
private boolean isMaxHeap(int k) {
    if (k > n) return true;
    int left = 2*k;
    int right = 2*k + 1;
    if (left  <= n && less(k, left))  return false;
    if (right <= n && less(k, right)) return false;
    return isMaxHeap(left) && isMaxHeap(right);
}

到此咱們的優先隊列已經差很少完成了,注意咱們上面實現了Iterable<Key>接口,因此咱們來實現iterator()方法:

/**
 * Returns an iterator that iterates over the keys on this priority queue
 * in descending order.
 * The iterator doesn't implement remove() since it's optional.
 *
 * @return an iterator that iterates over the keys in descending order
 */
public Iterator<Key> iterator() {
    return new HeapIterator();
}

private class HeapIterator implements Iterator<Key> {

    // create a new pq
    private MaxPQ<Key> copy;

    // add all items to copy of heap
    // takes linear time since already in heap order so no keys move
    public HeapIterator() {
        if (comparator == null) copy = new MaxPQ<Key>(size());
        else                    copy = new MaxPQ<Key>(size(), comparator);
        for (int i = 1; i <= n; i++)
            copy.insert(pq[i]);
    }

    public boolean hasNext()  { return !copy.isEmpty();                     }
    public void remove()      { throw new UnsupportedOperationException();  }

    public Key next() {
        if (!hasNext()) throw new NoSuchElementException();
        return copy.delMax();
    }
}

堆排序

將上面的優先隊列稍微作一下改進,咱們即可以實現堆排序,即對pq[]中的元素進行排序。對於堆排序的具體實現,下面咱們分爲兩個步驟:

  1. 首先咱們先來構造一個堆。
  2. 而後經過下沉的方式進行排序。

堆排序的實現代碼很是的簡短,咱們首先來看一下具體的代碼實現,而後咱們再具體分析它的實現原理:

/**
 * Rearranges the array in ascending order, using the natural order.
 * @param pq the array to be sorted
 */
public static void sort(Comparable[] pq) {
    int n = pq.length;
    for (int k = n/2; k >= 1; k--)
        sink(pq, k, n);
    while (n > 1) {
        exch(pq, 1, n--);
        sink(pq, 1, n);
    }
}

首先咱們來看一下堆的構造過程(下圖中的左圖)。咱們採用的方法是從右至左用sink()方法構造子堆。咱們只須要掃描數組中的一半元素,即5, 4, 3, 2, 1。這樣經過這幾個步驟,咱們能夠獲得一個堆有序的數組,即每一個結點的大小都大於它的兩個結點,並使最大元素位於數組的開頭。

接下來咱們來分析一下下沉排序的實現(下圖中的右圖),這裏咱們採起的方法是每次都將最大的元素刪除,而後從新經過sink()來維持堆有序,這樣每一次sink()操做咱們均可以的到數組中最大的元素。

堆排序過程

Referencs

ALGORITHM-4TH

Contact

GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site

本文爲做者原創,轉載請與開頭明顯處聲明博客出處:)

相關文章
相關標籤/搜索