数据结构:创建堆(或者叫“堆化”,Heapify)

目录

最直观的思路

更优化的思路(自底向上的构建)

第一步:重新审视问题

第二步:找到规律,形成策略

用一个实例来推演

第三步:编写代码

总结与分析


我们来深入探讨“创建堆”(或者叫“堆化”,Heapify)的过程。这是堆结构中一个非常核心且巧妙的操作。

我们的任务:给定一个包含任意数据的普通数组,如何最高效地将它原地转换成一个合法的堆?

例如,给定数组 arr = [3, 5, 80, 100, 70, 60],我们需要将它重新排列,使其满足大顶堆的属性。

同样,我们从第一性原理出发,探索解决之道。


最直观的思路

我们已经掌握了向堆中 insert 一个新元素的方法。一个非常自然的想法是:

  1. 假设我们有一个空的堆。

  2. 然后我们遍历给定的数组,把数组中的每一个元素,依次 insert 到这个新堆中。

  3. 当所有元素都插入完毕后,我们不就得到了一个合法的堆吗?

✅这个思路是完全正确的,它利用了我们已有的“工具”(insert 函数)。

数据结构:在堆中插入元素(Inserting In a Heap)-CSDN博客

我们知道,每次 insert 操作的核心是“上浮”(Sift Up),其时间复杂度是 O(logk),其中 k 是堆中当时的元素数量。

推演过程:

  • 插入第1个元素:代价是 O(log1)

  • 插入第2个元素:代价是 O(log2)

  • ...

  • 插入第 N 个元素:代价是 O(logN)

把所有这些代价加起来,总的时间复杂度近似为 O(NlogN)。

📍小结一下方法一:

  • 优点:思路简单,容易理解,直接复用了 insert 操作。

  • 缺点:效率不是最优的。O(NlogN) 虽然不错,但我们不禁要问:有没有可能更快?

这个问题,促使我们去寻找一个更根本、更高效的方法。


更优化的思路(自底向上的构建)

第一步:重新审视问题

我们手里的数组,本身就可以被看作一棵“完全二叉树”,只不过它的节点值不满足“堆序属性”。 我们的目标就是修复这个属性。

让我们从一个新的角度来观察这棵树,思考一个问题:

在这棵“乱序”的树中,哪些部分已经天然满足堆的属性了?

答案是:所有的叶子节点 (leaf nodes)。

一个叶子节点,因为它没有孩子,所以它自己就构成了一个合法的、大小为1的堆。 “父节点 >= 子节点” 这个条件因为没有子节点而天然成立。

这是一个至关重要的突破口!


第二步:找到规律,形成策略

在一个用数组表示的、大小为 n 的完全二叉树中,哪些是叶子节点呢?

  • 对于下标为 i 的节点,它的左孩子是 2*i + 1。如果 2*i + 1 >= n,那么它就没有左孩子,也就必然没有右孩子,它就是叶子节点。

  • 我们可以推断出,从下标 n/2n-1 的所有节点,都是叶子节点。

既然所有的叶子节点都已经是“合法”的堆了,我们就不需要对它们做任何操作。我们需要修复的,仅仅是那些非叶子节点

最后一个非叶子节点在哪里?它就在第一个叶子节点的前面,也就是下标为 (n/2) - 1 的位置。

这启发了我们一个自底向上的修复策略:

1️⃣我们从最后一个非叶子节点开始。

2️⃣对它调用我们之前学过的 siftDown (下沉) 操作。

siftDown 的作用是,假设一个节点的左右子树都已经是合法的堆,它可以让这个节点“下沉”到正确位置,从而使这个节点和它的子树共同构成一个更大的合法堆。

当我们从最后一个非叶子节点开始时,它的孩子必然是叶子节点(也就是合法的堆),所以 siftDown 的前提条件满足了!

3️⃣然后,我们向前移动到倒数第二个非叶子节点,对它也执行 siftDown

因为我们是自底向上处理的,所以当处理这个节点时,它的子树也必然已经被我们调整成合法的堆了。

4️⃣我们不断地向前重复这个过程,直到处理完根节点(下标为0)。当根节点也 siftDown 完毕后,整个数组就变成了一个合法的堆。


