算法之美:二叉堆原理剖析及堆应用案例讲解及实现

什么是堆

        堆(Heap)是计算机科学中一类特殊的数据结构,通常是一个可以被看做一棵完全二叉树的数组对象。

完全二叉树

         只有最下面两层节点的度可以小于2,并且最下层的叶节点集中在靠左连续的边界,只允许最后一层有空缺结点且空缺在右边,完全二叉树需保证最后一个节点之前的节点都齐全;
        对任一结点,如果其右子树的深度为j,则其左子树的深度必为j或j+1

什么是大顶堆(最大堆)

        大顶堆是一种完全二叉树,其每个父节点的值都大于或等于其子节点的值,即根节点的值最大,每个节点的两个子节点顺序没做要求,和之前的二叉查找树不一样。

 什么是小顶堆(最小堆)

        小顶堆是一种完全二叉树,其每个父节点的值都小于或等于其子节点的值,即根节点的值最小。每个节点的两个子节点顺序没做要求,和之前的二叉查找树不一样

 

存储原理剖析

        1)一般升序采用大顶堆,降序采用小顶堆;

        2)堆是一种非线性结构,用数组来存储完全二叉树是非常节省空间,把堆看作一个数组
方便操作,一般数组的下标0不存储,直接从1节点存储;

        3)堆其实就是利用完全二叉树的结构来维护一个数组;

        4)数组中下标为 k 的节点:

                左子节点下标为 2*k 的节点;
                右子节点就是下标 为 2*k+1 的节点;
                父节点就是下标为 k/2 取整的节点;

        5)对节点在树中的上下移动

                arr[k] 向上移动一层,则让k = k/2
                arr[k] 向下移动一层,则让k=2 k 或 k=2 k+1

 堆的定义通过公式描述:

        大顶堆:arr[k] >= arr[2k+1] && arr[k] >= arr[2k] 
        小顶堆:arr[k] <= arr[2k+1] && arr[k] <= arr[2k]

 应用场景简单介绍

        优先级队列、高精度定时器、TopK问题  ...

        下面我们将以高精度定时器案例进行讲解和代码实现。

二叉堆构建流程

        新插入一个元素之后,就是往数组最后面追加数据,堆可能就不满足堆的特性,需要进行调整,重新满足堆的特点,即该公式:大顶堆:arr[k] >= arr[2k+1] && arr[k] >= arr[2k] ,小顶堆:arr[k] <= arr[2k+1] && arr[k] <= arr[2k],顺着节点所在的路径,向上对比,然后交换。

小顶堆演示动画:Heap Visualization (usfca.edu)

         往堆中插入新元素,就是往数组中从索引0或1开始依次存放数据,但是顺序需要满足堆的特性如何让堆满足:
     1)不断比较新节点 arr[k]和对应父节点arr[k/2]的大小,根据情况交互元素位置;
     2)直到找到的父节点比当前新增节点大则结束;
     3)无需关注同级节点;

 

 大顶堆编码实现

        大顶堆是一种完全二叉树,其每个父节点的值都大于或等于其子节点的值,即根节点的值最大。

public class Heap {
    //用数组存储堆中的元素
    private int[] items;

    //堆中元素的个数
    private int num;

    public Heap(int capacity) {
        //数组下标0不存储数据,所以容量+1
        this.items = new int[capacity + 1];
        this.num = 0;
    }

    /**
     * 判断堆中 items[left] 元素是否小于 items[right] 的元素
     */
    private boolean rightBig(int left, int right) {
        return items[left] < items[right];
    }

    /**
     * 交换堆中的两个元素位置
     */
    private void swap(int i, int j) {
        int temp = items[i];
        items[i] = items[j];
        items[j] = temp;
    }

    /**
     * 往堆中插入一个元素,默认是最后面,++num先执行,然后进行上浮判断操作
     */
    public void insert(int value) {
        items[++num] = value;
        up(num);
    }

