2.4优先队列

有时候不是所有的数据都需要完全排序,可以选择一些优先级的数据来处理.例如支持,删除最大元素和插入元素,这种数据叫优先队列.优先队列的使用和队列或栈相同,但是更高效,实现更具有挑战性.

2.4.1 API

优先队列是一种抽象数据类型,最重要的操作就是删除最大元素和插入元素.

优先队列使用场景: 输入N个字符串,每个字符串都映射一个整数,任务是找出里面的最大或者最少的M个整数(及其字符串).在某些场景中输入量可能是非常巨大,甚至认为是无限输入的.解决的方法??
因为数据量之巨大,所以无法进行重新排序,然后在筛选;所以只能依靠一种插入时即可以进行排序,然后可以delMin的方法.
这就是堆排序.

2.4.3 堆的定义

在二叉堆的数组中,每个元素都要保证大于或等于其子节点,这些位置的元素又要大于或等于数组中的另外两元素,以此类推.

定义:

当一颗二叉树的每个节点都大于等于他的两个子节点时,称堆有序.

相应的么个堆有序的二叉树中,每个节点都小于等于他的父节点;从任意节点向上都能找到非递减的元素,而从任意节点向下,都能找到非递增的元素.

命题O:

根节点是堆有序二叉树的最大节点.
用完全二叉树表达非常方便,只需要数组,不需要指针就可以表示堆有序二叉树.

定义:

二叉堆是能用堆有序的完全二叉树排序的元素,并在数组中按层级储存(不适用数组的第一个位置).二叉堆简单起见,称堆.

在堆中,K节点的父节点位置为K/2,其两个子节点为 2K ,和2K+1.
用堆存储,能实现对数级别的插入数据和删除最大元素.

命题P:

一颗大小N的完全二叉树高度为lgN.

表示一个大小为N的堆,不会使用pq[0],数据放置在pq[1]~pq[N]中. 因为考虑到 顶点和左右子节点的关系,N2,N2+1 (所以考虑N≠0)

2.4.1 由下至上的堆有序化(上浮)

需要保证,比父节点小与或等于即可,如果比父节点大,则交换父节点;然后继续比较父节点与他的前k/2元素大小.

private void swim(int k){
  while(k>1&&arr[k]>arr[k/2]){
    swap(k,k/2);
    k=k/2;
   }
}
2.4.2 由上至下的堆有序化(下沉)
private void sink(int k){
      while(2k<=N){
           int j=2k;
           if(j<N&&arr[j]<arr[j+1]){
                  j++;
    }
          if(arr[k]>=arr[j]){
             break;     }
         swap(k,maxIndx);
         k=maxIndx;
     }
}

堆的插入和删除正好是利用swim,和shink 来实现堆重拍序.

插入元素

增加堆的大小,在数组尾部增加一个元素,增加堆大小,并且利用swim(),将尾部的元素上浮到合适的位置.

删除元素

删除数组最顶部(数组左边arr[1])的元素减去一个对大小,并且把数组尾部的元素调整到最顶部(左边 arr[1]),对顶部的新元素进行shink下沉到合适的位置.

《2.4优先队列》 image.png

代码如下:

package algorithm.sortAlgorithm.PriortyQueen;
/**
 * Created by leon on 18-1-27.
 */