用一个实例来推演

我们用 arr = [3, 5, 80, 100, 70, 60] (n=6) 来走一遍这个流程。

  • 非叶子节点的索引范围是 0(6/2) - 1 = 2。所以我们只需要处理索引 2, 1, 0

  • 初始状态:

        3/ \5   80/ \   /100 70 60

1. 从 i = 2 (值为80) 开始:

  • 对节点 80 执行 siftDown

  • 它的孩子是 6080 >= 60,满足堆序,无需交换。

  • 数组状态: [3, 5, 80, 100, 70, 60]

  • 树状态 (局部):

      80/60  (这个小堆是OK的)

2. 处理 i = 1 (值为5) :

  • 对节点 5 执行 siftDown

  • 它的孩子是 10070

  • 5 小于它的更大的孩子 100

  • 交换 5100

  • 数组状态: [3, 100, 80, 5, 70, 60]

  • 树状态 (局部):

      100/  \5    70 (这个中等大小的堆也OK了)

3. 处理 i = 0 (值为3,根节点) :

  • 对节点 3 执行 siftDown

  • 它的孩子是 10080

  • 3 小于它更大的孩子 100

  • 交换 3100

  • 数组状态: [100, 3, 80, 5, 70, 60]

  • 3 下沉到了原先 100 的位置(索引1)。现在要继续对它进行 siftDown

  • 节点 3 (在索引1) 的新孩子是 5 (索引3) 和 70 (索引4)。

  • 3 小于它更大的孩子 70

  • 交换 370

  • 数组状态: [100, 70, 80, 5, 3, 60]

  • 3 下沉到了索引4,成为了叶子节点,siftDown 结束。

  • 最终树状态:

    100/   \70    80/ \   /
5   3  60

至此,整个数组已经是一个合法的大顶堆了。


第三步:编写代码

这个过程的代码实现非常简洁。它只需要一个 for 循环,从最后一个非叶子节点开始,调用我们已经写好的 siftDown 函数。

