数据结构堆的深度解析:为什么它是高效处理最值问题的利器

数据结构堆的深度解析:为什么它是高效处理最值问题的利器

 前言

在非线性数据结构的家族中,堆是兼具 “完全二叉树特性” 与 “最值优先级” 的高效工具 —— 它以数组为物理载体,却暗藏树形逻辑,能在 O (1) 时间获取最值,O (logN) 时间完成插入删除,成为解决排序、Top-K 等经典问题的 “一把好手”。

📚 初阶数据结构

【 时间复杂度+空间复杂度 】

【 顺序表 】

【 单链表 】

【 链表OJ题(上篇)】

【 链表OJ题(下篇)】

【 栈和队列 】

【 栈和队列面试题 】

【 二叉树概念解析 】


目录

一、堆的核心概念与结构特性

1. 堆的定义

2. 核心特性

3. 直观示例

二、堆的实现 

1、堆的结构

2、堆的初始化

3、堆的销毁

4、堆的插入(含向上调整算法)

5、堆的删除(含向下调整算法)

【向下/向上调整算法时间复杂度计算】

6、堆的判空、元素个数、取堆顶元素接口

三、堆的应用

1、堆的创建

① 向下调整建堆

② 向上调整建堆

【建堆时间复杂度分析】

2、堆排序

3、TOP-K问题

四、堆相关选择题


一、堆的核心概念与结构特性

堆的本质是满足特定规则的完全二叉树,同时通过数组存储以最大化空间利用率(用连续数组存储完全二叉树,无需额外存储指针(链表存树的方式会浪费指针空间),几乎没有内存浪费)。

1. 堆的定义

堆就是这么个东西:

  • 看着像一棵 “层层填满、最后一层靠左排” 的树(完全二叉树),但实际是存在连续数组里的;
  • 有个铁规矩:要么每个节点都比自己的子节点大/相等(这叫大根堆,最顶上的是最大的数),要么每个节点都比自己的子节点小/相等(这叫小根堆,最顶上的是最小的数);
  • 核心用处就是快速找最大 / 最小数,不用挨个遍历,直接拿最顶上的就行。

2. 核心特性

 逻辑结构:必为完全二叉树,不存在 “断层” 节点,所有叶子节点集中在最后两层,且最后一层节点靠左排列;

 存储结构:物理上是连续数组,通过索引映射父子关系(设节点索引为i):

父节点索引:( i - 1) / 2(向下取整);左子节点索引:2 * i + 1;右子节点索引:2 * i + 2;

 最值特性:堆顶(数组第一个元素)始终是集合中的最大值(大根堆)或最小值(小根堆)。

3. 直观示例

 小根堆:逻辑上根节点最小,数组存储为 [10, 15, 25, 56, 30, 70]

大根堆:逻辑上根节点最大,数组存储为 [70, 56, 30, 15, 25, 10]。 

二、堆的实现 

1、堆的结构

堆的底层是用数组实现的,因此它的结构和顺序表的结构基本一致,具体定义如下:

typedef int HpDataType; typedef struct Heap { HpDataType* a; int size; int capacity; }heap;

2、堆的初始化

//初始化堆 void HeapInit(heap* php) { assert(php); php->a = (HpDataType*)malloc(sizeof(HpDataType) * 4); if (php->a == NULL) { perror("malloc"); return; } php->capacity = 4; php->size = 0; } 

3、堆的销毁

//堆的销毁 void HeapDestroy(heap* php) { assert(php); free(php->a); php->a = NULL; php->capacity = php->size = 0; } 

4、堆的插入(含向上调整算法)

入堆的操作逻辑很简单:先把新数据直接插入到堆的末尾(对应数组的最后一个位置),插入后如果堆的结构不符合小根堆 / 大根堆的规则,就通过向上调整算法,把这个新元素逐步 “往上挪”,直到它处于一个能让整个堆重新满足规则的位置

 

//入堆 void HeapPush(heap* php, HpDataType x) { assert(php); //扩容 if (php->capacity == php->size) { HpDataType* tmp = (HpDataType*)realloc(php->a, sizeof(HpDataType) * php->capacity * 2); if (tmp == NULL) { perror("realloc"); return; } php->a = tmp; php->capacity = 2 * php->capacity; } php->a[php->size] = x; php->size++; //向上调整 AdjustUp(php->a, php->size - 1); } 