public class MaxPQ<Key extends Comparable<Key>> {
    private Key[] prioryQueen;
    private int node = 0;
    public MaxPQ(int max) {
        prioryQueen = (Key[]) new Comparable[max + 1];
    }
    public boolean isEmpty() {
        return node == 0;package
    }
    public int size() {
        return node;
    }
    public void insert(Key v) {
        prioryQueen[++node] = v;
        swim(node);
    }
    public Key delMax() {
        if (isEmpty()) {
            return null;
        }
        Key max = prioryQueen[1];
        swap(1, node--);
        prioryQueen[node + 1] = null;
        shink(1);
        return max;
    }
    private boolean isLess(int i, int j) {
        return prioryQueen[i].compareTo(prioryQueen[j]) < 0;
    }
    private void swap(int i, int j) {
        Key temp = prioryQueen[i];
        prioryQueen[j] = prioryQueen[i];
        prioryQueen[i] = temp;
    }
    /**
     * 上浮 ,因为 如果插入的是 左节点或者右节点  k/2都是它的顶点.
     *
     * @param k
     */
    private void swim(int k) {
        while (k > 1 && isLess(k / 2, k)) {
            swap(k / 2, k);
            k = k / 2;
        }
    }
    /**
     * k作为 顶点,需要考虑 顶点与他的 左右子节点的大小,
     *
     * @param k
     */
    private void shink(int k) {
        while (2 * k <= node) {
            //这里有一个技巧,先引入一个j,记录左右子节点最大的节点号,
            // 同时判断下是否存在2k+1 的右子节点
            int j = 2 * k;
            if ((2 * k + 1) <= node && isLess(2 * k, 2 * k + 1)) {
                j++;
            }
            if (!isLess(k, j)) {
                break;
            }
            swap(k, j);
            k = j;
        }
    }
}
命题Q:

对于一个长度为N的元素基于堆的优先队列,插入一个元素,不超过(lgN+1)次比较;删除最大元素不会超过2lgN次比较.
对于需要大量插入和删除最大元素的操作的典型数据来说,命题Q是一个重要性的突破.因为使用有序或者无序的数组来存储元素总需要 线性级别时间完成这种操作,而用堆排序的优先队列则需要对数级别时间.

2.4.4.3多叉堆

可以采用多叉堆来扩展堆排序优先队列,例如:3叉堆 使得k节点 >=[3k-1,3k,3k+1] ,k节点<=(k+1)3.
扩展下,对于任意R叉堆,都会满足下面的性质.对于 k节点:

  • parent(k)节点=(k+R-2)/R
  • farLeft(k) 最左子节点:farLeft=(k-1)*R+2
  • farRight(k)最右子节点 =k*R+1
2.4.4.4带索引优先队列

因为优先队列不能直接访问在优先队列中的对象,并更新他们.所以出现了一种更加方便的优先队列-→索引.优先队列.
索引优先队列.我们创建两个数组分别是pq,elements。elements的作用是存储对象的引用,我们将每个对象存储在与之相关的整数作为下标的位置中,elements存储的对象不一定在数组中连续存放。pq存储是与对象相关的整数值,注意数组pq是连续存放的。此时pq作为优先队列,但是在上浮和下沉操作中,我们比较的是pq中值作为下标的elements数组中的值。这样我们就可以实现快速索引。
下图中,我们以字符串作为存储的对象类型,建立一个索引优先队列

《2.4优先队列》 image.png

从中我们可以看出,我们设计数组pq数组的目的。我们只需要对pq中的数值进行维护就可以实现一个优先队列,而elements中的对象的位置保持不变(出列时会置为null),这样就可以方便我们快速索引。比如通过elements数组我们可以知道与整数3相关的字符串为“f”。

在图中,我们插入一个与整数10相关的字符串“b”后,pq和elements中的值如下图所示。

《2.4优先队列》 image.png

假设在上图的基础上,我们要将与整数3相关的字符串修改为“a”,那么我们只需要让elements[3] = “a”即可。然后去维护pq中的值。但是在维护pq中的值时出现了一个问题,我们不知道pq中哪个位置中的值为3,只能从都到尾遍历,找到这个元素所在的位置后进行上浮和下沉操作(因为我们必须通过下标才能快速找到父节点或者孩子节点)。
为了能够快速找到****pq****中元素值对应的下标,我们需要额外设置一个数组****qp****,它的作用是存储与对象相关的整数在****pq****数组中的下标,并在上浮和下沉的过程中同时维护它。
《2.4优先队列》 image.jpeg

