数据结构基础-优先队列和堆

优先队列概念

优先队列可以看做队列的一种,区别在于,在优先队列中,元素进入队列的顺序可能与其被操作的顺序不同。他支持插入(Insert)和删除最小值(DeleteMin)操作(返回并删除最小元素)或删除最大值(DeleteMax)操作(返回并删除最大元素)。


20190131174534.png

优先队列应用

以操作系统的进程调度为例,用户使用手机过程中,来电的优先级比较高,我们不要求所有的元素有序,只处理当前键值最大的那个就可以了。在这种情况下,我们需要实现的只是删除键值最大的元素(获取优先级最高的进程)和插入新的元素(插入新的进程)。
其他例子:

  • 数据压缩:好夫曼编码算法
  • 最短路径算法:Dijkstra算法
  • 事件驱动仿真:顾客排队算法
  • 选择问题:查找第k个最小元素
  • 最小生成树算法:Prim算法

优先队列ADT

优先队列是元素的容器,每一个元素有一个相关键值。

  1. 优先队列主要操作
  • Insert(key,data):插入键为key的数据到优先队列中,元素以key进行排序
  • DeleteMin/DeleteMax:删除并返回最小/最大键值的元素
  • GetMiniMum/GetMaxinum:返回最小/最大键值的元素,但不删除他
  1. 辅助操作
  • 第k最小/第k最大:返回队列中键值为第k个最小/最大的元素
  • 大小(Size):返回队列中元素个数
  • 堆排序(Heap Sort):基于键值的优先级将优先队列中元素进行排序

在正式进入优先队列分析之前,我们有必要先了解一下对于堆的概念和相关操作。我们定义当一棵二叉树的每个结点都要大于等于它的两个子结点的时候,称这棵二叉树堆有序。

堆的基本概念

堆是一棵具有特殊性质的二叉树,堆的基本要求是结点的值必须大于等于(或小于等于)其孩子结点的值。除此还有另一个特性,就是当h>0,所有叶子结点都位于第h或h-1层(其中h为树的高度),所有堆是一棵完全二叉树。

堆的分类
  • 最小堆:结点的值必须等于或者小于其孩子结点的值
  • 最大堆:结点的值必须大于或等于其孩子结点的值
堆的声明
public class Heap{
    public int[] array;
    public int count;
    public int capacity;
    public int heapType;
    public Heap(int capacity, int heapType)
    public Parent(int capacity, int heapType)
    public int leftChild(int i)
    public int rightCHild(int i)
    public int getMaximum(int i)
}
创建堆
public Heap(int capacity, int heapType){
    this.heapType = heapType;
    this.count = 0;
    this.capacity = capacity;
    this.array = new int[capacity];
}
结点的双亲
public int parent(int i){
    if(i <= 0 ||i >= this.count)
        return -1;
    return i-1/2;
}
结点的孩子
public int leftChild(int i){
    int left = 2*i + 1;
    if(left >= this.count)
        return -1;
    return left;
}

public int rightChild(int i){
    int right = 2*i + 2;
    if(right >= this.count)
        return -1;
    return right;
}


获取最大元素
public int getMaximum(){
    if(this.count == 0) return -1;
    return this.array[0];
}
堆化元素

堆化其实是执行某些操作后或者建立堆的时候会有让堆不满足堆的特性的风险,所以有相应操作去堆化元素。

下沉操作(siftDown)多见于建立堆或者删除最大或最小值操作后。

递归版本:

public void siftDown(int k){
    int l,r,max,temp;
    l = leftChild(k);
    r = rightChild(k);
    if(l != -1 && array[l] > array[k]){
        max = l;
    }else{
        max = k;
    }
    if(r != -1 && array[r] > array[max]){
        max = r;
    }
    if(max != i){
        temp = array[i];
        array[i] = array[max];
        array[max] = temp;
    }
    sifDown(max);
}

迭代版本:

public void siftDown(int k) {
    while(2*k <= n){
        int j = 2*k;
        if(j < n && array[j] < array[j+1]) j++;
        if(array[k] > array[j]) break;
        int tmp = array[j];
        array[j] = array[k];
        array[k] = tmp;
        k = j;
    }
}

上浮操作(siftUp)多见于插入操作后

public void siftUp(int k) {
    while(k > 0 && array[k] > array[(k-1)/2]){
        int tmp = array[k];
        array[k] = array[(k-1)/2];
        array[(k-1)/2] = array[k];
        k = k-1 / 2;
    }
}
删除元素

