Java数据结构:从入门到精通(九)

优先级队列(堆)

1. 优先级队列

1.1 概念

前面我们了解了队列------一种先进先出(FIFO)的数据结构。但在某些场景中,数据操作需要考虑优先级,要求高优先级的元素先出队列。这时普通队列就无法满足需求了,比如:

  1. 手机游戏运行时接到来电,系统会优先处理电话
  2. 初中班主任安排座位时,成绩优异的学生可以优先选择

针对这类需求,数据结构需要提供两个核心操作:返回最高优先级对象和添加新对象。这种支持优先级处理的数据结构就称为优先级队列(Priority Queue)。

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

JDK1.8的PriorityQueue底层采用堆数据结构实现,该结构本质上是对完全二叉树的一种优化调整。

2.1 堆的概念

给定一个关键码集合 K = {k₀, k₁, k₂, ..., kₙ₋₁},若将其所有元素按照完全二叉树的顺序存储在一维数组中,并满足以下条件之一:

  1. 对于所有 i = 0, 1, 2...,有 kᵢ ≤ k₂ᵢ₊₁ 且 kᵢ ≤ k₂ᵢ₊₂,则称为小堆(或最小堆)
  2. 对于所有 i = 0, 1, 2...,有 kᵢ ≥ k₂ᵢ₊₁ 且 kᵢ ≥ k₂ᵢ₊₂,则称为大堆(或最大堆)

其中,根节点值最大的堆称为最大堆(大根堆),根节点值最小的堆称为最小堆(小根堆)。

堆的性质:

  • 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;
  • 堆总是⼀棵完全⼆叉树。

2.2 堆的存储⽅式

从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储

注意:对于非完全二叉树,顺序存储方式并不适用。因为需要额外存储空节点以确保树结构的正确还原,这将显著降低空间利用率。

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

  • 当i为0时,该节点为根节点;否则,其父节点下标为⌊(i-1)/2⌋
  • 若2i+1小于节点总数,则节点i的左子节点下标为2i+1,否则无左子节点
  • 若2i+2小于节点总数,则节点i的右子节点下标为2i+2,否则无右子节点

2.3 堆的创建

2.3.1 堆向下调整

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

仔细观察上图后发现:根节点的左右⼦树已经完全满⾜堆的性质,因此只需将根节点向下调整好即

可。

向下过程(以小顶堆为例):

  1. 初始化parent指向待调整节点,child指向parent的左孩子(注意:parent若有孩子必先有左孩子)
  2. 当child < size时(即左孩子存在),循环执行以下步骤:
    • 若parent存在右孩子,则比较左右孩子,取较小者作为child
    • 比较parent与child:
      • 若parent ≤ child,调整完成
      • 否则交换parent与child,并更新指针: parent = child child = 2*parent +1 然后继续步骤2的判断

参考代码:

java 复制代码
    public void shiftDown(int[] array, int parent){
        //child 先标记 parent的左孩子,因为 parent可能有左没有右
        int child = parent * 2 + 1;
        int size = array.length;
        while(child < size){
            //如果右孩子存在,找到左右孩子中较小的孩子,用child标记
            if(child+1 < size && array[child+1] < array[child]){
                child += 1;
            }
            //如果双亲比最小的孩子还要小,说明已经满足堆的特性
            if(array[parent] < array[child]){
                break;
            }else{
                //将双亲与最小的孩子交换
                int temp = array[parent];
                array[parent] = array[child];
                array[child] = temp;
                
                //parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整.
                parent = child;
                child = parent * 2 + 1;
            }
        }
    }

注意:在以parent节点为根的⼆叉树进行调整时,必须确保其左右⼦树已经是堆结构才能执行向下调整操作。

时间复杂度分析: 在最坏情况下(如图所示),需要从根节点一直比较到叶⼦节点,比较次数等于完全⼆叉树的⾼度

2.3.2 堆的创建

那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右⼦树不满⾜堆的特性,⼜该如何调整呢?

参考代码:

