数据结构-优先级队列(堆)

目录

一、优先级队列

二、优先级队列的模拟实现

        2.1 堆的概念

        2.2 堆的存储方式

        2.3 堆的创建

         2.4 堆的插入和删除

        2.5 用堆模拟实现优先级队列

三、常用接口

        3.1 PriorityQueue的特性

        3.2 PriorityQueue常用接口

        3.3 练习

四、堆应用

        4.1 PriorityQueue的实现

        4.2 堆排序

        4.3 Top-k问题


一、优先级队列

        队列是一种先进先出的数据结构,但有些情况下操作的数据可能带有优先级,一般出队

列时,可能需要优先级高的元素先出队列,该场景下,使用队列显然不合适。在这种情况下,数据

结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数

据结构就是 优先级队列 (Priority Queue)

二、优先级队列的模拟实现

        JDK1.8中的优先级队列底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进

行了一些调整。

                数据结构-优先级队列(堆)_第1张图片

        2.1 堆的概念

        如果有一个关键码的集合K = {k0k1 k2kn-1},把它的所有元素按完全二叉树的顺

序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 Ki<= K2i+2 (Ki >= K2i+1 Ki >=

K2i+2) i = 012…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆根节点最

小的堆叫做最小堆或小根堆

数据结构-优先级队列(堆)_第2张图片

        性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。

        2.2 堆的存储方式

        堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。

        注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,间中必须要存储空节点,就会导致空间利用率比较低

        将元素存储到数组中后,可以根据二叉树的性质5对树进行还原。假设i为节点在数组中的下标,则有

  • 如果i0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
  • 如果 2 * i + 1 小于节点个数,则节点 i 的左孩子下标为 2 * i + 1 ,否则没有左孩子
  • 如果 2 * i + 2 小于节点个数,则节点 i 的右孩子下标为 2 * i + 2 ,否则没有右孩子

        2.3 堆的创建

        对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,将其创建成堆

数据结构-优先级队列(堆)_第3张图片

       堆向下调整过程(以大堆为例):

  1. parent标记需要调整的节点的下标,child标记parent的左孩子下标(parent如果有孩子一定先有左孩子)
  2. 如果parent左孩子存在,即:child < len, 进行以下操作,直到parent左孩子不存在(len为节点个数)
  • 判断parent右孩子是否存在,如果存在找到左右孩子中最大的孩子,child存储最大的孩子下标
  • parent与较大的孩子child比较,如果parent大于较大的孩子child,调整结束;否则:交换parent与较大的孩子child,交换完成后,parent中大的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = childchild = parent*2+1; 然后继续进行2
private void siftDown(int parent,int len){
     //len是堆中有效数据的个数
    //child记录孩子的下标,因为有可能有孩子也可能没有孩子
    int child = 2*parent+1;
    //至少有左孩子
    while(child < len){
        //有右孩子 且 左右孩子比较
        if(child+1 < len && elem[child] < elem[child+1]){
            child = child+1;
        }
        //执行完if语句,child是左右孩子最大的下标
        if(elem[child] > elem[parent]){
            swap(child,parent);
            //判断子树-进行循环
            parent = child;
            child = 2*parent+1;
        }else {
            break;
        }
    }
}
//交换
private void swap(int i, int j){
    int tmp = elem[i];
    elem[i] = elem[j];
    elem[j] = tmp;
}

        创建一个堆,从最后一棵子树开始向下调整,调整完成后调整前一棵子树,直至所有子树全部调整完成。找最后一棵子树根节点下标 i:i = (len - 1)/2,len是二叉树的结点个数。

        创建大根堆:

数据结构-优先级队列(堆)_第4张图片

        最终效果图:

        数据结构-优先级队列(堆)_第5张图片

        时间复杂度堆是完全二叉树,而满二叉树也是完全二叉树,为简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响结果)

数据结构-优先级队列(堆)_第6张图片

        故建堆的时间复杂度为O(N)

         2.4 堆的插入和删除

        1.堆的插入

        堆的插入总共有两个步骤:

                1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)

                2. 将最后新插入的节点向上调整,直到满足堆的性质
数据结构-优先级队列(堆)_第7张图片
public void siftUp(int child){
    //获取child的双亲
    int parent = (child-1)/2;
    while (child > 0){
        //双亲parent比孩子child大,满足堆的性质,调整结束
        if(elem[parent] > elem[child]){
            break;
        }else {
            swap(parent,child);
            //大的元素向上移动,可能子树不满足堆的性质,需要继续向上调整
            child = parent;
            parent = (child-1)/2;
        }
    }
}
//交换
private void swap(int i, int j){
    int tmp = elem[i];
    elem[i] = elem[j];
    elem[j] = tmp;
}

        2.堆的删除

        堆的删除一定删除的是堆顶元素。具体如下:

                1. 将堆顶元素对堆中最后一个元素交换

                2. 将堆中有效数据个数减少一个
                3. 对堆顶元素进行向下调整
