跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C算法

数据结构堆详解:核心特性、实现与应用

深入解析数据结构中的堆。堆是一种满足特定规则的完全二叉树,通常用数组存储。分为大根堆和小根堆,支持 O(1) 获取最值,O(logN) 插入删除。文章涵盖堆的定义、结构、初始化、销毁、插入(向上调整)、删除(向下调整)等核心实现细节。同时介绍了建堆方法(向下/向上调整)、堆排序及 Top-K 问题解决方案,并包含相关选择题解析,帮助理解堆在高效处理最值问题中的应用。

修罗发布于 2026/3/30更新于 2026/10/2596 浏览
数据结构堆详解:核心特性、实现与应用

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

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

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

文章配图

文章配图

文章配图

目录

  1. 一、堆的核心概念与结构特性
  2. 1. 堆的定义
  3. 2. 核心特性
  4. 3. 直观示例
  5. 二、堆的实现
  6. 1. 堆的结构
  7. 2. 堆的初始化
  8. 3. 堆的销毁
  9. 4. 堆的插入(含向上调整算法)
  10. 5. 堆的删除(含向下调整算法)
  11. 【向下/向上调整算法时间复杂度计算】
  12. 6. 堆的判空、元素个数、取堆顶元素接口
  13. 三、堆的应用
  14. 1. 堆的创建
  15. ① 向下调整建堆
  16. ② 向上调整建堆
  17. 【建堆时间复杂度分析】
  18. 2. 堆排序
  19. 3. TOP-K 问题
  20. 四、堆相关选择题

更多推荐文章

查看全部
  • SpringDoc 基本使用指南
  • OpenClaw 多 Agent 与飞书机器人配置实战
  • KES V9 2025 构建 AI 时代数据基础设施
  • 彻底解决 Copilot 与 Codex 中文乱码问题(附自动化脚本)
  • AI Agent 学习路线:从小白到大神的 20 篇精选文章
  • Stable Diffusion 模型技术深度解析
  • 大模型实战:深入解析 LLaMA 核心算子 RMSNorm 开发
  • Llama 3-8B-Instruct 在昇腾 NPU 上的 SGLang 性能实测
  • Mac Intel 本地 LLM 部署实践:Ollama、Transformers 与 OpenVINO 对比
  • Vue 项目国际化配置指南:i18n 实战
  • OpenClaw Memory 本地模式配置:Ubuntu CUDA cuDNN llama.cpp
  • 分治算法实战:归并排序与数组逆序对详解
  • ZYNQ PS-PL 架构解析:ARM 与 FPGA 融合设计及通信机制
  • VLA 模型真实世界分布式在线后训练系统 SOP 解析
  • 3 步掌握 pywencai:Python 金融数据采集实战
  • Supabase 全栈开发实战:从云服务到本地部署
  • Trae IDE 安装与使用教程
  • VSCode 中配置与使用 Copilot MCP 快速上手指南
  • 生成式 AI 与大语言模型(LLM)原理简明教程
  • SpringBoot 集成 Spring AI 实现简易智能助手

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online