java 复制代码
public static void createHeap(int[] array) {
// 找倒数第⼀个⾮叶⼦节点,从该节点位置开始往前⼀直到根节点,遇到⼀个节点,应⽤向下调整
        int root = ((array.length-2)>>1);
        for (; root >= 0; root--) {
            shiftDown(array, root);
        }
    }
2.3.3 建堆的时间复杂度

堆结构属于完全二叉树的一种特殊情况,而满二叉树作为完全二叉树的特例,在此处被用来简化时间复杂度分析。由于时间复杂度本身考察的是近似量级,增加少量节点不会影响最终结论。

因此:建堆的时间复杂度为O(N)。

2.4 的插⼊与删除

2.4.1 堆的插⼊

堆的插⼊总共需要两个步骤:

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

  2. 将最后新插⼊的节点向上调整,直到满⾜堆的性质

参考代码:

java 复制代码
public void shiftUp(int[] array, int child){
        //找到child的双亲
        int parent = (child - 1) / 2;
        while (child > 0){
            //如果双亲比孩子小,parent满足堆的性质,调整结束
            if(array[parent] <= array[child]){
                break;
            }else{
                //将双亲与孩子节点进行交换
                int t = array[parent];
                array[parent] = array[child];
                array[child] = t;
                //大的元素向下移动,可能导致子树不满足堆的性质,因此要继续向上递增
                child = parent;
                parent = (child - 1) / 2;
            }
        }
    }
2.4.2 堆的删除

注意:堆的删除⼀定删除的是堆顶元素。具体如下:

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

  2. 将堆中有效数据个数减少⼀个

  3. 对堆顶元素进⾏向下调整

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

java 复制代码
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];
        }
    }

常⻅习题:

1.下列关键字序列为堆的是:()

A: 100,60,70,50,32,65 B: 60,70,65,50,32,100 C: 65,100,70,32,50,60

D: 70,65,100,32,50,60 E: 32,50,100,70,65,60 F: 50,100,70,65,60,32

2.已知⼩根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的⽐较

次数是()

A: 1 B: 2 C: 3 D: 4

3.最⼩堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A: [3,2,5,7,4,6,8] B: [2,3,5,7,4,6,8]

C: [2,3,4,5,7,8,6] D: [2,3,4,5,6,7,8]

参考答案

1.A 2.C 4.C

3. 常⽤接⼝介绍

3.1 PriorityQueue特性

Java集合框架提供了两种优先级队列实现:

  • PriorityQueue:线程不安全
  • PriorityBlockingQueue:线程安全

本文将重点介绍PriorityQueue的实现原理和使用方法。

关于PriorityQueue的使⽤要注意:

  1. 使⽤时必须导⼊PriorityQueue所在的包,即:

import java.util.PriorityQueue;

  1. PriorityQueue 中的元素必须实现大小比较功能,插入无法比较的对象会抛出 ClassCastException 异常
  2. 禁止插入 null 对象,否则会触发 NullPointerException
  3. 支持无限容量扩展,内部会自动进行扩容处理
  4. 插入和删除操作的时间复杂度为 O(log n)
  5. 底层采用堆数据结构实现
  6. 默认构建的是小顶堆,每次获取的都是最小元素

3.2 PriorityQueue 常用接口介绍

1. 优先级队列的构造

此处只是列出了PriorityQueue中常⻅的⼏种构造⽅式

构造器 功能介绍
PriorityQueue() 创建一个空的优先级队列,默认容量是 11
PriorityQueue(int initialCapacity) 创建一个初始容量为 initialCapacity 的优先级队列,注意:initialCapacity 不能小于 1,否则会抛 IllegalArgumentException 异常
PriorityQueue(Collection<? extends E> c) 用一个集合来创建优先级队列
java 复制代码
    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队列是⼩堆,如果需要⼤堆需要⽤⼾提供⽐较器

java 复制代码
// ⽤⼾⾃⼰定义的⽐较器:直接实现Comparator接⼝,然后重写该接⼝中的compare⽅法即可
    class IntCmp implements Comparator<Integer>{
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2-o1;
        }
    }
    public class TestPriorityQueue {
        public static void main(String[] args) {
            PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
            p.offer(4);
            p.offer(3);
            p.offer(2);
            p.offer(1);
            p.offer(5);
            System.out.println(p.peek());
        }
    }

