数据结构|二叉树(一):树的基础概念与二叉树核心结构
本文是二叉树数据结构的入门指南,首先介绍了树的基本概念和核心特点,包括根节点、子节点、层次等术语。然后重点讲解了二叉树的特殊结构和两种常见类型:满二叉树和完全二叉树。文章详细阐述了二叉树的五大核心性质,特别是完全二叉树在顺序存储中的父子节点定位公式。最后对比了顺序存储和链式存储的适用场景,并引入堆结构的概念及其应用。全文为后续深入学习二叉树算法奠定了理论基础,强调理解底层原理而非机械记忆代码。
写在前面
在前面的数据结构学习中,我们已经完整梳理了线性数据结构部分:
- 顺序表与链表:研究数据元素之间的一对一关系;
- 栈与队列:研究操作受限制的线性结构。
这些结构有一个共同特点:数据元素之间存在一条明确的线性关系,每个元素最多只有一个前驱和一个后继,形如A → B → C → D。
但在实际开发中,很多数据并不是简单的一条线:文件系统的目录结构、公司的组织架构、数据库的索引结构、搜索引擎的层级关系……它们更符合“层层向下展开”的形态。
因此,从这一篇开始,我们正式进入非线性数据结构的世界——树(Tree)。 而在所有树结构中,最经典、应用最广泛、也是整个数据结构体系核心的,就是二叉树(Binary Tree)。
前置知识推荐
在正式开始二叉树学习之前,非常建议先补一篇 C 语言底层原理内容
<a href="/#post/2026-08-20-function-stack-frame-1">C语言底层:函数栈帧的创建与销毁(一)|从汇编视角看懂函数调用的本质简述</a>
这篇内容不属于数据结构专题,但它讲解的「函数调用栈帧机制」,是理解二叉树递归的关键底层支撑。
为什么要先懂栈帧?
后续二叉树的学习中,我们会大量接触递归遍历、深度优先搜索(DFS)、树高计算、分治思想。而这些算法的底层,本质上都在依赖函数调用栈。
比如最经典的前序遍历:
void PrevOrder(TreeNode* root)
{
if(root == NULL)
return;
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
表面上是几行简单的递归代码,但程序真正执行时,每一次递归调用都会在系统栈中创建新的函数栈帧:
PrevOrder(A)
↓
PrevOrder(B)
↓
PrevOrder(D)
↓
逐层返回
每进入一层递归,系统都会保存当前函数的局部变量、下一步执行位置,开辟新栈帧继续执行;递归结束时,再按照后进先出的顺序逐层销毁,回到上一层继续执行。
理解了函数栈帧,你才能真正明白:
- 为什么递归可以独立保存每一层的数据;
- 为什么递归深度过大会导致栈溢出;
- 为什么二叉树遍历可以天然用递归实现。
如果对 C 语言内存、函数调用机制还比较陌生,建议先看完栈帧相关内容,再来学习二叉树递归会顺畅很多。
一、树的概念与基础特点
1.1 什么是树
树是由n(n ≥ 0)个有限节点组成的非线性数据结构,节点之间通过连接形成清晰的层次关系。当 n=0 时称为空树。
比如一棵简单的树:
A
/ | \
B C D
/ \
E F
其中 A 是根节点,B、C、D 是 A 的子节点,E、F 是 B 的子节点。
数据结构里的树和现实中的树形态一致,只不过是倒过来画的:根在上,叶子在下。
1.2 树的三个核心特点
- 有且仅有一个根节点:它是整棵树的最顶层节点,没有父节点。
- 除根节点外,每个节点有且仅有一个父节点:不存在一个节点有多个上层节点。
- 不存在环:树中任意两个节点之间有且只有一条路径。如果出现闭环结构,就不再属于树,而是图。
二、树的核心术语
这些术语是树结构的基础语言,必须逐个理解:
- 节点的度:一个节点直接拥有的子节点数量,称为该节点的度。
- 叶节点(终端节点):度为 0 的节点,也就是没有子节点、处于最底层的节点。
- 分支节点(非终端节点):度不为 0 的节点,也就是有子节点、可以继续分叉的节点。
- 父节点(双亲节点):如果一个节点包含子节点,该节点就是其子节点的父节点。
- 子节点(孩子节点):一个节点直接下属的节点,称为该节点的子节点。
- 兄弟节点:拥有同一个父节点的节点,互称为兄弟节点。
- 堂兄弟节点:父节点位于同一层的节点,互称为堂兄弟节点。
- 树的度:整棵树中,所有节点度的最大值。
- 节点的层次:从根节点开始计数,根为第 1 层,根的子节点为第 2 层,以此类推。
- 树的高度(深度):树中节点的最大层次数,也就是这棵树总共有多少层。
- 节点的祖先:从根节点到该节点的路径上,所有经过的节点都是它的祖先。
- 子孙:以某个节点为根的子树中,所有节点都称为该节点的子孙。
- 森林:由 m(m≥0)棵互不相交的树组成的集合,称为森林。
三、树的常见表示方法
普通树最大的特点是:一个节点可能有任意多个孩子。因此不同的表示方法,本质上都是在解决「如何存储多个孩子指针」的问题。
3.1 指针数组表示法(C 语言基础写法)
最直观的思路:每个节点存放数据,再用一个固定长度的指针数组,存储所有孩子节点的地址。
#define MAX_CHILD 10 // 预设最大孩子数量
typedef struct TreeNode
{
int data;
struct TreeNode* child[MAX_CHILD]; // 孩子指针数组
int childNum; // 实际孩子个数
} TreeNode;
优点:结构简单,容易理解。 缺点:孩子数量有上限,固定数组会造成空间浪费;树的度不确定时很难设定合适的数组大小。
3.2 动态数组表示法(C++ vector 写法)
为了解决固定数组的缺陷,C++ 中通常用vector动态数组来存放孩子指针,底层逻辑和指针数组完全一致,只是数组可以按需自动扩容。
struct TreeNode
{
int data;
vector<TreeNode*> child; // 动态数组存储孩子指针
};
优点:无需提前预估孩子数量,空间利用率更高。 本质上仍然是「孩子指针数组」的思路。
3.3 孩子兄弟表示法(最经典通用)
孩子兄弟表示法,也叫「左孩子右兄弟」表示法,是工业界最通用的树表示方式。它的核心思想是:把任意多叉树,统一转换成二叉结构。
每个节点只保存两个指针:
firstChild:指向第一个孩子节点nextBrother:指向右边第一个兄弟节点
typedef struct TreeNode
{
int data;
struct TreeNode* firstChild; // 第一个孩子
struct TreeNode* nextBrother; // 下一个兄弟
} TreeNode;
比如一棵普通树:
A
/ | \
B C D
用孩子兄弟表示法转换后,结构变为:
A
|
B
\
C
\
D
这种写法的优势非常突出:
- 无论树的度是多少,每个节点都只存两个指针,空间固定无浪费;
- 可以表示任意度数的树;
- 也是多叉树转二叉树的核心方法——这正是我们重点研究二叉树的原因:只要把二叉树吃透,所有普通树都可以通过这种方式转化后处理。
四、二叉树的概念与特殊结构
4.1 什么是二叉树
二叉树是树的特殊形式:每个节点最多有两棵子树,节点的度最大为 2。 两个子节点有严格的左右顺序,分别称为左孩子和右孩子,次序不能颠倒。即使节点只有一个孩子,也要明确区分是左还是右。
比如下面两棵树,是完全不同的二叉树:
A A
/ \
B B
4.2 满二叉树
如果一棵二叉树的每一层节点数都达到最大值,所有叶子节点都在最底层,所有分支节点的度都为 2,这就是满二叉树。
A
/ \
B C
/ \ / \
D E F G
核心特点:
- 第 i 层的节点数为
2^(i-1) - 高度为 h 的满二叉树,总节点数为
2^h - 1 - 没有度为 1 的节点,叶子全部在最底层
这里有一个很直观的结论:满二叉树的节点数量是指数级增长的。比如高度只有 30 的满二叉树,总节点数就超过了 10 亿。看似不高的层数,能容纳的数据量极其庞大,这也是树结构查询效率极高的底层原因。
4.3 完全二叉树
完全二叉树是满二叉树的“前半段”:
前 h-1 层全部填满,最后一层的节点从左到右连续排列,中间没有空缺。
A
/ \
B C
/ \ /
D E F
核心特点:
- 前 h-1 层一定是满的;
- 最后一层节点从左向右连续,不能有空隙;
- 满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树。
完全二叉树最大的意义,就是它非常适合用数组顺序存储——空间利用率高,并且可以通过下标公式快速定位父子节点。
五、二叉树的核心性质
性质1:每层最大节点数
二叉树的第 i 层上,最多有2^(i-1)个节点(i ≥ 1)。
性质2:整棵树最大节点数
高度为 h 的二叉树,最多有2^h - 1个节点,对应满二叉树的总节点数。
性质3:叶子节点与度为2节点的关系
对任意一棵二叉树,设度为 0 的叶子节点数为n₀,度为 2 的节点数为n₂,则一定满足:n₀ = n₂ + 1
叶子节点的数量,永远比度为 2 的节点多 1 个。
性质4:顺序存储下的父子下标关系
这是完全二叉树数组实现最核心的公式,也是后续堆结构的基础。
把完全二叉树按层序遍历存入数组,根节点下标为 0 时:
- 若父节点下标为
i左孩子下标 =**2i + 1右孩子下标 =2i + 2**
反过来,已知孩子节点下标为i:
- 父节点下标 =
(i - 1) / 2(整数除法,向下取整)
正是因为有这个公式,我们不需要指针,仅凭数组下标就能快速定位任意节点的父子关系,这也是完全二叉树适合顺序存储的根本原因。
六、二叉树的存储结构简介
二叉树主要有两种存储方式,分别对应不同的场景,选择的核心依据是:空间利用率 + 操作效率。
6.1 顺序存储(数组实现)
实现方式:将二叉树按层序遍历的顺序,依次存入一个连续数组中,通过下标公式计算父子节点的位置。
适用场景:满二叉树、完全二叉树
对于满二叉树和完全二叉树来说,节点是连续排布的,数组中几乎没有空位,空间利用率极高:
- 满二叉树:数组空间 100% 利用,没有任何浪费;
- 完全二叉树:只有最后一层右侧可能有空缺,浪费极少。
同时配合父子下标公式,定位父节点、子节点都只需要一次算术运算,效率极高。这也是堆选择数组作为底层实现的核心原因。
不适用场景:普通二叉树
如果是形态不规则的普通二叉树,用数组存储会造成极其严重的空间浪费。 举个极端例子:一棵只有左孩子的单支树(斜树),高度为 h,按照满二叉树的数组长度需要开辟2^h - 1个空间,但实际有效节点只有 h 个,绝大多数位置都是空的。
因此普通二叉树几乎不会采用顺序存储,更适合用链式结构。
6.2 链式存储(指针实现)
实现方式:每个节点存储数据 + 左孩子指针 + 右孩子指针,和链表的思路类似。
typedef struct TreeNode
{
int data;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
适用场景:普通二叉树
对于形态不确定、节点分布不规则的二叉树,链式存储按需申请节点,没有多余的空间浪费,灵活性极高。 后续我们学习的二叉树遍历、二叉搜索树、平衡树等内容,基本都采用链式存储实现。
七、初识堆(Heap)
既然完全二叉树的顺序存储兼具空间紧凑和父子定位高效的优势,工业界就基于这种结构设计出了一种非常经典实用的数据结构——堆(Heap)。
7.1 什么是堆
堆是一种特殊的完全二叉树,它除了满足完全二叉树的结构要求,还额外满足父子节点之间的大小关系。
主要分为两类:
小根堆
满足:父节点的值 ≤ 所有子节点的值
1
/ \
3 5
/
7
根节点一定是整棵堆中的最小值。
大根堆
满足:父节点的值 ≥ 所有子节点的值
9
/ \
7 8
/
3
根节点一定是整棵堆中的最大值。
7.2 常见误区:堆不是排序结构
这是学习堆最容易踩的坑:堆 ≠ 有序结构。
堆只保证父节点和子节点之间的大小关系,并不保证:
- 左子树全部小于右子树;
- 同一层的节点有序;
- 遍历结果有序。
比如下面这棵小根堆:
1
/ \
5 3
/
8
它满足1<5、1<3、5<8,完全符合小根堆规则;但5 > 3,兄弟节点之间并没有顺序关系。
一句话总结:堆的核心目标是快速获取最大值或最小值,而不是让所有数据有序。
7.3 堆的典型应用
- 优先级队列:普通队列是先进先出,而优先级队列让优先级高的元素先出队,底层通常由堆实现,常用于操作系统任务调度、网络请求处理等场景。
- 堆排序:利用堆的特性实现的排序算法,时间复杂度 O(NlogN)。注意:堆本身不是排序结构,堆排序是通过反复删除堆顶元素的过程完成排序。
- Top K 问题:比如找出海量数据中最大的前 K 个数,用堆可以把复杂度优化到 O(NlogK),是面试高频考点。
写在最后
本篇作为二叉树专题的开篇,我们沿着「 线性结构 → 树结构 → 二叉树 → 完全二叉树 → 顺序存储 → 堆」的脉络,把基础概念、术语、性质和应用场景完整串了一遍。
理解这些前置知识后,再去学堆的实现、二叉树的递归遍历,就不会只是机械地背代码,而是能明白“为什么这么设计”。
下一篇,我们继续学习堆相关的知识,进一步熟悉完全二叉树在工程上的实际运用。