    /**
     * 使用上浮操作,新增元素后,重新堆化
     * 不断比较新节点 arr[k]和对应父节点arr[k/2]的大小,根据情况交互元素位置
     * 直到找到的父节点比当前新增节点大则结束
     * <p>
     * 数组中下标为 k 的节点
     * 左子节点下标为 2*k 的节点
     * 右子节点就是下标 为 2*k+1 的节点
     * 父节点就是下标为 k/2 取整的节点
     */
    private void up(int k) {
        //父节点 在数组的下标是1,下标大于1都要比较
        while (k > 1) {
            //比较 父结点 和 当前结点 大小
            if (rightBig(k / 2, k)) {
                //当前节点大,则和父节点交互位置
                swap(k / 2, k);
            }
            // 往上一层比较,当前节点变为父节点
            k = k / 2;
        }

    }

    /**
     * 删除堆中最大的元素,返回这个最大元素
     */
    public int delMax() {
        int max = items[1];
        //交换索引 堆顶的元素(数组索引1的)和 最大索引处的元素,放到完全二叉树中最右侧的元素,方便后续变为临时根结点
        // 为啥不能直接删除顶部元素,因为删除后会断裂,成为森林,所以需要先交互,再删除
        swap(1, num);
        //最大索引处的元素删除掉, num--是后执行,元素个数需要减少1
        items[num--] = 0;

        //通过下浮调整堆,重新堆化
        down(1);
        return max;
    }

    /**
     * 使用下沉操作,堆顶和最后一个元素交换后,重新堆化
     * 不断比较 节点 arr[k]和对应 左节点arr[2*k] 和 右节点arr[2*k+1]的大小,如果当前结点小,则需要交换位置
     * 直到找到 最后一个索引节点比较完成  则结束
     * <p>
     * 数组中下标为 k 的节点
     * 左子节点下标为 2*k 的节点
     * 右子节点就是下标 为 2*k+1 的节点
     * 父节点就是下标为 k/2 取整的节点
     */
    private void down(int k) {
        //最后一个节点下标是num
        while (2 * k <= num) {
            //记录当前结点的左右子结点中,较大的结点
            int maxIndex;
            if (2 * k + 1 <= num) { //2 * k + 1 <= num 是判断 确保有右节点
                //比较当前结点下的左右子节点哪个大
                if (rightBig(2 * k, 2 * k + 1)) {
                    maxIndex = 2 * k + 1;
                } else {
                    maxIndex = 2 * k;
                }
            } else {
                maxIndex = 2 * k;
            }

            //比较当前结点 和 较大结点的值, 如果当前节点较大则结束
            if (items[k] > items[maxIndex]) {
                break;
            } else {
                //否则往下一层比较,当前节点k索引 变换为 子节点中较大的值
                swap(k, maxIndex);
                //变换k的值
                k = maxIndex;
            }

        }
    }

    //    测试
    public static void main(String[] args) {

        Heap heap = new Heap(20);
        heap.insert(42);
        heap.insert(48);
        heap.insert(93);
        heap.insert(21);
        heap.insert(90);
        heap.insert(9);
        heap.insert(3);
        heap.insert(40);
        heap.insert(32);

        int top;
        while ((top = heap.delMax()) != 0) {
            System.out.print(top + ",");
        }
    }
}

堆应用案例:高精度定时器

        实现一个定时器的任务存储,支持很多不同时间的定时任务,要求高精度,1秒级别执行

方案一:

        数据库建立一个task表,存储到数据库或Redis里面,每隔1秒扫描一遍定时任务列表,获取要执行的任务;

        缺点:每次都需要遍历整个定时任务列表,有些很久才执行的任务,也需要被IO遍历,浪费CPU,由于列表比较大,每次遍历都耗时多;

方案二:

        使用【小顶堆】数据结构存储,小顶堆的插入操作就是在最小堆最后插入一个节点,然后重新调整小顶堆的结构,每隔一秒扫下堆顶元素,删除堆顶元素进行执行任务即可,然后重新堆化
key规则是【年月日时分秒】比如:2024-03-29-23-41-22 作为key,堆顶的元素就是最小的。

优先级队列: 传统队列特性就是先进先出,支持优先级排序,优先级高的最先出队(大顶堆实现),优先级队列实现方式有多种,堆去实现属于高效的一种方案
Java的PriorityQueue就是通过二叉小顶堆实现,用一棵完全二叉树表示,通过数组来作为PriorityQueue的底层实现,JDK的PriorityQueue默认是最小堆,可以使用比较器来让它变成最大堆。