// 假设我们有一个数组 arr 和它的长度 n
// 我们可以复用之前的 siftDown, 但需要稍微修改一下,让它接受数组和范围
void siftDownForSort(int arr[], int n, int index) {int currentIndex = index;while (leftChild(currentIndex) < n) { // 注意这里的边界是 nint largerChildIndex = leftChild(currentIndex);int rightChildIndex = rightChild(currentIndex);if (rightChildIndex < n && arr[rightChildIndex] > arr[largerChildIndex]) {largerChildIndex = rightChildIndex;}if (arr[currentIndex] >= arr[largerChildIndex]) {break;}swap(&arr[currentIndex], &arr[largerChildIndex]);currentIndex = largerChildIndex;}
}
// 我们需要 siftDown, swap, leftChild, rightChild 这些我们已经写好的辅助函数
// ... (此处省略这些函数的代码)// buildHeap 函数:将一个任意数组转换成一个堆
// arr: 指向数组的指针
// n: 数组中元素的数量
void buildHeap(int arr[], int n) {// 找到最后一个非叶子节点的索引int lastNonLeafNode = (n / 2) - 1;// 从最后一个非叶子节点开始,自底向上,对每个节点执行 siftDownfor (int i = lastNonLeafNode; i >= 0; i--) {// siftDownForSort 函数就是我们之前为堆排序写的 siftDown// 它接受数组、数组大小和要操作的索引作为参数siftDownForSort(arr, n, i); }
}

总结与分析

我们通过两种不同的思路来解决“建堆”问题:

  1. 方法一(自顶向下):通过 N 次 insert (sift up),时间复杂度为 O(NlogN)。

  2. 方法二(自底向上):通过约 N/2 次 siftDown,时间复杂度为 O(N)。

虽然看起来都是循环N次,每次调用一个看似 O(logN) 的操作,但对于方法二,可以进行更严谨的数学分析:

大部分节点的 siftDown 操作都发生在树的底层,路径很短,只有根节点附近的少数节点才需要较长的 siftDown 路径。所有操作的成本累加起来,总的时间复杂度其实是线性的 O(N)。

因此,自底向上的 siftDown 方法是构建堆的标准、最高效的算法。 它完美地体现了算法设计的智慧:通过改变看问题的角度,找到一个更深刻的结构性规律(叶子节点天然是堆),从而设计出更优的解决方案。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于 GPT-OSS 的成人自考口语评测 API 开发全记录

1️⃣ 需求与指标 在项目启动前&#xff0c;我们设定了核心指标&#xff1a; 字错率&#xff08;WER&#xff09;< 5%响应延迟 < 800 ms高可用、可扩展 这些指标将贯穿整个开发和测试流程。 2️⃣ 数据准备 准备训练数据是关键步骤&#xff0c;我们使用了 1k 条自考口…

Linux初始——基础指令篇

Linux常用指令pwdlscdtouchmkdirrmmancpmvcatmorelesswhichwhereisaliasgrepfilezip/unzip 指令rzsztarpwd 在xshell中输入pwd并回车&#xff0c;将输出当前用户所存在的目录位置 可看到当前用户是在/home/hhw这个目录下 ls 在xshell中输入ls会显示当前目录所包含的文件 其中…

Vue-24-利用Vue3的element-plus库实现树形结构数据展示

文章目录 1 项目启动 1.1 创建和启动项目(vite+vue) 1.2 清理不需要的代码 1.3 下载必备的依赖(element-plus) 1.4 完整引入并注册(main.sj) 1.5 设置@别名(vite.config.js) 2 el-tree树形控件 2.1 TreeComponents.vue 2.1.1 模板部分 2.1.2 类型定义(Tree) 2.1.3 树形数据(dat…

Kubernetes 部署与发布完全指南:从 Pod 到高级发布策略

引言:告别手动,拥抱声明式 在传统的部署流程中,我们常常需要手动执行一系列命令:SSH 到服务器、拉取新代码、编译、重启服务、检查日志、处理错误…这个过程不仅繁琐低效,而且极易出错,难以保证环境的一致性。 Kubernetes 彻底改变了这一切。它通过一种 “声明式” 的模…

支持向量机核心知识总结

一、核心基础概念核心目标&#xff1a;在样本空间中找到划分超平面&#xff0c;将不同类别样本分开&#xff0c;且该超平面对训练样本局部扰动的 “容忍性” 最优&#xff08;即抗干扰能力强&#xff09;。超平面定义超平面是 n 维空间中的 n-1 维子空间&#xff0c;是 SVM 分类…

Spark学习记录

1、Spark基础介绍 1.1、Spark基础概念 Spark是一种基于内存的快速、通用、可扩展的大数据分析计算引擎 1.2、Spark运行架构 运行过程&#xff1a; Driver 执行用户程序&#xff08;Application&#xff09;的main()方法并创建 SparkContext&#xff0c;与 Cluster Manager 建…

二进制方式安装部署 Logstash

背景说明 Logstash 是一个开源的数据收集和处理引擎&#xff0c;是 Elastic Stack 的重要组件之一。在本方案中&#xff0c;我们使用 Logstash 作为 Kubernetes 集群日志收集的关键组件&#xff0c;主要用于&#xff1a; 从 Kafka 消费各服务的日志数据对日志数据进行过滤和转…

如何用 Kotlin 在 Android 手机开发一个计算器

使用 Kotlin 开发 Android 计算器1. 创建新项目 打开 Android Studio&#xff0c;选择新建项目&#xff0c;模板选择 "Empty Activity"&#xff0c;语言选择 Kotlin&#xff0c;确保最低 API 级别为 21 或更高。2. 设计用户界面 在 res/layout/activity_main.xml 中定…

【Hadoop】Zookeeper、HBase、Sqoop

Zookeeper概述Zookeeper可以监视HDFS系统的name node和data node&#xff0c;HBase也极度依赖zookeeper&#xff0c;因为zookeeper维护了HBase的源数据以及监控所有region server的健康状态&#xff0c;如果region server宕机会通知master 。它也可以避免脑裂&#xff08;只有一…

MLIR - Linalg

简介 Linalg是MLIR中的HHO&#xff08;High-level Hierarchical Optimization&#xff09;中的核心方言&#xff0c;设计用于支持如下的核心Transformation&#xff1a; Progressive Buffer Allocation.Parametric Tiling.Promotion to Temporary Buffer in Fast Memory.Tile…

SQL相关知识 CTF SQL注入做题方法总结

SQL MySQL基础 MySQL基本操作 1.查询本地所有数据库&#xff1a; show databases; 2.使用数据库&#xff1a;use 数据库名; 3.查看当前使用的数据库名&#xff1a;select database(); 4.查看当前使用的数据库的所有表&#xff1a;show tables; 5.查看数据库版本&#xff1a;sel…

魔方的使用

三阶魔方入门玩法教程 【简单实用11个公式】三阶魔方分步还原公式图解 【初级篇】三阶魔方入门教程 1、底棱归位&#xff08;底十字对中层&#xff09; 先顶黄白十字&#xff0c;旋转对齐中层后&#xff0c;R’2翻到底层 2、底角归位 上右-前-》右下 &#xff1a;URU’R’…

新手友好!剪映:开启你的视频剪辑之旅!(国际版)

一.软件介绍 剪映&#xff08;CapCut&#xff09;是一款由​​抖音旗下深圳市脸萌科技有限公司​​开发的全功能视频编辑软件&#xff0c;自2019年5月上线以来&#xff0c;因其简单易用且功能强大&#xff0c;受到了大量用户的喜爱。 1.功能和作用&#xff1a; 功能类别主要…

使用AI大模型Seed1.5-VL精准识别开车接打电话等交通违法行为

原文链接 本案例根据用户上传的电子警察或道路卡口抓拍的图片,使用豆包全新视觉深度思考模型Doubao-1.5-thinking-vision-pro,精准识别车牌号码、车牌颜色、车身颜色、车辆品牌等车辆信息,同时通过算法精确识别开车打电话、未系安全带等交通违法行为,具有极强的实用价值。…

骑行商城怎么开发

随着骑行运动普及与数字化消费升级&#xff0c;“骑行中控数据变现积分商城”模式成为新趋势。以下从核心步骤、关键要点、风险规避三方面&#xff0c;详解如何搭建该类型小程序。一、明确核心架构与需求定位在开发前需确定小程序的核心逻辑与目标用户&#xff0c;避免功能冗余…

揭秘表格推理的“思维革命”:RoT模型介绍

–– RoT: Enhancing Table Reasoning with Iterative Row-Wise Traversals今天&#xff0c;我想和大家探讨一个我们每天都会遇到&#xff0c;却可能从未深思过其背后奥秘的事物——表格。从公司的财务报表、医疗数据&#xff0c;到体育赛事统计&#xff0c;表格无处不在&#…

【C++】AVL树(详解)

文章目录 上文链接一、什么是 AVL 树二、AVL 树的实现1. 引入平衡因子2. 整体结构3. AVL 树中的插入操作(1) 插入节点(2) 更新平衡因子更新规则停止更新条件 4. 旋转(1) 旋转的目的(2) 右单旋(3) 左单旋(4) 左右双旋(5) 右左双旋 5. AVL 树的查找与删除6. AVL 树的平衡检测 三、…

shell编程-核心变量知识

文章目录shell简介如何学好shell初识shell什么是shell执行shell脚本常用的三种方式shell变量变量相关的配置文件变量的定义shell核心位置变量shell简介 为什么学习shell&#xff0c;shell的作用 面试题&#xff1a;给你一台主机你的操作流程是什么&#xff1f; 1.自动化安装操…

微电网调度(风、光、储能、电网交互)(MatlabPython代码实现)

赠读者&#xff1a;正在埋头科研的你&#xff0c;或许有时你会困惑于 “投入” 与 “回报” 的时差&#xff0c;会疲惫于 “未知” 与 “确定” 的博弈&#xff0c;但请记得&#xff1a;那些看似 “无用” 的试错&#xff0c;都是在为突破搭建阶梯&#xff1b;那些独自深耕的日…

CentOS 7 环境下安装 JDK 1.8 及解决 wget 命令缺失问题

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务) &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1;个人微信&a…