JAVA数据结构 DAY8-堆

本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!

系列文章目录

JAVA初阶---------已更完

JAVA数据结构 DAY1-集合和时空复杂度

JAVA数据结构 DAY2-包装类和泛型

JAVA数据结构 DAY3-List接口

JAVA数据结构 DAY4-ArrayList

JAVA数据结构 DAY5-LinkedList

JAVA数据结构 DAY6-栈和队列

JAVA数据结构 DAY7-二叉树

JAVA数据结构 DAY8-堆


拓展目录

手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解

链表高频 6 题精讲 | 从入门到熟练掌握链表操作

二叉树高频题精讲 | 从入门到熟练掌握二叉树操作

二叉树高频题精讲 | 从入门到熟练掌握二叉树操作2


目录

目录

系列文章目录

拓展目录

目录

前言

[一、优先级队列:打破普通队列 FIFO 规则的高级队列](#一、优先级队列:打破普通队列 FIFO 规则的高级队列)

[1.1 普通队列的局限](#1.1 普通队列的局限)

[1.2 优先级队列的定义](#1.2 优先级队列的定义)

二、堆(Heap):完全二叉树的特殊顺序存储结构

[2.1 堆的官方定义](#2.1 堆的官方定义)

[2.2 堆的两种形态](#2.2 堆的两种形态)

[2.3 堆的两大核心性质](#2.3 堆的两大核心性质)

[2.4 堆的顺序存储规则(下标公式)](#2.4 堆的顺序存储规则(下标公式))

三、堆的核心操作:向下调整、向上调整、建堆、插入、删除

[3.1 堆的向下调整(以小堆为例)](#3.1 堆的向下调整(以小堆为例))

适用前提

调整步骤(小堆)

完整代码实现

时间复杂度

[3.2 堆的创建(任意数组转堆)](#3.2 堆的创建(任意数组转堆))

建堆步骤

建堆代码

[3.3 建堆时间复杂度详细推导(面试常问)](#3.3 建堆时间复杂度详细推导(面试常问))

[3.4 堆的插入操作](#3.4 堆的插入操作)

插入步骤

向上调整代码(小堆)

[3.5 堆的删除操作](#3.5 堆的删除操作)

删除步骤

四、手动模拟实现优先级队列(MyPriorityQueue)

[五、Java 官方 PriorityQueue 完整使用指南](#五、Java 官方 PriorityQueue 完整使用指南)

[5.1 PriorityQueue 核心特性(必背)](#5.1 PriorityQueue 核心特性(必背))

[5.2 常用构造方法](#5.2 常用构造方法)

[5.3 常用核心 API](#5.3 常用核心 API)

[5.4 如何创建大根堆(面试高频)](#5.4 如何创建大根堆(面试高频))

[5.5 JDK 1.8 扩容机制(源码级)](#5.5 JDK 1.8 扩容机制(源码级))

六、堆的三大经典应用

[6.1 堆排序](#6.1 堆排序)

[6.2 Top-K 问题(面试 / 笔试最高频)](#6.2 Top-K 问题(面试 / 笔试最高频))

[七、配套习题 + 详细答案](#七、配套习题 + 详细答案)

[习题 1](#习题 1)

[习题 2](#习题 2)

[习题 3](#习题 3)

[习题 4](#习题 4)

八、全文总结(最强思维导图版)

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

在数据结构与算法体系中,优先级队列 是解决 "按优先级而非先后顺序出队" 场景的核心结构,而它的底层基石就是。无论是面试高频考点、算法题(Top-K、堆排序),还是工程中的任务调度、事件优先级处理,堆与优先级队列都是必须吃透的核心知识点。

本文将完整覆盖文档全部内容,从概念到原理、从手动实现到 Java API、从习题到应用,用最细致的讲解帮你彻底掌握。


一、优先级队列:打破普通队列 FIFO 规则的高级队列

1.1 普通队列的局限

我们之前学过的普通队列(Queue) 遵循严格的先进先出(FIFO) 规则:先入队的元素,一定先出队。

但现实中大量场景不适合 FIFO

  • 手机正在玩游戏,突然来电 → 来电必须优先处理
  • 班级排座位 → 成绩更优的学生优先选座
  • 操作系统任务调度 → 高优先级任务先执行
  • 医院急诊 → 危重病人优先就诊

这些场景的核心需求:出队时,优先级最高的元素先出,普通队列无法满足。

1.2 优先级队列的定义

优先级队列(Priority Queue) 是一种特殊的队列,它不遵守先进先出,而是:

  • 每次出队,都取出当前优先级最高的元素
  • 每次入队,都能维持内部的优先级规则

它只需要提供两个最核心操作:

  1. 添加新元素
  2. 获取 / 删除最高优先级元素

JDK 1.8 中,Java 集合框架的 PriorityQueue 底层完全基于堆(Heap)实现,堆是优先级队列的底层数据结构。


二、堆(Heap):完全二叉树的特殊顺序存储结构

2.1 堆的官方定义

假设有一个关键码集合:K = {k0, k1, k2, ..., kn-1}

把它按完全二叉树的层序规则存储在一维数组中,并且满足:

  • 小堆:Ki ≤ K[2i+1] 且 Ki ≤ K[2i+2]
  • 大堆:Ki ≥ K[2i+1] 且 Ki ≥ K[2i+2]

满足以上条件的结构,就称为

2.2 堆的两种形态

  • 大根堆(最大堆) :堆顶元素是整个堆的最大值,任意父节点 ≥ 子节点
  • 小根堆(最小堆) :堆顶元素是整个堆的最小值,任意父节点 ≤ 子节点

2.3 堆的两大核心性质

  1. 堆一定是一棵完全二叉树只有完全二叉树,才能用数组高效存储,不会浪费大量空间。
  2. 堆中任意节点的值,永远不大于 / 不小于它的孩子节点这是堆的 "有序性",也是优先级队列能快速取最值的原因。

2.4 堆的顺序存储规则(下标公式)

堆用数组存储,通过下标可以O (1) 找到父节点、左孩子、右孩子 :设当前节点下标为 i

  • 父节点下标:(i - 1) / 2
  • 左孩子下标:2 * i + 1
  • 右孩子下标:2 * i + 2

注意:非完全二叉树不适合顺序存储,因为要存大量空节点,空间利用率极低。


三、堆的核心操作:向下调整、向上调整、建堆、插入、删除

堆的所有功能,都基于两个最基础的算法:向下调整向上调整

3.1 堆的向下调整(以小堆为例)

适用前提

以某节点为根的左子树、右子树已经是堆,只有根节点不满足堆性质,需要向下调整。

调整步骤(小堆)
  1. parent 标记需要调整的节点,child 先标记左孩子(完全二叉树一定先有左孩子)
  2. 如果右孩子存在,找出左右孩子中更小的那个 ,用 child 标记它
  3. 比较 parentchild
    • 如果 parent ≤ child:已经满足堆性质,结束调整
    • 如果 parent > child:交换两者,继续向下调整
  4. 循环直到 child 超出数组范围(到叶子节点)
完整代码实现
java 复制代码
/**
 * 小堆的向下调整
 * @param array 存储堆的数组
 * @param parent 要调整的父节点下标
 */
public void shiftDown(int[] array, int parent) {
    // 先指向左孩子
    int child = 2 * parent + 1;
    int size = array.length;

    // 孩子存在才循环
    while (child < size) {
        // 右孩子存在,且更小,child 切换到右孩子
        if (child + 1 < size && array[child + 1] < array[child]) {
            child = child + 1;
        }

        // 父节点更小,满足堆,直接退出
        if (array[parent] <= array[child]) {
            break;
        }

        // 不满足,交换父节点与较小孩子
        int temp = array[parent];
        array[parent] = array[child];
        array[child] = temp;

        // 继续向下调整
        parent = child;
        child = 2 * parent + 1;
    }
}
时间复杂度

最坏情况从根走到叶子,次数 = 完全二叉树高度时间复杂度:O (log₂n)


3.2 堆的创建(任意数组转堆)

如果数组是完全无序 的(左右子树都不是堆),不能直接调整根节点,必须从底部往上批量调整。

建堆步骤
  1. 找到倒数第一个非叶子节点 下标公式:(array.length - 2) / 2(array.length - 2) >> 1
  2. 从这个节点开始,向前遍历到根节点(下标 0)
  3. 每个节点都执行一次向下调整
建堆代码
java 复制代码
/**
 * 将普通数组调整为堆
 */
public static void createHeap(int[] array) {
    // 找到倒数第一个非叶子节点
    int lastParent = (array.length - 2) >> 1;

    // 从后往前,逐个向下调整
    for (int root = lastParent; root >= 0; root--) {
        shiftDown(array, root);
    }
}

3.3 建堆时间复杂度详细推导(面试常问)

我们用满二叉树近似计算(满二叉树是特殊的完全二叉树):设树高度为 h,总结点数 n ≈ 2ʰ - 1

总调整步数:T(n) = 2⁰·(h-1) + 2¹·(h-2) + 2²·(h-3) + ... + 2ʰ⁻²·1

使用错位相减法2·T(n) = 2¹·(h-1) + 2²·(h-2) + ... + 2ʰ⁻¹·1

两式相减后化简:T(n) = 2ʰ - 1 - h ≈ n

最终结论:建堆的时间复杂度:O (N)


3.4 堆的插入操作

堆的插入必须保证插入后依然是堆。

插入步骤
  1. 把新元素放到数组末尾(完全二叉树最后一个位置)
  2. 对这个新元素执行向上调整,直到满足堆性质
向上调整代码(小堆)
java 复制代码
/**
 * 小堆向上调整
 * @param child 新插入节点的下标
 */
public void shiftUp(int child) {
    // 找到父节点
    int parent = (child - 1) / 2;

    while (child > 0) {
        // 父节点更小,满足堆,结束
        if (array[parent] <= array[child]) {
            break;
        }

        // 交换
        int temp = array[parent];
        array[parent] = array[child];
        array[child] = temp;

        // 继续向上
        child = parent;
        parent = (child - 1) / 2;
    }
}

3.5 堆的删除操作

堆的删除规则:只能删除堆顶元素(优先级最高 / 最低元素)

删除步骤
  1. 堆顶元素堆最后一个元素交换
  2. 堆的有效元素个数 减 1(逻辑删除)
  3. 对新的堆顶执行向下调整,恢复堆结构

四、手动模拟实现优先级队列(MyPriorityQueue)

结合上面的插入、删除、获取堆顶,我们可以写出一个最简可用的优先级队列(不考虑复杂扩容):

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 oldTop = array[0];
        // 最后一个元素放到堆顶
        array[0] = array[--size];
        // 向下调整恢复堆
        shiftDown(0);
        return oldTop;
    }

    // 查看堆顶元素(不删除)
    public int peek() {
        return array[0];
    }

    // 向上调整
    private void shiftUp(int child) {
        // 上文代码
    }

    // 向下调整
    private void shiftDown(int parent) {
        // 上文代码
    }
}

五、Java 官方 PriorityQueue 完整使用指南

Java 内置了开箱即用的优先级队列:java.util.PriorityQueue

5.1 PriorityQueue 核心特性(必背)

  1. 必须导包:import java.util.PriorityQueue;
  2. 元素必须可比较 ,否则抛出 ClassCastException
  3. 不能插入 null ,否则抛出 NullPointerException
  4. 无容量上限,会自动扩容
  5. 插入 / 删除时间复杂度:O(logn)
  6. 底层是默认是小根堆
  7. PriorityQueue 线程不安全;线程安全用 PriorityBlockingQueue

5.2 常用构造方法

构造方法 说明
PriorityQueue() 创建空优先级队列,默认容量 11
PriorityQueue(int initialCapacity) 指定初始容量(不能小于 1)
PriorityQueue(Collection<? extends E> c) 使用集合直接创建堆

示例:

java 复制代码
public static void testConstruct() {
    // 默认容量11
    PriorityQueue<Integer> q1 = new PriorityQueue<>();

    // 指定容量100
    PriorityQueue<Integer> q2 = new PriorityQueue<>(100);

    // 用集合创建
    List<Integer> list = Arrays.asList(4,3,2,1);
    PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
}

5.3 常用核心 API

方法 功能
boolean offer(E e) 插入元素,失败抛异常
E peek() 获取堆顶,空返回 null
E poll() 删除堆顶并返回,空返回 null
int size() 返回有效元素个数
void clear() 清空队列
boolean isEmpty() 判断是否为空

示例:

java 复制代码
public static void testAPI() {
    int[] arr = {4,1,9,2,8,0,7,3,6,5};
    PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);

    // 入队
    for (int num : arr) {
        q.offer(num);
    }

    System.out.println(q.size());   // 10
    System.out.println(q.peek());   // 0(小堆堆顶)

    // 出队两次
    q.poll();
    q.poll();
    System.out.println(q.peek());   // 2

    q.offer(0);
    System.out.println(q.peek());   // 0

    q.clear();
    System.out.println(q.isEmpty());// true
}

5.4 如何创建大根堆(面试高频)

默认是小根堆,想变成大根堆 ,必须传入自定义比较器(Comparator)

java 复制代码
// 自定义比较器:实现大根堆
class IntDescComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        // 降序:o2 - o1
        return o2 - o1;
    }
}

public class TestBigHeap {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>(new IntDescComparator());
        pq.offer(4);
        pq.offer(3);
        pq.offer(1);
        pq.offer(5);

        // 输出 5(大堆堆顶)
        System.out.println(pq.peek());
    }
}

5.5 JDK 1.8 扩容机制(源码级)

PriorityQueue 自动扩容规则:

java 复制代码
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // 容量 <64:2倍扩容;≥64:1.5倍扩容
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // 超过最大限制时处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

简化总结:

  • 容量 < 642 倍扩容
  • 容量 ≥ 641.5 倍扩容
  • 超过 Integer.MAX_VALUE - 8 → 按最大容量处理

六、堆的三大经典应用

6.1 堆排序

利用堆实现高效排序,时间复杂度 O (nlogn),空间复杂度 O (1)。

规则:

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

步骤:

  1. 把数组建成堆
  2. 循环:交换堆顶与最后一个元素 → 堆大小 - 1 → 向下调整堆顶

6.2 Top-K 问题(面试 / 笔试最高频)

问题描述 :在海量数据中,找出前 K 个最大 / 最小的数。比如:世界 500 强、成绩前 10 名、游戏战力前 100。

最优解法:堆(数据太大无法全部加载到内存时,只有堆能高效解决)

核心思路

  • 前 K 个最大元素 → 建小根堆
  • 前 K 个最小元素 → 建大根堆

简单版代码(最小 K 个数):

java 复制代码
class Solution {
    public int[] smallestK(int[] arr, int k) {
        if (arr == null || k <= 0) {
            return new int[0];
        }

        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int num : arr) {
            pq.offer(num);
        }

        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = pq.poll();
        }
        return res;
    }
}

七、配套习题 + 详细答案

习题 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

答案:A解析:A 满足大根堆规则:父节点均大于等于子节点。


习题 2

已知小根堆为 8,15,10,21,34,16,12,删除关键字 8 之后重建堆,关键字之间的比较次数是 ( )

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

答案:C

  • 删除堆顶 → 交换首尾
  • 从根向下调整
  • 选孩子:1 次
  • 父子比较:1 次
  • 下一层父子比较:1 次 总共 3 次!

15 10

12 10

12 16


习题 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]

答案:C


习题 4

一组记录排序码为 (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


八、全文总结(最强思维导图版)

  1. 优先级队列 = 按优先级出队,底层是
  2. = 完全二叉树 + 顺序存储 + 父节点与子节点有序
  3. 堆分两种:大根堆、小根堆
  4. 堆核心操作:
    • 向下调整:O (logn)
    • 向上调整:O (logn)
    • 建堆:O (N)
  5. Java PriorityQueue
    • 默认小堆
    • 不能存 null、元素必须可比较
    • 扩容:<64→2 倍,≥64→1.5 倍
  6. 堆最经典应用:堆排序、Top-K 问题

总结

以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

相关推荐
dovens2 小时前
Spring Boot(快速上手)
java·spring boot·后端
带娃的IT创业者2 小时前
WeClaw 心跳与重连实战:指数退避算法如何让 WebSocket 在弱网环境下的连接成功率提升 67%?
python·websocket·网络协议·算法·fastapi·实时通信
唠玖馆2 小时前
c++ 类和对象(全)
java·开发语言·c++
echome8882 小时前
Python 异步编程实战:async/await 从入门到精通
开发语言·python·php
2401_891482172 小时前
将Python Web应用部署到服务器(Docker + Nginx)
jvm·数据库·python
liuccn2 小时前
GeoTools跟GDAL 库的关系与区别以及应用场景
java·arcgis
为美好的生活献上中指2 小时前
*Java 沉淀重走长征路*之——《MyBatis与MyBatis-Plus一文打尽!》
java·jvm·maven·mybatis·mybatis-plus
brave_zhao2 小时前
javafx中能有异步调用业务方法吗
java
王夏奇2 小时前
python中的深浅拷贝和上下文管理器
java·服务器·前端