编码实现

public class MaxHeapPriorityQueue<T extends Comparable<T>> {

    /**
     * 用数组存储堆中的元素
     */
    private T[] items;

    /**
     * 记录堆中的元素个数
     */
    private int num;

    public MaxHeapPriorityQueue(int capacity) {
        //数组下标0,不存储数据,所有容量要+1
        this.items = (T[]) new Comparable[capacity+1];
        this.num = 0;
    }


    /**
     * 判断队列是否为空
     * @return
     */
    public boolean isEmpty() {
        return num==0;
    }

    /**
     * 比较大小,item[left] 元素是否小于 item[right]的元素
     */
    private boolean rightBig(int left, int right) {
        return items[left].compareTo(items[right])<0;
    }

    /**
     * 交互堆中两个元素的位置
     */
    private void swap(int i, int j) {
        T temp = items[i];
        items[i] = items[j];
        items[j] = temp;
    }

    /**
     * 往堆中插入一个元素,默认是数组最后面,然后进行上浮操作,++num会先执行,第一个数组0索引不存储数据
     */
    public void insert(T value) {
        items[++num] = value;
        up(num);

    }

    /**
     * 使用上浮操作,新增元素后,重新堆化
     * 不断比较新节点 arr[k]和对应父节点arr[k/2]的大小,根据情况交互元素位置
     * 直到找到的父节点比当前新增节点大则结束
     * <p>
     * 数组中下标为 k 的节点
     * 左子节点下标为 2*k 的节点
     * 右子节点就是下标 为 2*k+1 的节点
     * 父节点就是下标为 k/2 取整的节点
     */
    private void up(int k) {
        //父节点,在线数组的下标是1,数组索引大于1都要比较
        while (k > 1) {
            //比较 父节点 和 当前节点 的大小
            if (rightBig(k / 2, k)) {
                //如果当前节点比父节点大,则交互位置
                swap(k / 2, k);
            }
            //当前节点往上一层,当前节点变成父节点
            k = k / 2;
        }
    }

    /**
     * 使用下沉操作,堆顶和最后一个元素交换后,重新堆化
     * 不断比较 节点 arr[k]和对应 左节点arr[2*k] 和 右节点arr[2*k+1]的大小,如果当前结点小,则需要交换位置
     * 直到找到 最后一个索引节点比较完成  则结束
     * <p>
     * 数组中下标为 k 的节点
     * 左子节点下标为 2*k 的节点
     * 右子节点就是下标 为 2*k+1 的节点
     * 父节点就是下标为 k/2 取整的节点
     */
    private void down(int k) {
        // 最后一个节点的下标是num
        while (2 * k <= num) {

            //记录当前节点的左右子节点,较大的节点
            int maxIndex;

            if (2 * k + 1 <= num) {
                if (rightBig(2 * k, 2 * k + 1)) {
                    maxIndex = 2 * k + 1;
                } else {
                    maxIndex = 2 * k;
                }
            } else {
                maxIndex = 2 * k;
            }

            //比较当前节点和较大接的值,如果当前节点大则结束
            if (items[k].compareTo(items[maxIndex]) > 0) {
                break;
            } else {
                //否则往下一层比较,当前节点的k变为子节点中较大的值
                swap(k, maxIndex);
                k = maxIndex;
            }

        }

    }


    /**
     * 删除堆中最大的元素,并且返回这个元素
     *
     * @return
     */
    public T delMax() {

        T maxValue = items[1];
        //交换索引 堆顶的元素(数组索引1的)和 最大索引处的元素,放到完全二叉树中最右侧的元素,方便后续变为临时根结点
        // 为啥不能直接删除顶部元素,因为删除后会断裂,成为森林,所以需要先交互,再删除
        swap(1, num);
        //最大索引处的元素删除,num--是后执行,元素个数需要减少1
        items[num--] = null;

        //通过下沉操作,重新堆化
        down(1);

        return maxValue;
    }
}
public class Task implements Comparable<Task> {
    private int weight;
    private String name;

    public Task(String name,int weight){
        this.weight = weight;
        this.name = name;
    }

