优先级队列(堆)-灵析社区

英勇黄铜

我们了解过的队列,是一种先进先出的数据结构。但是呢,在有些情况下,数据的出入是有优先级的,一般出队时,可能需要优先级高的元素先出队列,在这种场景下,使用队列就不合适了。优先级就比如:我们使用手机玩游戏的时候,有电话打过来的时候,手机就要先处理打过来的电话。

在这种情况下,我们就应该提供两种基本的操作,返回最高优先级对象和新增对象。这种数据结构就是优先级队列。

优先级队列的模拟实现

JDK1.8 中的 priorityQueue 底层使用了堆这种数据结构,而这个堆又是在完全二叉树的基础上进行的调整。

什么是堆

如果有一个关键码的集合 K = {k0,k1, k2,…,kn-1} ,把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1  且  Ki<= K2i+2 (Ki >= K2i+1  且 Ki >= K2i+2) i = 0,1,2…,则称为小堆 ( 或大堆 ) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆

堆的性质:

堆中某个节点的值总是不大于 (小根堆)  或者不小于 (大根堆)  其父节点的值

堆一定是一颗完全二叉树

堆的存储方式

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

这里要注意:对于不是完全二叉树的,就不适合使用顺序存储了,因为要还原二叉树,就要将空节点也保存,这样子就会浪费空间,空间利用率低。

我们可以根据以下二叉树的性质来还原二叉树:

如果 i 为 0,表示的节点为根结点,否则 i 节点的双亲节点为(i - 1)/ 2

如果 2*i+1 小于节点个数,则节点i的左孩子下标为 2*i+1 ,否则没有左孩子

如果 2*i+2 小于节点个数, 则节点i的右孩子下标为 2*i+1 ,否则没有右孩子

堆的创建

这里创建堆的方式是使用向下调整的方式来建堆的。想法就是从堆的最后一个节点的父结点开始向下调整,调整完后再向前一个节点向下调整,依次不断到 0 下标的节点调整完后就算建堆成功了。

画图分析:

建堆的时间复杂度分析

这里我们考虑最坏的情况即是一颗满叉树且每一个节点都要调整

画图分析:



通过上图得知向上调整的时间复杂度为 O(n)

向下调整的复杂度会比向上调整的复杂度高上很多,不建议使用它来建堆

原因:

我们知道向上调整是从最后一层的最后一个节点开始,而向下调整只需要从倒数第二层的最后一个节点开始,而往往最后一层的节点就基本等于前面全部节点加起来之和 -1

堆的插入和删除

堆的插入

堆的插入就只有 4 个步骤:

检查空间大小

将需插入节点放入最后一个位置

再从最后一个位置开始向上调整

最后有效位置加一

画图分析:

堆的删除

注意这里的删除是堆顶的元素。具体步骤:

将堆顶元素和堆尾元素交换

有效位置减一

从堆顶开始向下调整

画图分析:

具体代码


import java.util.Arrays;
 
 
public class MyHeap {
    private int[] elem;
    int usedsize;
 
    public MyHeap() {
        this.elem = new int[10];
    }
 
    //初始化elem数组
    public void initElem(int[] array) {
        for(int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedsize++;
        }
 
    }
 
    //创建大根堆
    public void createHeap() {
        for(int parent = (usedsize-1-1)/2; parent>=0; parent--) {
            siftDown(parent, 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 += 1;
             }
 
             if(elem[child] > elem[parent]) {
                 //交换
                 int temp = elem[child];
                 elem[child] = elem[parent];
                 elem[parent] = temp;
                 parent = child;
                 child = 2*parent+1;
             }else {
                 break;
             }
         }
    }
 
    //插入元素
    public void push(int val) {
        //满了
        if(elem.length == usedsize) {
            elem = Arrays.copyOf(elem, elem.length * 2);
        }
 
        elem[usedsize] = val;
 
        siftUp(usedsize);
 
        usedsize++;
    }
    private void siftUp(int child) {
        int parent = (child - 1) / 2;
        while(child > 0 ) {
            if(elem[child] > elem[parent]) {
                int temp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = temp;
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }
 
    /*
    *
    * 删除元素
    *
    */
 
    public void pop() {
        if(isEmpty()) {
            return ;
        }
 
        int temp = elem[0];
        elem[0] = elem[usedsize-1];
        elem[usedsize-1] = temp;
        usedsize--;
 
        int parent = 0;
        int child = 2 * parent + 1 ;
        while(child < usedsize) {
            if(child+1<usedsize && elem[child] < elem[child+1]) {
                child+=1;
            }
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
 
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
 
    }
 
    public boolean isEmpty() {
        return usedsize == 0;
    }
}

priorityQueue

java 集合中提供了 priorityQueue 和 priorityBlockingQueue 两种类型的优先级队列,priorityQueue 是线程不安全的,PriorityQueue 是线程安全的。

使用 priorityQueue 的注意事项:


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

import java.util.PriorityQueue;

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

3  不能插入 null 对象,不然会 1 抛出 nullpointerException

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

4  插入和删除元素的时间复杂度都是 O(logn)

5  priorityQueue 的底层使用堆数据结构

6  priorityQueue 默认情况下是小堆---即每次获取到的元素都是最小的元素,想要建立大堆需要自己传比较器

priorityQueue 的使用

优先级队列的构造方法



static void TestPriorityQueue(){
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}

一般在默认的情况下,PriorityQueue队列是小堆,需要变成大堆需要提供比较器:

class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
插入,删除,获取优先级最高的元素


static void TestPriorityQueue2() {
        int[] arr = {4, 1, 9, 2, 8, 0, 7, 3, 6, 5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        for (int e : arr) {
            q.offer(e);
        }
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
        q.poll();
        q.poll();
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        q.offer(0);
        System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
        q.clear();
        if (q.isEmpty()) {
            System.out.println("优先级队列已经为空!!!");
        }
    }

这里要注意优先级队列扩容说明:

如果容量小于 64 时,就是按 oldCapacity 的两倍大小来扩容

如果容量大于等于 64 时,就是按 oldCapacity 的 1.5 倍大小扩容

如果扩容后的容量大小大于整型的最大值,则按整型的最大值来扩容

堆的应用

用堆为数据结构来封装优先级队列

Java 中的 priorityQueue 底层就是用堆来实现的

堆排序

堆排序就是使用堆的思想来进行排序

1. 建堆

升序:建大堆

降序:建小堆

2. 使用堆元素删除思想来进行排序

交换堆顶和堆尾元素,然后向下调整

阅读量:2030

点赞量:0

收藏量:0