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

数据结构基础:二叉树概念、存储结构与堆实现

树与二叉树是核心数据结构,梳理了树的定义、术语及存储方式,重点解析完全二叉树与堆的特性。通过 C 语言实现大顶堆,演示初始化、扩容、插入时的向上调整算法及内存管理细节,帮助读者掌握堆在优先级队列等场景下的底层逻辑。

疯疯癫癫发布于 2026/3/20更新于 2026/7/1933 浏览
数据结构基础:二叉树概念、存储结构与堆实现

树的概念与结构

树是一种非线性的数据结构,由 n(n>=0)个有限结点组成一个具有层次关系的集合。之所以称为'树',是因为它看起来像一棵倒挂的树,根朝上,叶朝下。

有一个特殊的结点,称为根结点,根结点没有前驱结点。除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm,其中每一个集合 Ti(1 <= i <= m) 又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有 0 个或多个后继。因此,树是递归定义的。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构了。

文章配图 文章配图

根据定义可以得出几个关键结论:

  • 子树是不相交的(如果存在相交就是图了)。
  • 除了根结点外,每个结点有且仅有一个父结点。
  • 一棵 N 个结点的树有 N-1 条边。

树的相关术语

理解树的结构需要掌握以下术语:

文章配图

  • 父结点/双亲结点:若一个结点含有子结点,则这个结点称为其子结点的父结点。
  • 子结点/孩子结点:一个结点含有的子树的根结点称为该结点的子结点。
  • 结点的度:一个结点有几个孩子,它的度就是多少。
  • 树的度:一棵树中,最大的结点的度称为树的度。
  • 叶子结点/终端结点:度为 0 的结点称为叶结点。
  • 分支结点/非终端结点:度不为 0 的结点。
  • 兄弟结点:具有相同父结点的结点互称为兄弟结点。
  • 结点的层次:从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推。
  • 树的高度或深度:树中结点的最大层次。
  • 结点的祖先:从根到该结点所经分支上的所有结点。
  • 路径:一条从树中任意节点出发,沿父节点 - 子节点连接,达到任意节点的序列。
  • 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
  • 森林:由 m(m>0)棵互不相交的树的集合称为森林。

树的实现结构

树结构相对线性表比较复杂,既要保存值域部分,也要保存结点和结点之间的关系。实际中树有很多种表示方式,这里介绍几种常见的存储方法:

双亲表示法

核心思想是用数组存储每个节点,每个节点记录其双亲节点的数组下标。

#define MAX_TREE_SIZE 100 
 
     data; 
     parent; 
} PTNode;
 
    PTNode nodes[MAX_TREE_SIZE]; 
     r, n; 
} PTree;
typedef
struct PTNode {
int
// 数据域
int
// 双亲节点的数组下标(根节点为 -1)
typedef
struct {
// 节点数组
int
// 根节点位置、总节点数
  • 优点:查找双亲节点极快(O(1))。
  • 缺点:查找孩子节点需遍历整个数组(O(n))。
  • 适用场景:频繁查询父节点的场景。

孩子表示法

核心思想是每个节点的所有孩子节点用单链表存储,再用数组记录每个节点的数据和孩子链表的头指针。

typedef struct CTNode {
    int child; // 孩子节点的数组下标
    struct CTNode* next; // 指向下一个孩子
} ChildPtr;
typedef struct {
    int data; // 数据域
    ChildPtr* firstchild; // 孩子链表头指针
} CTBox;
typedef struct {
    CTBox nodes[MAX_TREE_SIZE]; // 节点数组
    int r, n; // 根节点位置、总节点数
} CTree;
  • 优点:查找孩子节点高效(直接遍历链表)。
  • 缺点:查找双亲节点需遍历所有节点(O(n))。
  • 适用场景:频繁查询子节点的场景(如多叉树的遍历)。

孩子双亲表示法

结合前两种方法,每个节点同时记录双亲下标和孩子链表头指针,兼顾双亲与孩子的查询效率。

特性双亲表示法孩子表示法孩子双亲表示法
查找双亲O(1)O(n)O(1)
查找孩子O(n)O(k)O(k)
内存占用低中高
结构复杂度简单中等复杂

其中最常用的是孩子兄弟表示法。此部分作为了解,在实际应用中很少使用这种复杂的树结果,像二叉树这种结构的应用相对来说更广泛。

树的应用场景

文件系统是计算机存储和管理文件的一种方式,它利用树形结构来组织和管理文件和文件夹。在文件系统中,树结构被广泛应用,它通过父结点和子结点之间的关系来表示不同层级的文件和文件夹之间的关联。

文章配图

二叉树概念与结构

在树形结构中,我们最常用的是二叉树。定义为:由 n(n≥0)个节点组成的有限集合,满足:空集合(空二叉树);或由一个根节点和两棵互不相交的左子树、右子树组成,左、右子树本身也是二叉树。

基本形态有 5 种:空二叉树、仅含根节点、根节点 + 左子树、根节点 + 右子树、根节点 + 左子树 + 右子树。

文章配图

补充说明:

  1. 二叉树不存在度大于 2 的结点。
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

特殊二叉树类型

特殊二叉树是在普通二叉树基础上,通过结构约束或功能特性形成的特定形态。

满二叉树

如果一个二叉树层次为 k,其结点总数就是 2^k-1,则它就是满二叉树。也就是说,每一个层的结点数都达到最大值。

注:判断方法:深度为 k 且节点总数为 2ᵏ - 1 的二叉树,每一层节点数达到最大值,所有叶子节点集中在最底层。每个节点要么是叶子节点(度为 0),要么有两个子节点(度为 2),不存在度为 1 的节点。

文章配图 文章配图

完全二叉树

完全二叉树是由满二叉树而来的。对于深度为 k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时称之为完全二叉树。注意满二叉树是一种特殊的完全二叉树。

完全二叉树注意:

  • 叶子节点仅在最后两层,且最底层叶子靠左连续排列。
  • 若有度为 1 的节点,最多 1 个且仅含左孩子。
  • 满二叉树是完全二叉树的特殊情况,但反之不成立。

文章配图 文章配图

性质补充

  • 若规定根结点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i−1) 个结点。
  • 若规定根结点的层数为 1,则深度为 h 的二叉树的最大结点数是 2^h − 1。
  • 若规定根结点的层数为 1,具有 n 个结点的满二叉树的深度 h = log₂(n + 1)。

二叉树存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

顺序结构

顺序结构存储实际上就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使用顺序结构存储。

文章配图

至于为什么完全二叉树更适合使用顺序结构存储,与它的值的结构有关。顺序结构存储实际上就是使用数组来存储,为了访问到各个节点,需要将所有的节点存入数组中,但如果树中有很多的空节点,不存储任何值,我们也需要为空节点开辟空间,那样会造成空间上的浪费。

例: 文章配图

应用:现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

链式结构

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。到后面讲解高阶数据结构如红黑树等会用到三叉链。所以目前专心讲解二叉树知识。

文章配图 文章配图 文章配图

堆的概念与结构

实现顺序结构二叉树

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

堆的概念与结构

概念:如果有一个关键码的集合 K = {k₀, k₁, k₂, ... , kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki <= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2),i = 0、1、2...,则称为小堆 (或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

文章配图 文章配图

简单来说:大根堆的根节点值最大,小根堆的根节点值最小。堆中某个结点的值总是不大于或不小于其父结点的值;堆总是一棵完全二叉树。

性质:对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:

  • 若 i>0,i 的位置结点的双亲序号:(i-1)/2;i=0,i 为根结点编号,无双亲结点。
  • 若 2i+1<n,左孩子序号:2i+1,2i+1>=n,则无左孩子。
  • 若 2i+2<n,右孩子序号:2i+2,2i+2>=n,则无右孩子。

文章配图

堆的实现

堆底层结构为数组,因此定义堆的结构也与顺序表相同:

typedef int type;
typedef struct Heap {
    type* a;
    int size;
    int capacity;
}HP;

操作方面,也与顺序表相同,不同点是,堆是有大根堆、小根堆的结构要求的,所以实现时也不是那么容易。

堆的实现代码部分

1. 堆的初始化

堆的初始化是创建一个空堆并设置其初始容量、指针等基本属性的过程,通常分为大顶堆(父节点值≥子节点)和小顶堆(父节点值≤子节点)两种类型。本次实现选取大堆为例。

函数声明:void HPInit(HP* h);

实现代码:

void HPInit(HP* h) {
    assert(h);
    h->a = NULL;
    h->capacity = h->size = 0;
}

讲解:由于堆的底层是顺序表,所以,初始化的实现与顺序表的实现基本一致。

2. 堆的销毁

堆的销毁函数核心目标是释放动态分配的内存,避免内存泄漏,并将堆重置为空状态,实现也与顺序表一样。

函数:void HPDestory(HP* h)

代码:

void HPDestory(HP* h) {
    assert(h);
    if (h->a) {
        free(h->a);
    }
    h->capacity = h->size = 0;
}

3. 堆的插入数据

注意:堆的插入,是有大根堆、小根堆要求的,所以每当我们插入一个值时,都要进行堆调整。 需遵循'先插入元素,再向上调整'的核心逻辑。

至于:向上调整,举例: 未插入之前:符合堆结构 插入值:可能破坏堆结构 调整之后:恢复堆结构

在每次插入之前,我们认为是符合该(大根堆或小根堆)结构的状态,插入的值如果使树不符合该结构,则需让新插入的元素'上浮'到正确位置,恢复堆的性质。

所应用的函数:

  • void HPPush(HP* h, type x) // 插入函数
  • void AdjustUp(type* a, int child) // 向上调整算法
  • void swap(int* a, int* b) // 交换值函数

代码:(由于只有入堆会引起扩容,所以扩容代码,我将其写在了插入函数内)

void swap(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}

void AdjustUp(type* a, int child) {
    assert(a);
    int parent = (child - 1) / 2;
    while (child > 0) {
        if (a[parent] < a[child]) {
            swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1) / 2;
        } else {
            break;
        }
    }
}

void HPPush(HP* h, type x) {
    assert(h);
    if (h->capacity == h->size) {
        int n = h->capacity == 0 ? 4 : 2 * h->capacity;
        type* tmp = (type*)realloc(h->a, n*sizeof(type));
        if (tmp == NULL) {
            perror("realloc failed");
            return;
        }
        h->a = tmp;
        h->capacity = n;
    }
    h->a[h->size++] = x;
    AdjustUp(h->a, h->size - 1);
}

讲解:堆的插入操作实现,包含 swap(交换)、AdjustUp(向上调整)和 HPPush(堆插入)三个函数,核心是通过'扩容→插入→调整'维持堆的性质(以大顶堆为例)。swap 函数是交换两个整数的辅助函数,AdjustUp 负责向上调整,HPPush 则是堆的插入入口。

4. 堆打印值

堆打印值实际上与顺序表的值的打印一样,只是,结果是按照堆的值的顺序。

函数:void HPPrint(HP* h);

代码:

void HPPrint(HP* h) {
    assert(h);
    int i;
    for (i = 0; i < h->size; i++) {
        printf("%d ", h->a[i]);
    }
    printf("\n");
}

现有的堆代码的测试

接下来,我将展示我已经写完的代码部分:

本代码分三个文件完成:

头文件 (1.h)

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int type;
typedef struct Heap {
    type* a;
    int size;
    int capacity;
}HP;
void HPInit(HP* h);
void HPDestory(HP* h);
void HPPush(HP* h, type x);
void AdjustUp(type* a,int child);
void swap(int* a, int* b);
void HPPrint(HP* h);

实现文件 (1.c)

#include"1.h"
void HPInit(HP* h) {
    assert(h);
    h->a = NULL;
    h->capacity = h->size = 0;
}
void HPDestory(HP* h) {
    assert(h);
    if (h->a) {
        free(h->a);
    }
    h->capacity = h->size = 0;
}
void swap(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}
void AdjustUp(type* a, int child) {
    assert(a);
    int parent = (child - 1) / 2;
    while (child > 0) {
        if (a[parent] < a[child]) {
            swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1) / 2;
        } else {
            break;
        }
    }
}
void HPPush(HP* h, type x) {
    assert(h);
    if (h->capacity == h->size) {
        int n = h->capacity == 0 ? 4 : 2 * h->capacity;
        type* tmp = (type*)realloc(h->a, n*sizeof(type));
        if (tmp == NULL) {
            perror("realloc failed");
            return;
        }
        h->a = tmp;
        h->capacity = n;
    }
    h->a[h->size++] = x;
    AdjustUp(h->a, h->size - 1);
}
void HPPrint(HP* h) {
    assert(h);
    int i;
    for (i = 0; i < h->size; i++) {
        printf("%d ", h->a[i]);
    }
    printf("\n");
}