数据结构-优先级队列(堆)_第8张图片
public int pop(){
    //堆为空
    if(isEmpty()){
        return -1;
    }
    //获取出堆元素
    int ret = elem[0];
    //出堆的元素和最后一个元素交换
    swap(0,usedSize-1);
    usedSize--;
    //向下调整
    siftDown(0,usedSize);
    return  ret;
}

        2.5 用堆模拟实现优先级队列

//大根堆
public class MyPriorityQueue {
    private int[] elem;
    //堆中有效元素个数
    private int usedSize;
    public MyPriorityQueue(){
        elem = new int[10];
    }
    //判断堆是否满
    private boolean isFull(){
        return usedSize == elem.length;
    }
    //判断堆是否为空
    public boolean isEmpty(){
        return  usedSize == 0;
    }
    /**
     * 初始化elem数组
     */
    public  void  initElem(int[] array){
        for(int i = 0; i < array.length; i++){
            elem[i] = array[i];
            usedSize++;
        }
    }
    /**
     * 使用向下调整创建大根堆
     */
    public void createHeap(){
        for(int i = (usedSize-1-1)/2;i >= 0; i--){
            siftDown(i,usedSize);
        }
    }
    //向下调整
    private void siftDown(int parent,int len){
        int child = 2*parent+1;
        //至少有左孩子
        while(child < len){
            //有右孩子 且 左右孩子比较
            if(child+1 < len && elem[child] < elem[child+1]){
                child = child+1;
            }
            //执行完if语句,child是左右孩子最大值的下标
            if(elem[child] > elem[parent]){
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }
    //交换
    private void swap(int i, int j){
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }
    //入堆
    public void push(int val){
        //堆满
        if(isFull()){
            elem = Arrays.copyOf(elem,elem.length*2);
        }
        elem[usedSize] = val;
        //向上调整
        siftUp(usedSize);
        usedSize++;
    }
    //向上调整
    public void siftUp(int child){
        int parent = (child-1)/2;
        while (child >0){
            if(elem[parent] > elem[child]){
                break;
            }else {
                swap(parent,child);
                child = parent;
                parent = (child-1)/2;
            }
        }
    }
    //出堆
    public int pop(){
        //堆为空
        if(isEmpty()){
            return -1;
        }
        //获取出堆元素
        int ret = elem[0];
        //出堆的元素和最后一个元素交换
        swap(0,usedSize-1);
        usedSize--;
        //向下调整
        siftDown(0,usedSize);
        return  ret;
    }
}

三、常用接口

        3.1 PriorityQueue的特性

        Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。

        数据结构-优先级队列(堆)_第9张图片

        使用PriorityQueue需要注意:

       1. 使用时必须导入PriorityQueue所在的包         

import java.util.PriorityQueue;

       2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常

        3. 不能插入null对象,否则会抛出NullPointerException

        4. 没有容量限制,可以插入任意多个元素,内部可以自动扩容

        5. 插入和删除元素的时间复杂度为O(\log n),底数为2

        6. PriorityQueue底层使用堆数据结构

        7. PriorityQueue默认情况是小堆---即每次获取到的元素都是最小的元素

        3.2 PriorityQueue常用接口

        1. 优先级队列的构造 数据结构-优先级队列(堆)_第10张图片

        以上只是列出了PriorityQueue中常见的几种构造方式

//创建空的优先级队列,底层默认容量是11 
PriorityQueue priorityQueue = new PriorityQueue<>();
//创建空的优先级队列,底层的容量为initialCapacity-》100
PriorityQueue priorityQueue2 = new PriorityQueue<>(100);

ArrayList list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
//用ArrayList对象来构造一个优先级队列的对象
PriorityQueue priorityQueue3 = new PriorityQueue<>(list);
System.out.println(priorityQueue3.size());//4
System.out.println(priorityQueue3.peek());//1

        默认情况下,PriorityQueue队列是小堆,如果需要大堆需要提供比较器

//自定义比较器--->升序比较器:实现Comparator接口,重写接口中的compare方法
class Imp implements Comparator {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
}
public static void main(String[] args) {
    //传一个升序比较器
    PriorityQueue priorityQueue = new PriorityQueue<>(new Imp());
    priorityQueue.offer(4);
    priorityQueue.offer(3);
    priorityQueue.offer(2);
    priorityQueue.offer(1);
    System.out.println(priorityQueue.peek());//4
}

        上述创建了一个大根堆

        2. 插入/删除/获取优先级最高的元素

方法 解释
boolean offer(E e)
插入元素 e ,插入成功返回 true ,如果 e 对象为空,抛出 NullPointerException 异常,时间复杂度O(\log n) ,空间不够时候会进行扩容
E peek()
获取优先级最高的元素,如果优先级队列为空,返回 null
E poll()
移除优先级最高的元素并返回,如果优先级队列为空,返回 null
int size()
获取有效元素的个数
void clear() 清空
boolean isEmpty()
检测优先级队列是否为空,空返回 true
public static void main(String[] args) {
    int[] arr = {8,5,2,10,7};
    /**
     * 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好,这样既不会浪费内存空间,也不会由于需要扩容使效率降低
     * 否则可能需要不断扩容
     * 扩容机制:开辟更大空间,拷贝元素,效率比较低
     *
     * 创建对象时没有传递比较器,默认为null,如果没有传递容量大小,容量为默认大小-->11
     */
    PriorityQueue priorityQueue = new PriorityQueue<>(arr.length,new Imp());
    for (int x:arr) {
        priorityQueue.offer(x);
    }
    // 打印优先级队列中有效元素个数
    System.out.println(priorityQueue.size());//5
    //peek() 获取优先级最高的元素
    System.out.println(priorityQueue.peek());//10

    //poll() 从优先级队列中删除元素
    priorityQueue.poll();
    // 打印优先级队列中有效元素个数
    System.out.println(priorityQueue.size());//4
    // 获取优先级最高的元素
    System.out.println(priorityQueue.peek());//8

    //clear() 将优先级队列中的有效元素删除掉,检测其是否为空
    priorityQueue.clear();
    if(priorityQueue.isEmpty()){
        System.out.println("优先级队列已经为空!!!");
    }
    else{
        System.out.println("优先级队列不为空");
    }
}

       以下是JDK 1.8中,PriorityQueue的扩容方式

private static final int MAX_ARRAY_SIZE = Integer . MAX_VALUE - 8 ;
private void grow ( int minCapacity ) {
        int oldCapacity = queue . length ;
        int newCapacity = oldCapacity + (( oldCapacity < 64 ) ?
                                        ( oldCapacity + 2 ) :
                                        ( oldCapacity >> 1 ));
        if ( newCapacity - MAX_ARRAY_SIZE > 0 )
                newCapacity = hugeCapacity ( minCapacity );
        queue = Arrays . copyOf ( queue , newCapacity );
}
private static int hugeCapacity ( int minCapacity ) {
        if ( minCapacity < 0
                throw new OutOfMemoryError ();
        return ( minCapacity > MAX_ARRAY_SIZE ) ?
                        Integer . MAX_VALUE :
                        MAX_ARRAY_SIZE ;
}

        如果容量小于64时,是按照oldCapacity2倍方式扩容;如果容量大于等于64,是按照oldCapacity1.5倍方式扩容;如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容。

        3.3 练习

        top-k问题:最大或者最小的前k个数据。

        top-k问题:求最小的前k个数

/**
 *  top-k问题
 *  最小的k个数:返回数组array中最小的k个数,任意顺序都可
 *  应用场景:主要是在一组大量数据中找最小的k个数
 *  一般思路:1.将数组存于 默认是最小堆方式的优先级队列中
 *         2.优先级队列出队k次获取的k个数即为数组中最小的k个数
 *
 * 时间复杂度:O(n*logn),n太大时,效率会非常低
 *
 */
public static int[] smallestK(int[] arr, int k) {
    int[] ret =new int[k];
    if(arr == null || k== 0){
       return ret;
    }
    PriorityQueue priorityQueue = new PriorityQueue<>();
    //arr中有n个数,插入的时间复杂度O(n*logn)
    for (int x:arr) {
        priorityQueue.offer(x);
    }
    //删除的时间复杂度:O(k*logn)
    for (int i = 0; i < k; i++) {
        ret[i] = priorityQueue.poll();
    }
    return  ret;
}
        该解法并不是 topK 最好的做法,那 topk 该如何实现呢?在下文介绍

四、堆应用

        4.1 PriorityQueue的实现

        用堆作为底层结构封装优先级队列。

        4.2 堆排序

        堆排序即利用堆的思想来进行排序,总共分为两个步骤:

                1. 建堆 :升序:建大根堆,降序:建小根堆。

                2. 利用堆删除思想来进行排序

一组记录排序码为 (5 11 7 2 3 17), 则利用堆排序方法建立的初始堆为 ()
A: (11 5 7 2 3 17)   B: (11 5 7 2 17 3)   C: (17 11 7 2 3 5)
D: (17 11 7 5 3 2)   E: (17 7 11 3 5 2)   F: (17 7 11 3 2 5)
答案: C

        4.3 Top-k问题

        TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。对于Top-K问题,能想到的最简单直接的方式就是排序,但是如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)最佳的方式就是用堆来解决,基本思路如下:

        1. 用数据集合中前K个元素来建堆:k个最大的元素,则建小堆;前k个最小的元素,则建大堆。

        2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,比较完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

/**
 * Top-k问题
 * 解决思路:
 *     1.用数据集合中前K个元素来建堆(前k个最大的元素,则建小堆;前k个最小的元素,则建大堆。)
 *     2.用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
 * 时间复杂度为 O(k*logk) + O((n-k)*logk)--->nlogk
 */
//自定义比较器--->升序比较器:实现Comparator接口,重写接口中的compare方法
class Imp implements Comparator {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
}
public static int[] smallestK(int[] arr,int k) {
    //建立大根堆
    PriorityQueue priorityQueue = new PriorityQueue<>(new Imp());
    int[] ret = new int[k];
    if(arr == null || k== 0){
        return ret;
    }
    /**
     * 1.用数据集合中前K个元素来建堆
     * 时间复杂度为O(k*logk)
     */
    for (int i = 0;i 

你可能感兴趣的:(数据结构,java,算法)