优先级队列(堆)

1.优先级队列

1.1概念

优先级队列(Priority Queue) 是一种抽象数据类型(ADT),其行为类似于普通的队列或栈,但有一个关键区别:每一个元素都关联有一个“优先级”(Priority)

  • 出队顺序:元素出队(被删除)的顺序不是由它们进入队列的“时间”决定,而是由它们的优先级决定。
  • 最高优先级先出:优先级最高的元素最先被服务(即出队)。
  • 进入顺序无关:优先级低的元素即使比优先级高的元素更早进入队列,也必须等到所有优先级更高的元素都出队后才能被服务。

你可以把它想象成医院急诊室。病人(元素)并不是严格按照先来后到的顺序接受治疗(出队),而是由护士根据病情的紧急程度(优先级)进行分诊。一个突发心脏病的病人(高优先级)即使刚来,也会比一个已经等了两个小时但只是手指划伤的病人(低优先级)优先得到救治。

2.优先级队列的模拟实现

2.1 堆的概念

是一种特殊的、基于树的、满足某些特定性质的完全二叉树。它主要用于实现优先级队列这种抽象数据类型。

堆主要有两种类型,其核心区别在于堆的性质:

  1. 最大堆(Max-Heap)
    • 性质:在任何子树中,根节点的值大于或等于其所有子节点的值。
    • 结果:堆中的最大值始终位于根节点
  2. 最小堆(Min-Heap)
    • 性质:在任何子树中,根节点的值小于或等于其所有子节点的值。
    • 结果:堆中的最小值始终位于根节点
关键特性
  1. 完全二叉树(Complete Binary Tree)
    • 这是堆的结构特性。意味着树的所有层都是完全填充的,除了最后一层,最后一层的节点都尽可能地集中在左边。
    • 这个特性使得堆可以非常高效地使用数组列表来实现,而不需要像普通二叉树那样用节点和指针。对于数组中位置 i 的节点:
      • 父节点的位置:parent(i) = (i - 1) // 2
      • 左子节点的位置:left_child(i) = 2*i + 1
      • 右子节点的位置:right_child(i) = 2*i + 2
  2. 堆序性(Heap Order Property)
    • 这是堆的顺序特性,即上面提到的最大堆或最小堆的性质。
    • 这个特性保证了我们能在常数时间 O(1) 内找到最大或最小元素(直接取根节点)。

2.2 堆的存储方式

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

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

​ 可以根据二叉树知识:假设i为节点在数组中的下标,则有:

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

2.3 堆的创建

2.3.1 堆向下调整
方法一:自顶向下插入法(Heapify-Up)

这种方法最为直观,类似于我们一个一个地向空堆中添加元素。

算法步骤:

  1. 从一个空堆开始。
  2. 遍历输入数组中的每一个元素。
  3. 对于每个元素,执行以下操作:
    • 将新元素追加到堆数组的末尾
    • 对新元素执行 sift_up(上浮) 操作,使其“游”到正确的位置,以恢复堆的性质。
private void siftDown(int parent, int usedSize){
        int child = 2 * parent+1;
        while(child < usedSize){
            //判断左右孩子的最大值
            if (child+1 < usedSize && elem[child] < elem[child+1]){
                child++;
            }
            if (elem[child] > elem[parent]){
                swap(elem,child,parent);
                parent = child;
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }

​ **注意:**在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。

时间复杂度分析: 最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为 O(logN)

2.3.2 堆的创建
public void createHeap(){
    for (int parent = (this.usedSize - 1)/2; parent >= 0 ; parent--) {
       siftDown(parent,this.usedSize);
    }
}

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

2.4 堆的插入与删除

插入操作的目标是向堆中添加一个新元素,同时保持堆的性质不变。

堆的插入(Insertion / Push)
算法步骤:
  1. 添加到末尾:将新元素插入到堆数组的最后一个位置(完全二叉树的最后一个节点)。
  2. 上浮调整(Sift Up):将新元素与其父节点进行比较:
    • 如果它大于父节点(违反最大堆性质),则与父节点交换位置
    • 重复这个过程,直到新元素不再大于其父节点,或者到达根节点为止。
堆的删除(Deletion / Pop)

删除操作通常指的是删除并返回堆中的最大元素(最大堆的根节点)。

算法步骤:
  1. 保存根节点:记录根节点的值(这是要返回的最大值)。
  2. 移动末尾元素:将堆数组的最后一个元素移动到根节点的位置。
  3. 删除末尾:从数组中移除最后一个元素(现在已移动到根节点)。
  4. 下沉调整(Sift Down):从新的根节点开始,将其与两个子节点中较大的那个进行比较:
    • 如果它小于较大的子节点,则与那个子节点交换位置
    • 重复这个过程,直到它大于或等于它的两个子节点,或者到达叶子节点为止。
  5. 返回最大值:返回步骤1中保存的根节点值。

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

public class MyPriorityQueue {
    // 演示作用,不再考虑扩容部分的代码
    private int[] array = new int[100];
    private int size = 0;
    
    public void offer(int e) {
        array[size++] = e;
        shiftUp(size - 1);
   }
    
    public int poll() {
        int oldValue = array[0];
        array[0] = array[--size];
        shiftDown(0);
        return oldValue;
   }
    
    public int peek() {
        return array[0];
   }
}

3.常用接口介绍

3.1 PriorityQueue的特性

​ Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线 程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。

关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包
  2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
  3. 不能插入null对象,否则会抛出NullPointerException
  4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  5. 插入和删除元素的时间复杂度为
  6. PriorityQueue底层使用了堆数据结构
  7. PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素

3.2 PriorityQueue常用接口介绍

1.优先级队列的构造
构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(Collection c)用一个集合来创建优先级队列

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

2.插入/删除/获取优先级最高的元素
函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空返回true

​ 优先级队列的扩容说明:

  • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
  • 如果容量大于等于64 ,是按照oldCapacity的1.5倍方式扩容 的
  • 如果容量超过MAX ARRAY_ SIZ E ,按照MAX ARRAY_ SIZ E来进行扩容

3.3.oj练习

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

public int[] samllestK(int[] arr,int k){
        int[] ret = new int[k];
        if (arr == null || k == 0){
            return ret;
        }
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k,new IntCmp());
        //k*logk
        for (int i = 0; i < k; i++) {
            priorityQueue.offer(arr[i]);
        }
        //(n-k)*logk
        for (int i = k; i < arr.length; i++) {
            int peekVal = priorityQueue.peek();
            if (arr[i] < peekVal){
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }

4.堆的应用

4.1 PriorityQueue的实现

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

4.2 堆排序

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

  • 建堆升序:建大堆降序:建小堆

  • 利用堆删除思想来进行排序 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

4.3 Top-k问题 TOP-K问题:

​ TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

​ 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

​ 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  • 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆 前k个最小的元素,则建大堆
  • 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
public int[] smallestK1(int[] arr,int k){
    PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
    //O(N*logN)
    for (int i = 0; i < arr.length; i++) {
        priorityQueue.offer(arr[i]);
    }
    //O(K*logN)
    int[] ret = new int[k];
    for (int i = 0; i < k; i++) {
        ret[i] = priorityQueue.poll();
    }
    return ret;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值