二叉树作为经典的非线性数据结构,是数据结构与算法学习的核心内容。本文从二叉树的基础概念出发,逐步讲解其核心性质、存储结构,并结合 C 语言代码实现二叉树的链式操作(遍历、结点统计、销毁等)。
一、树的基础概念铺垫
在学习二叉树之前,我们需要先了解树的基本定义,因为二叉树是树的特殊形式,掌握树的概念能帮助我们更好地理解二叉树的特性。
1.1 树的定义
树是由n(n≥0)个有限结点组成的层次关系集合,形似倒挂的树(根朝上、叶朝下),核心特点:
- 有且仅有一个根结点,根结点无前驱;
- 除根外,每个结点有且仅有一个父结点,可以有 0 个或多个后继;
- 子树之间互不相交,一棵
N个结点的树必有N-1条边; - 树的定义是递归的,即每个子树也都是一棵独立的树。

1.2 树的核心术语
为了后续描述方便,先明确树的常用术语(这些术语同样适用于二叉树):
| 术语 | 定义 |
|---|---|
| 结点的度 | 一个结点含有的子树个数 |
| 叶结点(终端结点) | 度为 0 的结点(无孩子) |
| 分支结点(非终端结点) | 度不为 0 的结点 |
| 树的度 | 一棵树中最大的结点的度 |
| 结点的层次 | 根为第 1 层,根的孩子为第 2 层,依次类推 |
| 树的高度 / 深度 | 树中结点的最大层次 |
| 森林 | m(m>0)棵互不相交的树的集合 |
1.3 树的常用表示法
树的存储需要同时保存结点值 和结点间的关系 ,最常用的是孩子兄弟表示法,通过结构体实现:
cpp
typedef int DataType;
struct Node
{
struct Node* firstChild; // 指向第一个孩子结点
struct Node* pNextBrother; // 指向下一个兄弟结点
DataType data; // 结点数据域
};
这种表示法的优势是能将任意树结构转化为二叉树结构,也是二叉树能成为树结构核心的重要原因。
二、二叉树的核心概念与性质
二叉树是树的特殊形式,也是实际开发中最常用的树结构,其约束性更强,因此具备更多可利用的特性。
2.1 二叉树的定义
二叉树是结点的有限集合,满足以下两种情况之一:
- 集合为空(空二叉树);
- 由一个根结点 + 两棵互不相交的左子树和右子树组成,且左、右子树也都是二叉树。

核心约束:
- 二叉树中不存在度大于 2 的结点(每个结点最多有 2 个孩子);
- 二叉树的子树有左右之分 ,次序不能颠倒(是有序树),比如只有左孩子和只有右孩子是两棵不同的二叉树。

2.2 两种特殊的二叉树
(1)满二叉树
如果二叉树的每一层结点数都达到最大值,则为满二叉树。
- 若层数为
K,则结点总数为2^K - 1; - 满二叉树的每个分支结点都有左、右两个孩子,叶结点都在最下层。
(2)完全二叉树
完全二叉树由满二叉树引出,是效率最高的二叉树结构:
- 对于深度为
K、有n个结点的二叉树,每个结点都与深度为K的满二叉树中编号 1~n的结点一一对应; - 满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树;
- 完全二叉树的叶结点仅出现在最下层和次下层,且最下层的叶结点都靠左排列。

2.3 二叉树的五大核心性质
- 非空二叉树的第
i层上最多有 2^(i-1) 个结点; - 深度为
h的二叉树最大结点数为 2^h - 1(满二叉树的结点数); - 对任意二叉树,叶结点数
n0= 度为 2 的结点数n2 + 1; - 具有
n个结点的满二叉树,深度h = log2(n+1); - 具有
n个结点的完全二叉树,按0 开始编号 ,对序号为i的结点:- 双亲结点序号:
(i-1)/2(i>0,i=0为根,无双亲); - 左孩子序号:
2i+1(2i+1 < n,否则无左孩子); - 右孩子序号:
2i+2(2i+2 < n,否则无右孩子)。
- 双亲结点序号:
性质 3 推导(关键):
- 总结点数:
N = n0 + n1 + n2(n1为度为 1 的结点数); - 总边数:
N-1 = n1 + 2*n2(度为 0 无边,度为 11 条边,度为 22 条边); - 联立两式,消去
N和n1,得n0 = n2 + 1。
2.4 二叉树的存储结构
二叉树有两种存储方式:顺序存储 和链式存储 ,二者各有适用场景,核心使用链式存储。
(1)顺序存储
用数组 存储二叉树,结点按层序遍历的顺序存入数组,仅适合完全二叉树:
- 优势:通过数组下标可直接找到结点的双亲、孩子,操作简单;
- 劣势:非完全二叉树会出现大量空间浪费(空结点也要占数组位置);
- 实际应用:仅用于堆(特殊的完全二叉树),普通二叉树不使用。

