【优先级队列(堆)】

本文详细介绍了Java中的PriorityQueue以及堆数据结构,包括堆的概念、存储方式、创建、插入与删除,重点讲解了堆排序和PriorityQueue在解决TOP-K问题中的实际应用。
摘要由CSDN通过智能技术生成


优先级队列(堆)

队列是一种先进先出(FIFO)的数据结构,优先级队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都有一个与之关联的优先级。在优先级队列中,元素按照优先级的顺序进行排列,具有较高优先级的元素会被先出队。
注意,优先级队列并不保证在所有时间点上都以完全有序的方式维护元素。它只保证在删除最大/最小元素时能够返回具有最高/最低优先级的元素。

一、Java中的PriorityQueue

PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
在这里插入图片描述
为了更好理解优先级队列,下面介绍一下堆这种数据结构

二、堆

1.堆的概念及存储方式

堆(Heap)是一种特殊的树状数据结构,它满足以下两个性质:

1、堆是一个完全二叉树(Complete Binary Tree):除了最后一层外,其它层都是满的,并且最后一层的节点都靠左对齐。
2、堆中每个节点的值都大于等于(或小于等于)其子节点的值,这被称为堆的堆序性(Heap
Property)。如果每个节点的值都大于等于其子节点的值,则称为最大堆(Max Heap),反之则称为最小堆(Min Heap)。
在堆中,根节点的值是最大(或最小)的元素。因此,堆常常用于实现优先级队列和排序算法,如堆排序。

堆可以通过数组来实现,其中数组的每个元素表示堆中的一个节点。具体地,假设堆的根节点存储在索引位置 0 处,则对于任意一个索引 i,其父节点的索引为 (i-1)/2,左子节点的索引为 2i+1,右子节点的索引为 2i+2。

2.堆的创建

例如:对于一个数组{27,15,19,18,28,34,65,49,25,37},建大根堆的步骤如下:
1.对于每个根结点向下调整,这里的根节点是从有孩子节点的最后一个根节点开始选取,而每个子节点的根节点索引为 (i - 1) / 2,即最后一个根节点的下标为 (array.length - 1 - 1) / 2
2.向下调整的过程,对于每个父节点,去比较父节点和父节点的最大子节点,如果前者小,就交换;反之,由于每个父节点的子树都是一个最大堆,因此可以直接break
注意:每调整一个父结点,被交换的子节点作为父节点的这个子树还要重新调整
堆的属性:

    public int[] elem;
    public int usedSize;

    public TestHeap(int[] array) {
        this.elem = Arrays.copyOf(array, array.length);
        this.usedSize = array.length;
    }

堆的创建(根据数组创建):

//创建堆
    public void createBigHeap() {
        //处理所有子树的父节点(向下调整)
        for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
            siftDown(parent, usedSize);
        }
    }

    /*  向下调整:
     *      参数一:要调整的父节点
     *      参数二:调整的结束位置
     * */
    private void siftDown(int parent, int end) {
        int child = 2 * parent + 1;
        //每调整一个父结点,被交换的子节点作为父节点的这个子树还要重新调整
        //child < end:表示当前parent还有左孩子
        while (child < end) {
            //有右孩子的前提下,找出最大的孩子
            if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
                child++;
            }
            //此时child指向最大的孩子,如果这个值大于父结点,就交换
            if (elem[parent] < elem[child]) {
                swap(parent, child);
                //此时被交换的孩子成为新的父结点,继续向下调整
                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;
    }

3.堆的插入与删除

堆的插入总共需要两个步骤:
1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
2. 将最后新插入的节点向上调整,直到满足堆的性质

向上调整:

当前插入节点与其父节点比较,
如果小于,则不需要调整;
反之,交换该节点和父节点的位置,并再以该节点为子节点,直到调整到堆顶

    //堆的插入(堆底元素)
    public void offer(int val) {
        if (isFull()) {
            //扩容
            elem = Arrays.copyOf(elem, usedSize * 2);
        }
        //插入
        elem[usedSize] = val;
        usedSize++;
        //向上调整
        siftUp(usedSize - 1);
    }
    
	public boolean isFull() {
        return usedSize == elem.length;
    }
    
    //向上调整
    private void siftUp(int child) {
        int parent = (child - 1) / 2;
        while (child > 0) {
            if (elem[parent] < elem[child]) {
                swap(child, parent);
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;
            }
        }
    }

三、堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

一句话概括:堆顶元素和堆底元素交换,堆顶再向下调整
建大堆时,堆顶元素都是最大值,调整到堆底,就保证堆底是最大值,然后对堆顶向下调整,以此反复直到遍历完数组,完成排序

    //堆排序
    public void heapSort() {
        int end = usedSize - 1;
        //堆顶(最大值)和堆底交换
        while (end > 0) {
            swap(0, end);
            siftDown(0, end - 1);
            end--;
        }
    }

四、PriorityQueue的实际应用-TOP-K问题

  • 讲完了PriorityQueue的底层堆,再使用这个工具类就很清晰了,下面是其实际应用

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

Top-k问题-最小K个数
先给上代码:

class Solution {
    public int[] smallestK(int[] arr, int k) {
        if (k == 0) {
            return new int[0];
        }
        //top-k
        //1.建k个元素大的大顶堆
        PriorityQueue<Integer> pq = new PriorityQueue<>((o1, o2) -> o2 - o1);
        for (int i = 0; i < k; i++) {
            pq.offer(arr[i]);
        }
        //2.遍历数组剩余元素,找更小值更新堆
        for (int i = k; i < arr.length; i++) {
            //如果碰见比堆顶元素小,移除堆顶
            if (arr[i] < pq.peek()) {
                pq.poll();
                pq.offer(arr[i]);
            }
        }
        //3.返回堆里的所有元素
        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = pq.poll();
        }
        return result;
    }
}

这里的注意点是,PriorityQueue这个工具类默认创建的是最小堆,而在这题中我们所需要的是最大堆,而PriorityQueue有一个构造方法是这样的:
在这里插入图片描述
现在我们就能知道,通过传入Comparator这个比较器的实现类对象,就能够指定PriorityQueue的排序规则。

  • 简单介绍一下Comparator:
    Comparator接口是Java中的一个接口,位于java.util包下。它定义了一种用于比较两个对象的规则。
    Comparator接口中只有一个方法compare(),因此它也是一个函数式接口(只有一个方法需要重写),它用于比较两个对象的大小关系。

通常我们在一个类只需要使用一次时,会使用匿名内部类的方式,匿名内部类又能使用lambda表达式简化,这是JavaSE的内容。因此就由定义一个新的实现类简化成了这行代码。
PriorityQueue<Integer> pq = new PriorityQueue<>((o1, o2) -> o2 - o1);

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值