前言

本blog将给大家优先级队列的实现及应用,我们知道数据结构中的队列遵循先进先出(FIFO)的原则,但是现实情况中任务通常都有优先级的概念,就得应用优先级队列的方式来解决,优先级队列的底层就是一个堆。

基本概念

1)存储方式

堆通常的表示方式是,将完全二叉树用层序遍历的方式存储在数组中。

java 优先级队列最小堆构建_sed

2)下标关系
在已知双亲(parent)的下标,则

left(左孩子下标) = 2 * parent + 1;
right(右孩子下标) = 2 * parent + 2;

在已知孩子下标(child),则

parent(父亲节点的下标) = (child - 1)/ 2;

1)堆逻辑上是一颗完全二叉树 2)堆在物理上是保存在数组中 3)大根堆:父亲节点的值大于子树中节点的值 4) 小根堆:父亲节点的值小于子树中节点的值 5)堆的基本作用是,快速找到集合的最值小根堆

java 优先级队列最小堆构建_数据结构_02


大根堆

java 优先级队列最小堆构建_sed_03

向下调整算法

进行向下调整的前提是左右子树已经是一个大堆或则小堆。
1)向下调整的思想(以建大堆为例):

1.从根节点开始,选出左右孩子中的最大值;
2.将孩子的最大值与父亲节点的值进行比较;
3a.若大于父亲节点的值,则交换该子节点与父亲节点;
3b.若小于父亲节点的值,则这个堆就是大堆,不在需要进行调整了。
4.然后把交换后的子节点当成父亲节点,重复步骤2.3;

2)图示:

java 优先级队列最小堆构建_sed_04


java 优先级队列最小堆构建_开发语言_05


java 优先级队列最小堆构建_java 优先级队列最小堆构建_06


3)代码实现

public void ShiftDown2(int parent){
        int child = 2 * parent + 1;
        //当孩子节点的下标超过堆的长度调整结束
        while (child < this.usedSize){
            //得到孩子节点中的最大值
            if (child + 1 < this.usedSize && this.elem[child] < this.elem[child + 1]){
                child++;
            }
            //与父亲节点进行比较,若大于就进行交换
            if (this.elem[child] > this.elem[parent]){
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }

时间复杂度:最坏的情况是一直比较到叶子节点,比较的次数是完全二叉树的高度即为O(logn)。

建堆

把数组逻辑上可以看成一个完全二叉树,但是该数组还不是一个堆,现在我们通过向下调整算法把它构建成一个堆。我们从最后一个节点的父亲节点开始进行调整,一直调整到根节点,这个数组在逻辑上就变成了堆。

具体步骤:

1)通过最后一个节点的下标通过公式 parent = (child - 1) / 2得到该父亲节点的下标。

2) 调整完成之后父亲节下标加一,一直调整到根节点,就构建了一个大堆。

java 优先级队列最小堆构建_java 优先级队列最小堆构建_07


代码实现:

public void createBigHeap1(int[] array){
        int parent = (array.length - 1 - 1) / 2;
        for (int i = parent; i >= 0 ; i--) {
            shiftDown2(i);
        }
    }
    public void shiftDown2(int parent){
        int child = 2 * parent + 1;
        //当孩子节点的下标超过堆的长度调整结束
        while (child < this.usedSize){
            //得到孩子节点中的最大值
            if (child + 1 < this.usedSize && this.elem[child] < this.elem[child + 1]){
                child++;
            }
            //与父亲节点进行比较,若大于就进行交换
            if (this.elem[child] > this.elem[parent]){
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }

时间复杂度分析:

java 优先级队列最小堆构建_java_08

建堆最坏的情况是,每一次都需要进行交换,所以总的时间复杂度为:总的时间复杂度为:
每一个需调整的节点的个数*每个节点需调整的次数

T(N) = 1*(h - 1) + 2 * (h - 2) + 2^2 * (h - 3) + … + 2^(h - 2) * 1;
两边乘以二
2 T(N) = 2*(h - 1) + 2^ 2* (h - 2) + 2^3 * (h - 3) + … + 2^(h - 1) * 1;
两式相减的
T(N) = 1 + 2 + 2^2 + … + 2 ^ (h - 2) + 2 ^(h - 1) - h;
T(N) = 2^h - h -1;
由二叉树的性质 有 N = 2^h -1 、 h= log(N + 1)
T(N) = N - log(N + 1)
当N 趋于无穷时
T(N) = O(N);

所以建堆的时间复杂度为:O(N)。

堆的应用 – 优先级队列

在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象吗,然后处理次高的对象。最简单的一个例子是,在手机玩游戏时,如果有来电,那么系统应该有限处理打进来的电话。在这种情况下,我们的数据结构应该提供两个基本的操作,一个是返回最高优先级对象,一个是添加新的对象,这种数据结构就是优先级队列(PriorityQueue).

入队列

1)(以大堆为例)入队列步骤(此过程也被称为向上调整):

1.首先在将新的对象按照尾插的方式放入数组。
2.比较其和双亲的值的大小,如果小于父亲节点的值,就满足堆的性质,插入结束
3.否则,与父亲节点交换值,重新进行2.3步骤
4.知道根节点结束。

2)图示

java 优先级队列最小堆构建_开发语言_09

java 优先级队列最小堆构建_数据结构_10


java 优先级队列最小堆构建_数据结构_11


3)代码实现

public void shitfUp1(int child){
        int parent = (child - 1) / 2;
        while (parent >= 0){
            if (this.elem[parent] < this.elem[child]){
                int tmp = this.elem[parent];
                this.elem[parent] = this.elem[child];
                this.elem[child] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }
public void push1(int val){
        if (isFull()){
            this.elem = Arrays.copyOf(this.elem,2*this.usedSize);
        }
        this.elem[this.usedSize] = val;
        this.usedSize++;
        shitfUp1(this.usedSize-1);
    }

出队列

为了防止破坏左右子树堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶的元素,然后通过向下调整方式重新调整为大堆。
代码实现

public void poll1(){
        if (isEmpty()){
            throw  new RuntimeException("队列为空");
        }
        int tmp = this.elem[this.usedSize - 1];
        this.elem[this.usedSize - 1] = this.elem[0];
        this.elem[0] = tmp;
        
        this.usedSize--;
        shiftDown2(0);
    }

PriorityQueue使用的注意事项

  1. 使用时需导入包;
  2. PriorityQueue中放置的元素必须要能够比较大小 (只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常;
    3.不能插入null对象,否则会空指针异常;
    4.没有容量限制,内部会自动扩容;
    5.插入和删除元素的时间复杂度均为O(logn);
    6.底层是由堆实现。

优先级队列的应用 TOPK问题

解题思路:先用前k个元素生成一个大顶堆,这个大顶堆用于存储,当前最小的k个元素,接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素小于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。

题目:链接: 找出最小K个数.

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if(k == 0){
            return ret;
        }
        
        //构建大堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>(){
            @Override
            public int compare(Integer o1,Integer o2){
                return o2 - o1;
            }
        });
        
        for(int i = 0; i < arr.length; i++){
        //先放入k个元素如大堆中
            if(maxHeap.size() < k){
                maxHeap.offer(arr[i]);
            }else{
            //以后的元素与栈顶元素比较,小的元素与堆顶元素互换
                int top = maxHeap.peek();
                if(top > arr[i]){
                    maxHeap.poll();
                    maxHeap.offer(arr[i]);
                }
            }
        }
           //将堆中元素放入数组中 
        for(int i = 0; i < k; i++){
            ret[i] = maxHeap.poll();
        }
        return ret;
    }
}

堆的应用-- 堆排序

堆排序在这篇文章中有详细的介绍:
链接: 七大经典排序.

创作不易,点个赞吧