(2)链式存储
用链表 存储二叉树,每个结点保存数据域和指向左、右孩子的指针,是二叉树的主流存储方式,分为两种:
- 二叉链 :包含数据域 + 左孩子指针 + 右孩子指针,最常用;
- 三叉链 :包含数据域 + 左孩子指针 + 右孩子指针 + 双亲指针,适用于需要快速找双亲的场景(如红黑树)。

cpp
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType val; // 结点数据
struct BinaryTreeNode* left; // 左孩子指针
struct BinaryTreeNode* right; // 右孩子指针
}BTNode;
三、二叉树链式结构的实战实现(C 语言)
3.1 代码工程结构
为了代码的模块化和可读性,采用头文件 + 源文件的结构,共 4 个文件:
BinaryTree.h:二叉树的结构体定义和函数声明;BinaryTree.c:二叉树所有操作的函数实现;Queue.h:队列的结构体定义和函数声明(层序遍历需要队列);Queue.c:队列的函数实现(链式队列)。
3.2 基础准备:队列的实现
二叉树的层序遍历 需要借助队列 实现(先进先出),此处实现一个链式队列 ,队列中存储的是二叉树结点的指针(BTNode*)。
(1)队列头文件Queue.h
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 队列存储的是二叉树结点的指针
typedef struct BinaryTreeNode* QDataType;
// 队列结点结构
typedef struct QListNode {
QDataType val;
struct QListNode* next;
}QNode;
// 队列的头/尾指针 + 有效元素个数
typedef struct Queue {
QNode* head;
QNode* tail;
int size;
}Queue;
// 队列操作声明
void QueueInit(Queue* q); // 初始化
void QueuePush(Queue* q, QDataType data); // 入队
void QueuePop(Queue* q); // 出队
QDataType QueueFront(Queue* q); // 获取队头
bool QueueEmpty(Queue* q); // 判断空
void QueueDestroy(Queue* q); // 销毁
(2)队列源文件Queue.c
cpp
#include"Queue.h"
// 初始化队列
void QueueInit(Queue* Q)
{
assert(Q);
Q->head = Q->tail = NULL;
Q->size = 0;
}
// 创建队列结点
QNode* CreatNode(QDataType x)
{
QNode* ret = (QNode*)malloc(sizeof(QNode));
if (ret == NULL)
{
perror("malloc fail!");
exit(1);
}
ret->next = NULL;
ret->val = x;
return ret;
}
// 队尾入队
void QueuePush(Queue* Q, QDataType x)
{
QNode* NewNode = CreatNode(x);
assert(Q);
if (Q->head == NULL)
{
Q->head = Q->tail = NewNode;
}
else
{
Q->tail->next = NewNode;
Q->tail = NewNode;
}
Q->size++;
}
// 队头出队
void QueuePop(Queue* Q)
{
assert(Q);
assert(Q->head); // 空队列不能出队
QNode* next = Q->head->next;
free(Q->head);
Q->head = next;
if (Q->head == NULL)
{
Q->tail = NULL;
}
Q->size--;
}
// 获取队头元素
QDataType QueueFront(Queue* Q)
{
assert(Q);
assert(Q->head);
return Q->head->val;
}
// 判断队列是否为空
bool QueueEmpty(Queue* Q)
{
assert(Q);
return Q->head == NULL;
}
// 销毁队列
void QueueDestroy(Queue* Q)
{
assert(Q);
while (Q->size--)
{
QNode* next = Q->head->next;
free(Q->head);
Q->head = next;
}
Q->head = Q->tail = NULL;
}
3.3 二叉树的核心操作实现
(1)二叉树头文件BinaryTree.h
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
// 二叉树结点数据类型
typedef int BTDataType;
// 二叉链结点定义
typedef struct BinaryTreeNode {
BTDataType val;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 二叉树操作声明
void PreOrder(BTNode* root); // 前序遍历
void InOrder(BTNode* root); // 中序遍历
void PostOrder(BTNode* root); // 后序遍历
int BinaryTreeSize(BTNode* root); // 结点总数
int BinaryTreeLeafSize(BTNode* root);// 叶子结点数
int BinaryTreeLevelKSize(BTNode* root, int k); // 第k层结点数
BTNode* BinaryTreeFind(BTNode* root, BTDataType x); // 查找值为x的结点
void LevelOrder(BTNode* root); // 层序遍历
void BinaryTreeDestory(BTNode* root); // 销毁二叉树
bool BinaryTreeComplete(BTNode* root); // 判断是否为完全二叉树
(2)二叉树源文件BinaryTree.c
所有操作均基于递归思想 (二叉树的定义是递归的,递归操作最贴合其结构),层序遍历和判断完全二叉树基于队列实现。
cpp
#include"BinaryTree.h"
#include"Queue.h"
// 前序遍历:根 -> 左 -> 右
void PreOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->val); // 先访问根
PreOrder(root->left); // 再遍历左子树
PreOrder(root->right); // 最后遍历右子树
}
// 中序遍历:左 -> 根 -> 右
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left); // 先遍历左子树
printf("%d ", root->val); // 再访问根
InOrder(root->right); // 最后遍历右子树
}
// 后序遍历:左 -> 右 -> 根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left); // 先遍历左子树
PostOrder(root->right); // 再遍历右子树
printf("%d ", root->val); // 最后访问根
}
// 统计结点总数:递归累加左、右子树结点数 + 根结点
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
// 统计叶子结点数:左、右孩子都为空的结点是叶子
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
// 叶子结点,返回1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
// 非叶子,递归累加左、右子树的叶子数
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
// 统计第k层结点数:k=1时为根结点,否则递归找左、右子树的第k-1层
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL || k <= 0)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
// 查找值为x的结点:先找根,再找左子树,最后找右子树
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
// 找到目标,返回结点指针
if (root->val == x)
{
return root;
}
// 左子树查找,找到则返回
BTNode* ret = BinaryTreeFind(root->left, x);
if (ret)
{
return ret;
}
// 左子树没找到,找右子树
return BinaryTreeFind(root->right, x);
}
// 层序遍历:自上而下、自左至右逐层访问,借助队列实现
void LevelOrder(BTNode* root)
{
Queue Q1;
QueueInit(&Q1);
// 根结点入队
if (root != NULL)
{
QueuePush(&Q1, root);
}
// 队列非空则循环
while (!QueueEmpty(&Q1))
{
BTNode* tree = QueueFront(&Q1); // 取队头结点
QueuePop(&Q1); // 队头出队
printf("%d ", tree->val); // 访问结点
// 左孩子存在则入队
if (tree->left)
QueuePush(&Q1, tree->left);
// 右孩子存在则入队
if (tree->right)
QueuePush(&Q1, tree->right);
}
QueueDestroy(&Q1); // 销毁队列,避免内存泄漏
}
// 销毁二叉树:后序遍历销毁(先销毁孩子,再销毁根,避免野指针)
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root); // 释放当前结点
}
// 判断是否为完全二叉树:层序遍历,遇到NULL后若还有非NULL结点,则不是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue Q1;
QueueInit(&Q1);
if (root != NULL)
{
QueuePush(&Q1, root);
}
while (!QueueEmpty(&Q1))
{
BTNode* tree = QueueFront(&Q1);
QueuePop(&Q1);
// 遇到NULL,停止入队,检查后续是否有非NULL结点
if (tree == NULL)
{
while (!QueueEmpty(&Q1))
{
BTNode* tmp = QueueFront(&Q1);
QueuePop(&Q1);
// 存在非NULL结点,不是完全二叉树
if (tmp != NULL)
{
QueueDestroy(&Q1);
return false;
}
}
break;
}
// 不管孩子是否为NULL,都入队
QueuePush(&Q1, tree->left);
QueuePush(&Q1, tree->right);
}
QueueDestroy(&Q1);
return true;
}
3.4 测试代码:创建二叉树并验证操作
为了快速验证上述操作,我们手动创建一棵简单的二叉树,测试所有核心功能:
cpp
#include"BinaryTree.h"
// 创建二叉树结点
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->val = x;
node->left = node->right = NULL;
return node;
}
// 手动创建一棵二叉树
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
int main()
{
// 创建二叉树
BTNode* root = CreatBinaryTree();
printf("前序遍历:");
PreOrder(root); // 1 2 3 4 5 6
printf("\n中序遍历:");
InOrder(root); // 3 2 1 5 4 6
printf("\n后序遍历:");
PostOrder(root);// 3 2 5 6 4 1
printf("\n层序遍历:");
LevelOrder(root);// 1 2 4 3 5 6
printf("\n结点总数:%d\n", BinaryTreeSize(root)); // 6
printf("叶子结点数:%d\n", BinaryTreeLeafSize(root)); // 3(3、5、6)
printf("第3层结点数:%d\n", BinaryTreeLevelKSize(root, 3)); // 3(3、5、6)
// 查找值为5的结点
BTNode* findNode = BinaryTreeFind(root, 5);
if (findNode)
{
printf("找到值为5的结点,地址:%p\n", findNode);
}
// 判断是否为完全二叉树
if (BinaryTreeComplete(root))
{
printf("该二叉树是完全二叉树\n");
}
else
{
printf("该二叉树不是完全二叉树\n");
}
// 销毁二叉树
BinaryTreeDestory(root);
root = NULL; // 避免野指针
return 0;
}
运行结果:
cpp
前序遍历:1 2 3 4 5 6
中序遍历:3 2 1 5 4 6
后序遍历:3 2 5 6 4 1
层序遍历:1 2 4 3 5 6
结点总数:6
叶子结点数:3
第3层结点数:3
找到值为5的结点,地址:000002207856A2E0
该二叉树是完全二叉树
四、二叉树学习的核心技巧与理解
4.1 递归是二叉树的灵魂
二叉树的定义是递归的,因此绝大多数二叉树操作都可以用递归实现,递归的核心思路是:
- 把整棵树的问题 分解为根结点 + 左子树 + 右子树的子问题;
- 递归的终止条件 是遇到空结点 (
root == NULL); - 比如统计结点总数:整棵树的结点数 = 左子树结点数 + 右子树结点数 + 1(根结点)。
递归误区 :不要试图手动模拟递归的每一步(会绕晕),只需关注当前层的逻辑 和终止条件。
4.2 遍历是二叉树操作的基础
二叉树的遍历是所有操作的前提,无论是结点统计、查找还是销毁,本质上都是对遍历的应用:
- 前序 / 中序 / 后序遍历:基于深度优先搜索(DFS),核心是 "先深后广";
- 层序遍历:基于广度优先搜索(BFS),核心是 "先广后深",需要借助队列实现;
- 不同的遍历顺序决定了访问结点的时机,比如后序遍历适合销毁(先销毁孩子,再销毁根)。
4.3 完全二叉树的判断技巧
判断完全二叉树的核心思路是层序遍历 + NULL 检测:
- 层序遍历二叉树,所有结点(包括 NULL)都入队;
- 当遇到第一个 NULL 时,停止入队,开始检查队列剩余元素;
- 若剩余元素全为 NULL,则是完全二叉树;若存在非 NULL 结点,则不是。
这种方法的依据是:完全二叉树的空结点只会出现在层序遍历的最后。
4.4 内存泄漏的注意事项
二叉树是动态分配的内存(malloc),使用完毕后必须手动销毁,销毁时注意:
- 采用后序遍历的方式销毁:先销毁左、右子树,再销毁根结点;
- 若先销毁根结点,会导致左、右子树的指针变为野指针,无法访问和销毁;
- 队列使用完毕后也需要销毁,避免内存泄漏。
五、总结与学习建议
二叉树是数据结构的分水岭,学好二叉树能为后续学习树、红黑树、B 树、堆等结构打下坚实基础,总结核心要点:
- 掌握二叉树的定义和核心性质 ,尤其是
n0 = n2 + 1和完全二叉树的编号规则; - 熟练实现二叉树的四种遍历(前、中、后、层序),理解递归和队列的应用;
- 掌握二叉树的基本统计操作(结点数、叶子数、第 k 层结点数),核心是递归分解问题;
- 理解完全二叉树的判断 和二叉树的销毁,注意内存管理。
创作不易,如果这篇文章对你有帮助,欢迎点赞、收藏、关注!后续会持续更新数据结构与算法的干货内容,一起学习,一起进步~