当前位置: 首页 > news >正文

【UCB CS 61B SP24】Lecture 21: Data Structures 5: Priority Queues and Heaps 学习笔记

本文介绍了优先队列与堆,分析了最小堆的插入与删除过程,并用 Java 实现了一个通用类型的最小堆。

1. 优先队列

1.1 介绍

优先队列是一种抽象数据类型,其元素按照优先级顺序被处理。不同于普通队列的先进先出(FIFO),优先队列每次取出优先级最高(或最低)的元素。

在 Java 中,PriorityQueue 是基于(Heap)实现的,默认使用自然排序(最小堆),也可以通过自定义 Comparator 调整优先级顺序:

package CS61B.Lecture21;

import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;

public class PriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> Q = new PriorityQueue<>();  // 默认为小根堆
        PriorityQueue<Integer> RQ = new PriorityQueue<>(Collections.reverseOrder());  // 大根堆

        for (int i = 5; i > 0; i--)
            Q.add(i);  // 也可以用 offer() 方法
        System.out.println(Q);  // [1, 2, 4, 5, 3],直接输出不一定有序

        while (!Q.isEmpty())
            System.out.print(Q.poll() + " ");  // 也可以用 remove() 方法,输出 1 2 3 4 5
        System.out.println();

        RQ.addAll(List.of(8, 6, 7, 10, 9));

        while (RQ.peek() != null)
            System.out.print(RQ.remove() + " ");  // 10 9 8 7 6
        System.out.println();
    }
}

堆是一种完全二叉树,完全二叉树是指除了最后一层外,其他层的节点都必须是满的(所有可能的节点都存在),且最后一层的节点必须尽可能靠左排列。最后一层可以不满,但所有节点必须集中在左侧,中间不能有空缺。完全二叉树的高度为 ⌊ l o g n ⌋ + 1 \lfloor log n\rfloor + 1 logn+1,在相同节点数的二叉树中高度最小。

堆又分为最小堆和最大堆:

  • 最小堆:父节点的值 ≤ 子节点的值,堆顶元素为最小值;
  • 最大堆:父节点的值 ≥ 子节点的值,堆顶元素为最大值。

堆通常用数组实现,利用完全二叉树的性质能够简化父子节点索引计算,优先级最高的元素一定在堆顶,也就是索引为 0 的节点:

  • 父节点索引:(i - 1) / 2
  • 左子节点索引:2 * i + 1
  • 右子节点索引:2 * i + 2

下图是一个最小堆的示例:

在这里插入图片描述

1.2 插入

我们以最小堆为例,插入步骤如下:

  • 将新元素插入到堆的末尾(数组的最后一个位置)。
  • 上浮(Swin)调整:从新元素的位置开始,与其父节点比较。
    • 如果新元素的值小于父节点,则交换两者的位置。
    • 重复此过程,直到新元素到达根节点,或满足父节点 ≤ 当前节点的条件。

我们用图片来展示一下最小堆的插入与上浮过程,首先在末尾插入节点 3,然后判断与其父节点的大小关系,小于父节点 5,因此与父节点交换位置,然后继续判断还是小于其父节点 5,和父节点交换,最后判断该节点大于等于其父节点 1,完成上浮调整:

在这里插入图片描述

1.3 删除

还是以最小堆为例,删除步骤如下:

  • 移除堆顶元素(最小值),将其返回。
  • 将堆的最后一个元素移动到堆顶(填补空缺,同时保持完全二叉树的状态)。
  • 下沉(Sink)调整:从堆顶开始,与左右子节点中的较小者比较。
    • 如果当前节点的值大于较小子节点,则交换两者的位置。
    • 重复此过程,直到当前节点到达叶子节点,或满足父节点 ≤ 任意子节点的条件。

同样用图片展示一下最小堆的删除与下沉过程,我们在前面的最小堆上删除堆顶元素(最小值),删除后先将最后一个元素 5 移动到堆顶,然后进行下沉调整,判断 5 大于较小子节点 1,因此与 1 进行交换,接着继续判断还是小于较小子节点 3,因此与 3 进行交换,交换后已经为叶子节点,完成下沉调整:

在这里插入图片描述

可以看到堆的上浮与下沉操作最坏情况下就是遍历树的高度,因此添加和删除操作的时间复杂度最坏情况下均为 O ( l o g n ) O(log n) O(logn)。优先队列与其他数据结构的时间复杂度对比如下:

数据结构插入查看最值删除最值空间复杂度适用场景
有序数组 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n) O ( n ) O(n) O(n)静态数据、极少插入
平衡搜索树 O ( l o g n ) O(log n) O(logn) O ( l o g n ) O(log n) O(logn) O ( l o g n ) O(log n) O(logn) O ( n ) O(n) O(n)需范围查询或全局有序
哈希表 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( n ) O(n) O(n)快速查找某个键、无顺序要求
二叉堆 O ( l o g n ) O(log n) O(logn) O ( 1 ) O(1) O(1) O ( l o g n ) O(log n) O(logn) O ( n ) O(n) O(n)动态数据、频繁插入和提取