主程序 (main.c)

#include"1.h"
void test() {
    HP h;
    HPInit(&h);
    HPPush(&h, 10); //10
    HPPush(&h, 20); //20 10
    HPPush(&h, 6);  //20 10 6
    HPPrint(&h);
    HPPush(&h, 30); //30 20 6 10
    HPPrint(&h);
    HPPush(&h, 60); //60 30 6 10 20
    HPPrint(&h);
    HPDestory(&h);
}
int main() {
    test();
}

测试结果: 文章配图 文章配图

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为:

  1. 树讲解:树的概念与结构、树相关术语、树的表示方法、树形结构实际运用场景。
  2. 二叉树:概念与结构、特殊的二叉树讲解、二叉树存储结构、实现顺序结构二叉树的相关内容。

希望这些内容能帮助你更好地理解数据结构中的树与堆。后续章节会继续更新其他相关内容。

目录

  1. 树的概念与结构
  2. 树的相关术语
  3. 树的实现结构
  4. 双亲表示法
  5. 孩子表示法
  6. 孩子双亲表示法
  7. 树的应用场景
  8. 二叉树概念与结构
  9. 特殊二叉树类型
  10. 满二叉树
  11. 完全二叉树
  12. 性质补充
  13. 二叉树存储结构
  14. 顺序结构
  15. 链式结构
  16. 堆的概念与结构
  17. 实现顺序结构二叉树
  18. 堆的概念与结构
  19. 堆的实现
  20. 堆的实现代码部分
  21. 1. 堆的初始化
  22. 2. 堆的销毁
  23. 3. 堆的插入数据
  24. 4. 堆打印值
  25. 现有的堆代码的测试
  26. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Leptos + Tauri 2 配置指南:Trunk、SSG 与移动端热重载
  • Java 大数据在智能家居能源消耗趋势预测与节能策略优化中的应用
  • 大模型:人工智能前沿技术与应用详解
  • 基于 Docker 和 Ollama 部署 DeepSeek 本地大模型
  • ARINC 825 协议核心面试百问
  • Mac 前端开发环境配置指南
  • Java 房屋租赁系统的设计与实现
  • Ubuntu 部署 Ghost 1.0 博客系统实战
  • C++ string 类详解:初始化、遍历与常用成员函数
  • HoloCubic_AIO:基于 ESP32 的多功能开源智能显示终端
  • Spring Boot 数据访问与数据库集成实战
  • 基于 SSM 框架的 Java 共享客栈管理系统设计与实现
  • 基于 Go 语言与大模型的 AIOps 监控系统实战
  • C++ 标准库 string 类详解:接口、模拟实现与写时拷贝
  • 2025 前端技术复盘:框架收敛、AI 赋能与生态趋势
  • Python 数据统计实战指南
  • JavaScript Promise 完整指南
  • 大模型核心面试题解析:Transformer、LoRA、RoPE 等关键技术
  • ComfyUI 基于 CNB 平台的云端搭建与使用指南
  • 2026 年 3 月全球大模型全景:国产登顶与智能体爆发

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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