在上述的基础上,假设我们需要将与整数3相关的字符串修改为“a”,那么我们只需要让elements[3] = “a”,然后通过qp[3]中的值2就可以知道数组pq中值为3的下标为2,然后对pq[2]进行上浮或下沉操作。这里显然需要进行上浮操作,那么我们要交换pq[1]和pq[2]的值。这个时候我们需要注意的是,在交换pq数组中的两个元素的值时,我们也需要交换qp对应两个元素的值,因为与对象相关的整数在pq的不同位置上,那么显然该整数在pq所在的下标也变了,所以qp中的值也应该发生变化。而需要交换的qp中的两元素的下标正好就是pq中两元素的值。结果如下图所示。所以我们也需要交换qp[3]和qp[10]的值。

《2.4优先队列》 image.jpeg


/*************************************************************************
 *  Compilation:  javac IndexMinPQ.java
 *  Execution:    java IndexMinPQ
 *
 *  Indexed PQ implementation using a binary heap.
 *
 *********************************************************************/
import java.util.Iterator;
import java.util.NoSuchElementException;
public class IndexMinPQ<Key extends Comparable<Key>> implements Iterable<Integer> {
    private int N;           // number of elements on PQ
    private int[] pq;        // binary heap using 1-based indexing
    private int[] qp;        // inverse of pq - qp[pq[i]] = pq[qp[i]] = i
    private Key[] keys;      // keys[i] = priority of i
    public IndexMinPQ(int NMAX) {
        keys = (Key[]) new Comparable[NMAX + 1];    // make this of length NMAX??
        pq   = new int[NMAX + 1];
        qp   = new int[NMAX + 1];                   // make this of length NMAX??
        for (int i = 0; i <= NMAX; i++) qp[i] = -1;
    }
    // is the priority queue empty?
    public boolean isEmpty() { return N == 0; }
    // is k an index on the priority queue?
    public boolean contains(int k) {
        return qp[k] != -1;
    }
    // number of keys in the priority queue
    public int size() {
        return N;
    }
    // associate key with index k
    public void insert(int k, Key key) {
        if (contains(k)) throw new RuntimeException("item is already in pq");
        N++;
        qp[k] = N;
        pq[N] = k;
        keys[k] = key;
        swim(N);
    }
    // return the index associated with a minimal key
    public int min() {
        if (N == 0) throw new RuntimeException("Priority queue underflow");
        return pq[1];
    }
    // return a minimal key
    public Key minKey() {
        if (N == 0) throw new RuntimeException("Priority queue underflow");
        return keys[pq[1]];
    }
    // delete a minimal key and returns its associated index
    public int delMin() {
        if (N == 0) throw new RuntimeException("Priority queue underflow");
        int min = pq[1];
        exch(1, N--);
        sink(1);
        qp[min] = -1;            // delete
        keys[pq[N+1]] = null;    // to help with garbage collection
        pq[N+1] = -1;            // not needed
        return min;
    }
/*
    // change key associated with index k; insert if index k is not present
    public void put(int k, Key key) {
        if (!contains(k)) insert(k, key);
        else changeKey(k, key);
    }
    // return key associated with index k
    public Key get(int k) {
        if (!contains(k)) throw new RuntimeException("item is not in pq");
        else return keys[pq[k]];
    }
*/
    // change the key associated with index k
    public void change(int k, Key key) {
        if (!contains(k)) throw new RuntimeException("item is not in pq");
        keys[k] = key;
        swim(qp[k]);
        sink(qp[k]);
    }
    // decrease the key associated with index k
    public void decrease(int k, Key key) {
        if (!contains(k)) throw new RuntimeException("item is not in pq");
        if (keys[k].compareTo(key) <= 0) throw new RuntimeException("illegal decrease");
        keys[k] = key;
        swim(qp[k]);
    }
    // increase the key associated with index k
    public void increase(int k, Key key) {
        if (!contains(k)) throw new RuntimeException("item is not in pq");
        if (keys[k].compareTo(key) >= 0) throw new RuntimeException("illegal decrease");
        keys[k] = key;
        sink(qp[k]);
    }
    /**************************************************************
     * General helper functions
     **************************************************************/
    private boolean greater(int i, int j) {
        return keys[pq[i]].compareTo(keys[pq[j]]) > 0;
    }
    private void exch(int i, int j) {
        int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap;
        qp[pq[i]] = i; qp[pq[j]] = j;
    }
    /**************************************************************
     * Heap helper functions
     **************************************************************/
    private void swim(int k)  {
        while (k > 1 && greater(k/2, k)) {
            exch(k, k/2);
            k = k/2;
        }
    }
    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && greater(j, j+1)) j++;
            if (!greater(k, j)) break;
            exch(k, j);
            k = j;
        }
    }
    /***********************************************************************
     * Iterators
     **********************************************************************/
    /**
     * Return an iterator that iterates over all of the elements on the
     * priority queue in ascending order.
     * <p>
     * The iterator doesn't implement <tt>remove()</tt> since it's optional.
     */
    public Iterator<Integer> iterator() { return new HeapIterator(); }
    private class HeapIterator implements Iterator<Integer> {
        // create a new pq
        private IndexMinPQ<Key> copy;
        // add all elements to copy of heap
        // takes linear time since already in heap order so no keys move
        public HeapIterator() {
            copy = new IndexMinPQ<Key>(pq.length - 1);
            for (int i = 1; i <= N; i++)
                copy.insert(pq[i], keys[pq[i]]);
        }
        public boolean hasNext()  { return !copy.isEmpty();                     }
        public void remove()      { throw new UnsupportedOperationException();  }
        public Integer next() {
            if (!hasNext()) throw new NoSuchElementException();
            return copy.delMin();
        }
    }
    public static void main(String[] args) {
        // insert a bunch of strings
        String[] strings = { "it", "was", "the", "best", "of", "times", "it", "was", "the", "worst" };
        IndexMinPQ<String> pq = new IndexMinPQ<String>(strings.length);
        for (int i = 0; i < strings.length; i++) {
            pq.insert(i, strings[i]);
        }
        // delete and print each key
        while (!pq.isEmpty()) {
            int i = pq.delMin();
            StdOut.println(i + " " + strings[i]);
        }
        StdOut.println();
        // reinsert the same strings
        for (int i = 0; i < strings.length; i++) {
            pq.insert(i, strings[i]);
        }
        // print each key using the iterator
        for (int i : pq) {
            StdOut.println(i + " " + strings[i]);
        }
        while (!pq.isEmpty()) {
            pq.delMin();
        }
    }
}
2.4.5堆排序