新数据插入堆尾的操作,本质上就是顺序表的尾插(这部分内容我们在之前的文章里已经讲过啦),所以这一部分我重点讲向上调整算法的思路

【向上调整算法】:

 

//交换算法 void swap(HpDataType* p1, HpDataType* p2) { HpDataType x = *p1; *p1 = *p2; *p2 = x; } //向上调整 void AdjustUp(HpDataType* a, int child) { //计算父节点的索引 int parent = (child - 1) / 2; // 循环条件:child > 0 表示当前节点还没到堆顶(堆顶节点的child为0,无父节点) // 只要没到堆顶,就持续检查当前节点与父节点是否符合堆的规则 while (child > 0) { //孩子小于父亲就交换值 if (a[child] < a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } //符合堆的规则就跳出循环 else { break; } } }

这段向上调整算法是针对小根堆设计的;如果要适配大根堆,只需要把判断条件改成 “子节点值大于父节点” 即可。

5、堆的删除(含向下调整算法)

堆的删除操作是针对堆顶元素的,具体逻辑是:先把堆顶元素和堆的最后一个元素交换位置,接着删除数组末尾(原堆顶元素),最后对新的堆顶元素执行向下调整算法,让堆重新满足规则。

之所以不直接删除堆顶元素,是因为堆顶是堆的核心节点,直接删除会彻底打乱整个堆的父子节点逻辑,后续几乎无法恢复成合法的堆结构。

 

 

//出堆 void HeapPop(heap* php) { assert(php); assert(!HeapEmpty(php)); //删除数据 swap(&php->a[0], &php->a[php->size - 1]); php->size--; //向下调整 AdjustDown(php->a, php->size, 0); } 

【向下调整算法】:

 

//交换算法 void swap(HpDataType* p1, HpDataType* p2) { HpDataType x = *p1; *p1 = *p2; *p2 = x; } //向下调整 void AdjustDown(HpDataType* a, int n, int parent) { int child = 2 * parent + 1;//假设左孩子小 while (child < n) { //选出孩子中小的那一个 if (child + 1 < n && a[child + 1] < a[child])//避免越界访问 { ++child;//假如右孩子小,child++ } if (a[child] < a[parent]) { swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } 

这段代码实现的是小根堆的向下调整逻辑。如果要适配大根堆的出堆操作,只需要把两处判断条件的符号修改为 “大于” 即可:

子节点选择部分:将 a[child + 1] < a[child] 改为 a[child + 1] > a[child](选出子节点中更大的那个);父子节点交换部分:将 a[child] < a[parent] 改为 a[child] > a[parent](若子节点值大于父节点,执行交换)。

【向下/向上调整算法时间复杂度计算】

 1、向上调整的过程,是从堆的某个叶子节点(新插入节点)向堆顶移动,调整的次数由堆的高度决定。

2、由于堆是完全二叉树结构,若堆的元素个数为 n,则完全二叉树的高度为 log2(N + 1)

3、因此,向上调整算法的时间复杂度为 O(logN)(在数据结构中log以2为底N的对数可以写作O(logN))