下面文章只讨论最大堆的情况,因为最小堆其实是最大堆的对称情况。删除元素就是删除二叉堆中根结点,然后将最后一个结点复制到根结点,然后删除最后元素。当用最后元素替换根结点后可能会导致不满足堆的性质。为之能再次成为堆,需要堆化(下沉)。

int deleteMax(){
    if(count == 0){
        return -1;
    }
    int data = array[0];
    array[0] = array[count-1];
    count--;
    siftDown(0);
}
插入元素

类似于堆化和删除过程

  1. 堆大小加1
  2. 将新元素放在堆的尾部
  3. 从下至上堆化这个元素
int insert(int data){
    int i;
    if(count == capacity){
        resizeHeap();
    }
    count++;
    i = count - 1;
    while(i >= 0 && data > array[(i-1)/2]){
        array[i] = array[(i-1)/2];
        i = i-1/2;
    }
    this.array[i] = data;
}

void resizeHeap(){
    int[] oldArray = new int[capacity];
    System.arraycopy(array,o,oldArray,count-1);
    array = new int[capacity*2];
    for(int i=0;i<capacity;i++)
        array[i] = oldArray[i];
    capacity *=2;
    oldArray = null;
}
数组建堆

tips:叶子结点总是满足堆的性质,所以在数组放入堆后要满足堆的性质堆化总是要关注非叶子结点。所以先要关注如何找到第一个非叶子结点。堆的最后一个元素位置是count-1,通过他能找到最后一个非叶子结点。

void buildHeap(Heap h, int A[], int n){
    if(h == null) return;
    while(n > capacity){
        h.resizeHeap();
    }
    for(int i=0;i<n;i++){
        h.array[i]=a[i];
    }
    h.count=n;
    for(int i=(n-1)/2;i>=0;i--){
        h.siftDown(i);
    }
}
堆排序
void heapSort(int A[], int n){
    Heap h = new Heap(n,0);
    int oldSize,i,temp;
    buildHeap(h,A,n);
    oldSize = h.count;
    for(i=n-1;i>0;i--){
        temp = h.array[0];h.array[0]=h.array[h.count-1];h.array[h.count-1]=temp;
        h.count--;
        h.siftDown(i);
    }
    h.count=oldSize;
}

优先队列的相关问题

eg1:高度为h的堆,其最小元素个数和最大元素个数是多少?
解:因为堆是一棵完全二叉树(除了最底层,其他所有层都是满的),它最多有2h+1-1个元素,最少是2h - 1 + 1 = 2^h(当最底层只有一个元素,而其他各层都满时)。

eg2:请说出n个元素的堆的高度为什么是logn?
解:由上得,2^h <= n <= 2^h+1 -1,因为h为整数,所有h=logn。

eg3:从最小堆中删除任意元素的算法。
解:先找到元素然后删掉,接下来的操作类似于删除最小元素。

int delete(Heap p, int i){
    int key;
    key = h.array[i];
    h.array[i]=h.array[h.count-1];
    h.siftDown(i);
    return key;
}

eg4:给出最小堆中找出第k小元素的算法。

解:借助一个辅助最小堆来实现。假定原始最小堆为HOrig,辅助最小堆为HAux。初始化时,将HOrig顶部最小放入HAux中。

Heap HOrig,HAux;
int findKthLargestEle(int k){
    int heapElement;
    int count = 1;
    HAux.insert(HOrig.deleteMin());
    while(true){
        heapElement = HAux.deleteMin();
        if(++count == k){
            return heapElement;
        }else{
            HAux.insert(heapElement.leftChild());
            HAux.insert(heapElement.rightChild);
        }
    }
}

eg5:给定一个包含上百万个数字的大文件,如何在这个文件中找出最大的10个值?
解:当需要找到最大n个元素时,最好的数据结构是优先队列。
把数据分割为1000个元素的集合,然后创建为堆。然后依次从堆中取出10个元素。最后采用堆对10个元素的集合进行排序,取出前10个元素。但是这需要耗费很大的内存。
重复使用前面堆的10个元素就可以解决这个问题。就是第一个采用1000个元素,后面的每个用990。初始时,对前1000个进行堆排序,取出最大的10个放入第二个集合中990个元素混合,再进行堆排序,取出放入第三个集合中990个元素混合,重复此操作到最后990个元素放入一个集合得到问题的解。

优先队列实现原理分析

推荐阅读更多精彩内容