我们可以把任意的优先队列变成一种排序方法.例如用最小优先队列进行,按从小到大的排序.
只需要每次取最小的元素,然后删除,把最小的元素进行重排序.这样的方法就是堆排序.

堆排序分两个阶段:

1.构造堆
2.提取,删除最小值,重拍序.

构造堆的方法: 2种:
  • 1.对于未知长度的元素(动态添加元素)来构造堆,一般采用swim (上浮法)时间复杂度NlgN
  • 2.对于已知长度的元素(静态元素),一般采用shink(下沉法)

1.用swim方法构造堆,相当于把数据插入到数组的尾部,然后遍历swim上浮,比较arr[k/2]与arr[k]大小,如果刚好arr[k/2]>=arr[k]就停止,否则交换位置,继续比较arr[k/2] arr[(k/2)/2]大小,直到k>1.
2.用shink的方法,从最后一棵数的 顶点开始构造.因为在优先队列中,堆的parent,和left,Child,rightChild的关系有 k, 2K,2K+1. 所以最后一棵二叉数的parent的是数组的 1/2处.

分析下shink方法的时间复杂度:O(N)

《2.4优先队列》 image.png

假设树的高度为h,整个树的个数为n.
考察最底层,因为最底层有可能只有一个叶子,也可能全部布满了叶子.
所以 2h≤n≤2h+1-1<2h+1 //有高度为h时最大数量为 2h+1-1
分析:h≤lgn<h+1,所以得出,