(向下调整算法的时间复杂度逻辑一致:从堆顶向叶子节点移动,最大次数也是树的高度,复杂度同样为 O(logN) 

6、堆的判空、元素个数、取堆顶元素接口

//取堆顶元素 HpDataType HeapTop(heap* php) { assert(php); return php->a[0]; } //判空 bool HeapEmpty(heap* php) { assert(php); return php->size == 0; } //堆元素个数 int HeapSize(heap* php) { assert(php); return php->size; }

三、堆的应用

1、堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?

① 向下调整建堆

向下调整建堆逻辑:这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆;

建大根堆
// n: 数组中元素的总个数 // 核心逻辑:从最后一个非叶子节点开始,向前逐个调整节点,使每个子树满足堆的性质 // 最后一个非叶子节点的下标计算: // 数组下标从0开始时,最后一个元素的下标是n-1,它的父节点下标为 (n-1-1)/2 for (int i = (n - 1 - 1) / 2; i >= 0; i--) { // 对下标为i的节点进行"向下调整",使以i为根的子树成为堆 AdjustDown(a, n, i); }

② 向上调整建堆

向上调整法建堆:从第 2 个元素(i=1)开始,逐个对每个元素调用 AdjustUp,让新元素向上和父节点比较、交换,直到满足堆的性质,最终把整个数组建成堆;

//建堆 -- 向上调整建堆 for (int i = 1; i < n; i++) { AdjustUp(a, i); } 

我就不画图展示啦,这个过程其实相当于模拟堆的插入操作:每往堆里加入一个新元素,就对它做一次向上调整;等所有元素都插入并调整完毕,整个堆也就构建完成了。

【建堆时间复杂度分析】

向下调整建堆:O (N)

 

 


向上调整建堆:O(N * logN)

 

核心差异:

 1、向上调整建堆:低层(数量多)+ 调整次数多(底层节点要逐层往上调,最多需 logN 次),最终总代价是 “大量节点 × 多次调整”→ O (N logN)。

 2、向下调整建堆:高层(数量少)+ 调整次数多(根节点要逐层往下调,最多需 logN 次);

低层(数量多)+ 调整次数少(底层节点基本不用调),最终总代价是 “少量节点 × 多次调整 + 大量节点 × 少量调整”→ O (N)。


2、堆排序

堆排序(升序)思想:先建大顶堆(堆顶是最大值),再反复将堆顶与堆尾交换(最大值归位),然后调整剩余元素为大顶堆,直到堆空,数组升序

//排升序 建大堆 O(N*logN) void HeapSort_Up(int *a, int n) { //建堆 -- 向下调整建堆 -- O(N) for (int i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown(a, n, i); } int end = n - 1; //排序 -- O(N*logN) while (end > 0) { swap(&a[end], &a[0]); AdjustDown(a, end, 0); end--; } } 

拍降序的话,建小顶堆就行:堆顶始终是当前堆的最小值,把它和堆尾元素交换(最小值就放到了当前未排序区的末尾,逐步归位),再将剩下的元素重新调整为小顶堆,重复这个过程,直到堆处理完,数组就降序排好了。

//排降序 建小堆 O(N*logN) void HeapSort_Down(int* a, int n) { //建堆 -- 向上调整建堆 -- O(N*logN) for (int i = 1; i < n; i++) { AdjustUp(a, i); } int end = n - 1; //排序 -- O(N*logN) while (end > 0) { swap(&a[end], &a[0]); AdjustDown(a, end, 0); end--; } } 

3、TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1、用数据集合中前K个元素来建堆前k个最大的元素,则建小堆前k个最小的元素,则建大堆

2、用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
//TOP-K 求前k个最大/最小的元素 //前k个最大的元素,则建小堆 //前k个最小的元素,则建大堆 //当前 n = 15; k = 10; //求前10个最大的元素: void TOP_K() { //造数据 int n = 15; int* a = (int*)malloc(sizeof(int) * n); srand((unsigned int)time(NULL)); for (size_t i = 0; i < n; ++i) { a[i] = rand() % n + 1; } int k = 10; //向下调整建堆 -- 建小堆 for (int i = (k - 1 - 1)/ 2; i >= 0; i--) { AdjustDown(a, 10 ,i); } //接下来剩下的元素和堆顶的元素进行比较 for (int i = k; i < n; i++) { if (a[0] < a[i]) { swap(&a[0], &a[i]); //向下调整 AdjustDown(a, 10, 0); } } //打印所有数据,观察是否满足 for (int i = 0; i < n; i++) { printf("%d ",a[i]); } } 

四、堆相关选择题

  

堆的判断需验证序列是否符合 “大根堆(父节点≥子节点)” 或 “小根堆(父节点≤子节点)” 的规则。以选项 A [100,60,70,50,32,65] 为例:

 根节点 100,左子节点 60、右子节点 70(100≥60 且 100≥70,符合大根堆);节点 60,左子节点 50、右子节点 32(60≥50 且 60≥32,符合);节点 70,左子节点 65(70≥65,符合);所有节点均满足大根堆规则,因此 A 是堆。 

 


 

只有向下调整建堆的结果,所以选 C


 

    

Read more

【保姆级】无需公网 IP!Windows 本地一键部署 OpenClaw,10 分钟打造你的飞书 AI 数字员工

【保姆级】无需公网 IP!Windows 本地一键部署 OpenClaw,10 分钟打造你的飞书 AI 数字员工

目录 写在前面 OpenClaw 是什么? 蓝耘平台是什么?与 OpenClaw 的关系 步骤一:极速安装,一行命令搞定环境 步骤二:启动向导,初始化配置参数 步骤 三:注入灵魂,获取蓝耘MaaS API Key 步骤四:打通渠道,搭建飞书长连接桥梁 步骤五:引擎点火,启动核心网关服务 步骤六:仪表盘检阅,后台状态可视化 步骤七:实战演练,验证智能交互效果 快速排错提示 写在末尾 写在前面 本文面向:想在 Windows 本地(PowerShell)一键部署 OpenClaw,使用蓝耘MaaS作为大模型,并通过飞书长连接模式实现 AI 机器人的用户。 内容涵盖:从零开始安装配置、对接飞书机器人、验证与排错的完整流程,

By Ne0inhk
学生党自动排版 AI 写论文工具推荐(小白必备)

学生党自动排版 AI 写论文工具推荐(小白必备)

对于论文写作小白,自动排版是最能节省时间、避免格式错误的核心功能。以下是 2026 年实测好用的 5 款工具,覆盖从选题到终稿的全流程,尤其适合学生党快速上手、高效完成论文。 一、全流程王者:PaperRed(推荐指数:★★★★★) 核心优势:一站式解决论文写作所有痛点,自动排版功能尤为强大 * 智能排版:内置 2000 + 高校专属模板,一键匹配本校格式规范(字体、行距、页眉页脚、标题层级全到位) * 文献自动排版:支持 GB/T 7714 全类型引用,自动生成参考文献列表,避免手动编号错误 * 动态目录:章节修改后自动更新,公式编号、图表引用精准匹配 * 小白友好:分步引导式写作,从选题到答辩 PPT 全流程辅助,降低学术写作门槛 * 免费额度:基础排版、查重、AIGC 检测免费,

By Ne0inhk
一篇文章讲清楚:在单片机上部署AI的整个流程!(TinyML+STM32)

一篇文章讲清楚:在单片机上部署AI的整个流程!(TinyML+STM32)

本公众号的老粉丝比较清楚,我们正在做嵌入式AI的课程,其中最主要的目的是教会大家如何将AI部署到单片机中,让单片机学会思考,可以实现自动推理。 最近文章很久没有更新,是因为小编在忙着录制课程,已经报名课程的同学感觉直播讲课太慢并且还不能快放,所以我们将直播课程改为了“录播+直播答疑”的形式。 今天小编带大家讲清楚整个部署的流程是什么样的! 上面这张图是本次课程出场次数比较多的“老演员”。下面本文以上面图为例,讲解清楚整个流程。 从上面流程中可以发现,共分为了8个主要的步骤。其中最上面带着芯片图片的代表相关工作是在单片机或MCU测完成的。即:数据采集和运行推理是在MCU测进行的。其他步骤在云端或者PC端运行。 一:收集数据(数据采集) 收集数据是整个TInyML的第一步,目的是为了进行模型训练。 数据采集是“把物理世界的真实信号,转换为MCU可以识别的数字信号”。比如把环境或者设备的状态转变为温度,把(电机负载的转换为电流、把声音转换为麦克风的信号、把震动或者冲击转换为加速度计或陀螺仪。 再用一句简单的话讲解:其实就是单片机的ADC 虽然看似简单的一句话,但是整个

By Ne0inhk
用老 Mac 跑本地 AI:OpenClaw 环境一键搭建

用老 Mac 跑本地 AI:OpenClaw 环境一键搭建

用老 Mac 跑本地 AI:OpenClaw 环境一键搭建 老款 Mac 可以通过一键搭建 OpenClaw 环境,快速部署本地 AI 服务。本文将详细介绍如何使用自动化脚本一键搭建 OpenClaw 环境,让老 Mac 发挥余热,成为强大的本地 AI 工作站。 一、硬件要求 1.1 最低配置 组件最低配置推荐配置说明CPUIntel i3 第 3 代Intel i5 第 4 代及以上支持 VT-x/VT-d内存4GB8GB 或更高DDR3存储128GB SSD256GB SSD 或更高SATA 或 NVMe网络Wi-FiWi-Fi + 有线有线网络优先

By Ne0inhk