2. Java 实现最小堆

相信看完上面的讲解也能感觉到堆的实现并不复杂,Java 实现最小堆代码如下:

package CS61B.Lecture21;

public class MinHeap<T extends Comparable<T>> {
    private T[] heap;
    private int size;
    private static final int DEFAULT_CAPACITY = 2;  // 默认容量

    public MinHeap() {
        this(DEFAULT_CAPACITY);
    }

    public MinHeap(int capacity) {
        heap = (T[]) new Comparable[capacity];
        size = 0;
    }

    /** 核心操作:添加 */
    public void add(T x) {
        if (size >= heap.length) resize();
        heap[size] = x;
        swim(size);  // 从末尾开始上浮
        size++;
    }

    /** 核心操作:删除并返回堆顶元素(最小值) */
    public T remove() {
        if (size == 0) return null;
        T root = heap[0];
        heap[0] = heap[--size];
        heap[size] = null;  // 清除引用,防止内存泄漏
        sink(0);  // 从根节点开始下沉
        return root;
    }

    /** 核心操作:返回堆顶元素(最小值) */
    public T peek() {
        return heap[0];
    }

    /** 获取大小 */
    public int size() {
        return size;
    }

    /** 是否为空 */
    public boolean isEmpty() {
        return size == 0;
    }

    /** 核心操作:上浮 */
    private void swim(int idx) {
        int parent = idx - 1 >> 1;
        while (parent >= 0 && heap[idx].compareTo(heap[parent]) < 0) {
            swap(idx, parent);
            idx = parent;
            parent = idx - 1 >> 1;
        }
    }

    /** 核心操作:下沉 */
    private void sink(int idx) {
        int left = (idx << 1) + 1;  // 左子节点的索引
        while (left < size) {  // 当存在子节点即当前节点还不是叶子节点时循环
            int right = left + 1;
            int minChild = left;  // 先假设最小的子节点为左子节点
            if (right < size && heap[right].compareTo(heap[left]) < 0) minChild = right;
            if (heap[idx].compareTo(heap[minChild]) <= 0) break;  // 如果已经小于等于最小子节点则完成下沉
            swap(idx, minChild);
            idx = minChild;
            left = (idx << 1) + 1;
        }
    }

    /** 将容量扩容至原来的两倍 */
    private void resize() {
        int newCapacity = heap.length * 2;
        T[] newHeap = (T[]) new Comparable[newCapacity];
        System.arraycopy(heap, 0, newHeap, 0, size);
        heap = newHeap;
    }

    /** 交换 heap 中两个位置的元素 */
    private void swap(int idx1, int idx2) {
        T temp = heap[idx1];
        heap[idx1] = heap[idx2];
        heap[idx2] = temp;
    }

    /** 测试 */
    public static void main(String[] args) {
        MinHeap<Integer> minHeap = new MinHeap<>();
        for (int i = 5; i > 0; i--) minHeap.add(i);
        while (!minHeap.isEmpty())
            System.out.print(minHeap.remove() + " ");
        System.out.println();
    }
}

相关文章:

  • 如何在无图形化界面的服务器上下载百度网盘的超大文件(10GB以上)?
  • RefuseManualStart/Stop增强Linux系统安全性?详解systemd单元保护机制
  • 【时序预测】时间序列有哪些鲁棒的归一化方法
  • PMP项目管理—资源管理篇—5.管理团队
  • PySide(PyQT)的@Slot()、@pyqtSlot()装饰器的作用
  • 【区块链 + 绿色低碳】东方易电城市微电网智能平台 | FISCO BCOS 应用案例
  • 【Java---数据结构】链表 LinkedList
  • 物联网数据中台 数据采集器 边缘盒子三者之间应用思考点
  • Windows10系统构建本地安全私有化的个人知识库——采用DeepSeek+RAGFlow
  • Android中的Content Provider是什么以及它有哪些用途
  • 责任链模式:让请求在链条中流动
  • 数组扩展【ES6】
  • npm、Yarn 与 pnpm:选择最适合你的包管理工具
  • 汽车材料耐候性测试仪器-太阳光模拟器介绍
  • 9道Dubbo面试题
  • 爬虫Incapsula reese84加密案例:Etihad航空
  • 力扣记录(二)
  • 什么是网络安全?网络安全包括哪几个方面?学完能做一名黑客吗?
  • 安卓android原生数据打包成.aar
  • 语音合成服务中的声码器是什么
  • 搜索引擎不收录网站/360优化大师官网
  • 网站开发 手机 验证码/360推广平台登录入口
  • wordpress 自动升级/seo现在还有前景吗
  • 日本樱花云服务器网站推荐/简述什么是网络营销
  • 烟台百度网站建设推广/营销型企业网站推广的方法有哪些
  • 海外网购网站/seo工具不包括