结论1:
一个N个元素的树,高度为lgN 向下取整.

我们再来看我们分析的第二个结论。对应树每一个高度的一层,该有多少个元素呢?假设高度为1的那一层他们的元素个数为k,那么他们的访问时间复杂度为O(k)。根据前面的分析,我们还发现一个情况,就是如果从根结点开始计数,往下第i层的元素如果不是最后一层的话,这一层的元素数量为2**i(2的i次方)。正好这个第i层就相当于树的总高度减去当前层的结点的高度。假设第i层的高度为h,那么也就是i = lgn – h。

结论2:

这一层的元素数量为:

《2.4优先队列》 image.jpeg

那么他们所有元素的运算时间总和为如下:

《2.4优先队列》 image.jpeg

根据如下公式:

《2.4优先队列》 image.jpeg

则有:

《2.4优先队列》 image.jpeg

现在,我们发现,buildMaxHeap方法的时间复杂度实际上为O(n).

而swim构造堆的时间复杂度为O(NlgN)

分析Swim 方式的时间复杂度:先分析最优情况,也就一次刚好比parent小,不需要重排.时间复杂度就是常数1次O(N);最坏情况,直到最顶层的父节点才比较完长度为lgN.

《2.4优先队列》 image.png

堆排序的代码

package algorithm.sortAlgorithm.PriortyQueen;
import algorithm.sortAlgorithm.StdOut;
/*************************************************************************
 *  Compilation:  javac Heap.java
 *  Execution:    java Heap < input.txt
 *  Dependencies: StdOut.java StdIn.java
 *  Data files:   http://algs4.cs.princeton.edu/24pq/tiny.txt
 *                http://algs4.cs.princeton.edu/24pq/words3.txt
 *
 *  Sorts a sequence of strings from standard input using heapsort.
 *
 *  % more tiny.txt
 *  S O R T E X A M P L E
 *
 *  % java Heap < tiny.txt
 *  S O R T E X A M P L E A               [ one string per line ]
 *
 *  % more words3.txt
 *  bed bug dad yes zoo ... all bad yet
 *
 *  % java Heap < words3.txt
 *  all bad bed bug dad ... yes yet zoo   [ one string per line ]
 *
 *************************************************************************/
public class Heap {
    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);
        }
    }
    /***********************************************************************
     * Helper functions to restore the heap invariant.
     **********************************************************************/
    private static void sink(Comparable[] pq, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && less(pq, j, j + 1)) j++;
            if (!less(pq, k, j)) break;
            exch(pq, k, j);
            k = j;
        }
    }
    /***********************************************************************
     * Helper functions for comparisons and swaps.
     * Indices are "off-by-one" to support 1-based indexing.
     **********************************************************************/
    private static boolean less(Comparable[] pq, int i, int j) {
        return pq[i - 1].compareTo(pq[j - 1]) < 0;
    }
    private static void exch(Object[] pq, int i, int j) {
        Object swap = pq[i - 1];
        pq[i - 1] = pq[j - 1];
        pq[j - 1] = swap;
    }
    // is v < w ?
    private static boolean less(Comparable v, Comparable w) {
        return (v.compareTo(w) < 0);
    }
    /***********************************************************************
     *  Check if array is sorted - useful for debugging
     ***********************************************************************/
    private static boolean isSorted(Comparable[] a) {
        for (int i = 1; i < a.length; i++)
            if (less(a[i], a[i - 1])) return false;
        return true;
    }
    // print array to standard output
    private static void show(Comparable[] a) {
        for (int i = 0; i < a.length; i++) {
            StdOut.println(a[i]);
        }
    }
}
优先队列的作用:
1.如果有海量数据,希望寻找Top10的元素,因为数据量太大,无法排序,因此可以只构造一个大小为10的优先队列,数据小于前10个数自动丢弃.
2.可以实现在常数时间级寻找最大数
    原文作者:浩林Leon
    原文地址: https://www.jianshu.com/p/cda29fa79b91
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