    public void doTask(){
        System.out.println(name+" task 运行,权重 = "+weight);
    }

    @Override
    public int compareTo(Task task) {
            return this.weight - task.weight;
    }

}
 public static void main(String[] args) {
        MaxHeapPriorityQueue<Task> queue = new MaxHeapPriorityQueue(20);

        queue.insert(new Task("99任务",99));
        queue.insert(new Task("88任务",88));
        queue.insert(new Task("200任务",200));
        queue.insert(new Task("70任务",70));
        queue.insert(new Task("300任务",300));
        queue.insert(new Task("10任务",10));


        //通过循环从队列中获取最大的元素
        while(!queue.isEmpty()){
            Task task = queue.poll();
            task.doTask();
        }

    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/510206.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

工艺品wordpress外贸主题

工艺品wordpress外贸主题 简约大气的wordpress外贸主题&#xff0c;适合做工艺品进出品外贸的公司官网使用。 https://www.jianzhanpress.com/?p5377

记一次 pdfplumber 内存泄漏导致的服务器宕机

有一个项目需求&#xff0c;要在每天凌晨5点的时候执行一个任务&#xff0c;获取一系列的PDF文件并解析。 后端是Django框架&#xff0c;定时任务用Celery来实现的。 本地跑没什么问题&#xff0c;但是一放到服务器上跑就会宕机&#xff0c;而且是毫无征兆的宕机&#xff0c;…

前端学习<二>CSS基础——17-CSS3的常见边框汇总

CSS3 常见边框汇总 <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><title>CSS3 边框</title><style>body, ul, li, dl, dt, dd, h1, h2, h3, h4, h5 {margin: 0;padding: 0;}​body {background-c…

erp系统开发报价:企业如何选择一套合适的智能erp管理系统-亿发

在选择ERP系统时&#xff0c;企业通常希望了解上一套系统到底需要多少资金&#xff0c;但实际上这个问题并没有一个明确的答案。一般的erp系统从几万到几百万不等&#xff0c;一些简单的erp系统甚至只需要几千元。ERP系统的价格取决于多种因素&#xff0c;包括企业的业务规模、…

Linux多进程通信(1)——无名管道及有名管道使用例程

管道是半双工通信&#xff0c;如果需要 双向通信&#xff0c;则需要建立两个管道&#xff0c; 无名管道&#xff1a;只能父子进程间通信&#xff0c;且是非永久性管道通信结构&#xff0c;当它访问的进程全部终止时&#xff0c;管道也随之被撤销 有名管道&#xff1a;进程间不需…

【算法刷题day14】二叉树理论基础、递归遍历、迭代遍历、统一迭代

二叉树理论基础 题目分类 二叉树的种类 无数值两种&#xff1a;满二叉树 和 完全二叉树 有数值&#xff1a;二叉搜索树 1.若它的左子树不空&#xff0c;则左子树上所有结点的值均小于它的根结点的值; 2.若它的右子树不空&#xff0c;则右子树上所有结点的值均大于它的根结点…

Python快速入门系列-6(Python高级特性)

第六章: Python高级特性 6.1 列表推导式与生成器6.1.1 列表推导式6.1.2 生成器6.1.2.1 生成器表达式6.1.2.2 生成器函数6.2 装饰器与迭代器6.2.1 装饰器6.2.2 迭代器6.3 异常处理与错误调试6.3.1 异常处理6.3.1.1 try-except语句6.3.1.2 try-except-else语句6.3.2 错误调试6.3…

恶劣条件下GNSS定位的鲁棒统计

全球导航卫星系统&#xff08;GNSS&#xff09;作为定位信息的主要来源&#xff0c;在智慧工厂、智慧能源、智慧交通的未来应用中发挥着重要作用。此外&#xff0c;GNSS为电网或股市等关键应用提供定时同步功能。然而&#xff0c;GNSS的性能很容易因自然现象和信号反射而降低。…

AI技术创业有哪些机会?

AI技术创业有哪些机会&#xff1f; 人工智能&#xff08;AI&#xff09;技术作为当今科技创新的前沿领域&#xff0c;为创业者提供了广阔的机会和挑战。随着AI技术的快速发展和应用领域的不断拓展&#xff0c;未来AI技术方面会有哪些创业机会呢&#xff1f; 创什么业打工才是…

Fluentd介绍

1.什么是Fluentd Fluentd是一个开源的日志收集和分发系统&#xff0c;它能够从多种数据源采集日志信息&#xff0c;并对日志进行过滤、加工处理后发送到不同的存储和处理系统。 以下是关于Fluentd的一些关键信息&#xff1a; 基本概念&#xff1a;Fluentd被设计为一个高性能…

RPA机器人如何支持滑块验证码?泽众RPA如何轻松解决?

为了提高软件的安全性&#xff0c;很多系统&#xff0c;包括web系统和手机上的应用&#xff0c;越来越多的使用验证码来提升系统的安全性&#xff0c;防止非法访问&#xff0c;特别是防止机器人的访问。 如上图所示&#xff0c;就是最近比较常用的“滑块验证码”。它要求用户“…

广告业务知识-数据

最近做了些广告业务&#xff0c;梳理下&#xff0c;分广告术语、业务架构、数据架构三篇。以效果广告为例&#xff0c;下面是数据篇&#xff08;图片做了脱敏处理哈&#xff09;&#xff1a; 1.效果广告实体关系 2.广告数据大图 2.1数据模块大图 2.2 详细核心数据大图

ollama本地部署大模型(纯CPU推理)实践

文章目录 说明Ollama和Ollama WebUI简介Ollama模型硬件要求内存要求 Ollama容器部署Ollama容器内模型下载和对话Ollama WebUI部署Ollama WebUI下载模型和对话轻量模型推荐机器硬件信息概览qwen:0.5b推理体验gemma:7b推理体验 说明 本文旨在分享在linux(centos8)平台使用docker…

ry - vue项目 docker部署

一、创建网络 1.搭建net-ry局域网 用于部署若依项目 docker network create net-ry --subnet172.68.0.0/16 --gateway172.68.0.1查看一下。 2、关闭防火墙 1&#xff09;、关闭防火墙 systemctl stop firewalld如果不关闭防火墙&#xff0c;容器内部的mysql、redis等服务…

“一起华裔洗钱案震惊全球”,涉案6.1万枚比特币!英国欲将其“充公”?中方:赃款为潜逃资金,有权追回!

最近&#xff0c;英国警方公布了一桩国际洗钱大案&#xff0c;查获超过6.1万枚比特币&#xff0c;这些资金由华裔英国女子Jian Wen&#xff08;温简&#xff09;涉嫌协助被中国通缉的诈骗集团首脑Zhimin Qian&#xff08;钱志敏&#xff09;而获得&#xff0c;据悉她将于5月10日…

正大国际:安全合规的外盘期货途径

“外盘期货”一词是指在中国大陆以外建立的期货交易市场。交易所基于国内期货和外盘期货的全球定价、价格权威、巨大的外部交易量、成熟的交易市场和交易机制、强大的流动性、巨大的市场容量、在中国大陆没有控制和强劲的趋势。然而&#xff0c;许多人被引诱进入非法甚至非法平…

函数调用实现小米汽车智能语音助手

上周小米汽车发布&#xff0c;其中有一个特色功能就是智能语音&#xff0c;小爱同学整合了语音大模型&#xff0c;实现智能座舱体验。 雷老板的PPT也演示了&#xff0c;一些口语化的对话就能触发各种指令&#xff0c;无论是开空调、播放音乐&#xff0c;还是找手机、识别前方汽…

Python学习:面相对象

面向对象 面向对象技术简介 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。方法:类中定义的函数。类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实…

测试打工仔的5年职场感悟:软件测试还有未来吗?

工作过程 目前坐标广州&#xff0c;从毕业至今五年一直在当前的公司工作着&#xff0c;从部门最开始的十几人团队发展到现在的将近两百号人&#xff0c;几年了没换工作不是因为习惯舒适区&#xff0c;相反这一路过来都是不断的突破&#xff0c;因为团队在快速壮大&#xff0c;…

RK3568驱动指南|第十四篇 单总线-第158章DS18B20编写字符设备驱动框架

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…