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

数据结构基础:二叉树与堆的实现详解

数据结构中的树与二叉树是核心基础,涵盖树的定义、术语、存储结构及特殊二叉树类型。重点阐述堆的原理与大根堆实现,包括初始化、插入、销毁等操作,通过 C 语言代码演示顺序表存储下的堆调整算法,适合希望深入理解底层数据结构的开发者。

dehua dong发布于 2026/3/27更新于 2026/6/212 浏览
数据结构基础:二叉树与堆的实现详解

树部分知识

一、树的概念和结构

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

文章配图

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

根据上述定义,我们可以得出:

文章配图

图中的树都不是合法的树结构,因为子树之间存在交集。

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

二、树的相关术语和定义

接下来介绍树的一些关键术语:

文章配图

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

文章配图

三、树的实现结构

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

1. 双亲表示法

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

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

2. 孩子表示法

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

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))。
  • 适用场景:频繁查询子节点的场景(如多叉树的遍历)。

3. 孩子双亲表示法

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

typedef struct CTNode {
    int child; // 孩子节点的数组下标
    struct CTNode* next; // 指向下一个孩子
} ChildPtr;
typedef struct {
    int data; // 数据域
    int parent; // 双亲节点的数组下标
    ChildPtr* firstchild; // 孩子链表头指针
} CTBox;
typedef struct {
    CTBox nodes[MAX_TREE_SIZE]; // 节点数组
    int r, n; // 根节点位置、总节点数
} CTree;
特性双亲表示法孩子表示法孩子双亲表示法
查找双亲O(1)O(n)O(1)
查找孩子O(n)O(k)O(k)
内存占用低中高
结构复杂度简单中等复杂

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

四、树的应用场景

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

文章配图

二叉树部分知识讲解

一、二叉树概念与结构

在树形结构中,我们最常用的是二叉树。定义为:

二叉树是由 n(n≥0)个节点组成的有限集合,满足:空集合(空二叉树);或由一个根节点和两棵互不相交的左子树、右子树组成,左、右子树本身也是二叉树。

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

文章配图

补充说明:

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

二、特殊二叉树类型

特殊二叉树是在普通二叉树基础上,通过结构约束或功能特性形成的特定形态,常见的包括满二叉树、完全二叉树。

1. 满二叉树

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

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

文章配图

2. 完全二叉树

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

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

文章配图

3. 性质补充

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

三、二叉树存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。实现结构取决于树的数据情况。

顺序结构

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

文章配图

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

文章配图

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

链式结构

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

文章配图

四、堆的概念与结构

1. 实现顺序结构二叉树

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

2. 堆的概念与结构

概念:如果有一个关键码的集合 K = {k₀, k₁, k₂, ... , kₙ₋₁},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:kᵢ <= k₂ᵢ₊₁ 且 kᵢ <= k₂ᵢ₊₂ (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,则无右孩子。

文章配图

3. 堆的实现

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

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 HPDestroy(HP* h);

代码:

void HPDestroy(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(堆插入)三个函数,核心是通过'扩容→插入→调整'维持堆的性质(以大顶堆为例)。

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");
}

六、现有的堆代码测试

接下来展示完整的代码部分,本代码分三个文件完成:

heap.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 HPDestroy(HP* h);
void HPPush(HP* h, type x);
void AdjustUp(type* a, int child);
void swap(int* a, int* b);
void HPPrint(HP* h);

heap.c

#include"heap.h"

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

void HPDestroy(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");
}

test.c

#include"heap.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);
    HPDestroy(&h);
}

int main() {
    test();
    return 0;
}

测试结果展示了堆插入后的大顶堆顺序,符合预期。

总结

本文涵盖了树的基本概念、术语、存储结构及特殊二叉树类型,重点讲解了堆的原理与大根堆的具体实现。通过 C 语言代码演示了初始化、插入、销毁等关键操作,以及顺序表存储下的堆调整算法。希望这些内容能帮助你更好地掌握底层数据结构。

目录

  1. 树部分知识
  2. 一、树的概念和结构
  3. 二、树的相关术语和定义
  4. 三、树的实现结构
  5. 1. 双亲表示法
  6. 2. 孩子表示法
  7. 3. 孩子双亲表示法
  8. 四、树的应用场景
  9. 二叉树部分知识讲解
  10. 一、二叉树概念与结构
  11. 二、特殊二叉树类型
  12. 1. 满二叉树
  13. 2. 完全二叉树
  14. 3. 性质补充
  15. 三、二叉树存储结构
  16. 顺序结构
  17. 链式结构
  18. 四、堆的概念与结构
  19. 1. 实现顺序结构二叉树
  20. 2. 堆的概念与结构
  21. 3. 堆的实现
  22. 五、堆的实现代码部分
  23. 1. 堆的初始化
  24. 2. 堆的销毁
  25. 3. 堆的插入数据
  26. 4. 堆打印值
  27. 六、现有的堆代码测试
  28. heap.h
  29. heap.c
  30. test.c
  31. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • JavaScript Proxy 代理机制与核心方法详解

相关免费在线工具

  • 加密/解密文本

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