此时创建出来的就是⼀个⼤堆。

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
java 复制代码
    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("优先级队列已经为空!!!");
        }
        else{
            System.out.println("优先级队列不为空");
        }
    }

注意:以下是JDK 1.8中,PriorityQueue的扩容⽅式:JDK17也是类似的,只不过部分进⾏了封装

java 复制代码
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
// Double size if small; else grow by 50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                (oldCapacity + 2) :
                (oldCapacity >> 1));
// overflow-conscious code
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

优先级队列的扩容说明:

• 当容量小于64时,扩容方式是将原容量(oldCapacity)扩大2倍 • 当容量达到或超过64时,扩容方式是将原容量(oldCapacity)扩大1.5倍 • 当容量超过MAX_ARRAY_SIZE时,直接扩容至MAX_ARRAY_SIZE

3.3 编程练习

top-k问题:寻找最大或最小的前k个数据。例如:世界500强企业排名

top-k问题:查找最小的k个数

java 复制代码
    class Solution {
        public int[] smallestK(int[] arr, int k) {
// 参数检测
            if(null == arr || k <= 0)
                return new int[0];
            PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
// 将数组中的元素依次放到堆中
            for(int i = 0; i < arr.length; ++i){
                q.offer(arr[i]);
            }
// 将优先级队列的前k个元素放到数组中
            int[] ret = new int[k];
            for(int i = 0; i < k; ++i){
                ret[i] = q.poll();
            }
            return ret;
        }
    }

该解法只是PriorityQueue的简单使⽤,并不是topK最好的做法,那topk该如何实现?

4. 堆的应⽤

4.1 PriorityQueue的实现

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

4.2 堆排序

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

1. 建堆
  • 升序:建⼤堆
  • 降序:建⼩堆
2. 利⽤堆删除思想来进⾏排序

建堆和堆删除中都⽤到了向下调整,因此掌握了向下调整,就可以完成堆排序。

常⻅习题:

1.⼀组记录排序码为(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个最大或最小的元素,这类问题通常涉及海量数据处理。常见的应用场景包括:专业排名前10、世界500强企业榜单、富豪排行榜、游戏活跃玩家前100等。

传统解决方案是通过排序,但当数据量极大时,排序方法存在明显缺陷(如无法一次性将所有数据加载到内存)。此时,采用堆结构是更高效的解决方案,其实现步骤如下:

  1. 构建初始堆:

    • 使用数据集的前K个元素建立堆
    • 若求前K个最大元素,建小顶堆
    • 若求前K个最小元素,建大顶堆
  2. 筛选处理:

    • 将剩余N-K个元素依次与堆顶元素比较
    • 不满足条件则替换堆顶元素
    • 处理完成后,堆中剩余的K个元素即为所求

(具体代码实现详见后续博客)

感谢您的观看!

相关推荐
float_六七2 小时前
设备分配核心数据结构全解析
linux·服务器·数据结构
wifi chicken3 小时前
Linux 内核开发之单链表的增删查改详解
linux·数据结构·链表
im_AMBER4 小时前
数据结构 18 【复习】广义表 | 各种内部排序 | 二叉排序树的平均查找长度 ASL
数据结构·笔记·学习·排序算法
leaves falling7 小时前
冒泡排序(基础版+通用版)
数据结构·算法·排序算法
C雨后彩虹8 小时前
无向图染色
java·数据结构·算法·华为·面试
程序员-King.8 小时前
二分查找——算法总结与教学指南
数据结构·算法
Xの哲學8 小时前
Linux自旋锁深度解析: 从设计思想到实战应用
linux·服务器·网络·数据结构·算法
好奇龙猫9 小时前
【大学院-筆記試験練習:线性代数和数据结构(9)】
数据结构·线性代数
0和1的舞者9 小时前
力扣hot100-链表专题-刷题笔记(一)
数据结构·链表·面试·刷题·知识