跳到主要内容
数据结构:链式二叉树的递归实现与遍历分析 | 极客日志
C 算法
数据结构:链式二叉树的递归实现与遍历分析 综述由AI生成 链式二叉树利用链表表示树形结构,节点包含数据域及左右指针,具有自相似递归特性。文章详细阐述了前序、中序、后序及层序遍历的实现原理与代码,涵盖构造二叉树、统计节点数(含错误方法对比)、计算叶子节点数、第 k 层节点数、树的高度深度、查找指定节点以及判断完全二叉树等功能接口。通过递归思维将复杂问题分解为子树处理,是掌握数据结构与算法的关键基础。
数字游民 发布于 2026/3/30 更新于 2026/6/3 21 浏览数据结构:链式二叉树的递归实现与遍历分析
引言
递归是程序员的必修课,而链式二叉树正是理解递归的绝佳范例。在这篇文章中,你将通过实现二叉树的遍历、统计节点、计算深度等操作,真正掌握递归思维。
一、介绍链式二叉树
1.1 概念
链式结构:是指用链表来表示一颗二叉树,即用链表来指示元素的逻辑关系。通常链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别指向该节点左孩子和右孩子所在的地址。
1.2 基本结构(结构上的递归性)
既然链式二叉树是用链表来表示,那么二叉树就是由一个个的节点构成,与前面的队列的实现相同,链式二叉树的结构就是一个节点的结构。
那么,链式二叉树的节点又是由什么构成?根据链式二叉树的概念,节点由三部分构成:存储数据的变量、指向左孩子节点的指针、指向右孩子节点的指针。
typedef int BTDataType;
typedef struct BinaryTreeNode {
int data;
struct BinaryTreeNode * left ;
struct BinaryTreeNode * right ;
} BTNode;
通过这样节点的结构,将所有节点链接在一起形成一棵树。观察图示、节点结构,递归结构就此显现:
一个二叉树由三部分组成根节点;左子树,它本身也是一个二叉树;右子树,它本身也是一个二叉树。
这样的结构定义是自相似的,那么再来看递归的定义:'递归,就是将问题逐步分解为与原始问题相似的更小规模的子问题,来解决原始问题;直到转化为最简单的子问题,递归调用结束'。
这也就进一步证实了链式二叉树的结构是递归性质的。
二、遍历接口实现(操作上的递归性)
链式二叉树的结构是递归的,那么对树进行的大多数操作,其算法必然是递归的。处理整个二叉树树和处理它的子树,结构都是相似的,导致使用的是完全相同的方法。
前序遍历 先访问当前节点,然后递归遍历左子树,最后递归遍历右子树 根 -> 左 -> 右 中序遍历 先递归遍历左子树,然后访问当前节点,最后递归遍历右子树 左 -> 根 -> 右 后序遍历 先递归遍历左子树,然后递归遍历右子树,最后访问当前节点 左 -> 右 -> 根 层序遍历 按树的深度,从根节点开始,一层一层地依次访问节点 从上到下,从左到右
2.1 前序遍历(根左右)
1. 概念 前序遍历,又称为先根遍历,其遍历规则为:先遍历根节点,再遍历左子树,最后遍历右子树。简记为'根左右'。
根据图解:首先铭记根左右规则。
从根节点 A 出发,打印 A ——> 进入左子树,打印 B,现在以 B 为节点(根左右) ——> 进入左子树,打印 D,现在以 D 为根节点(根左右)——> 进入左子树,打印 NULL,没有子树 ——>
开始递归返回 D,进入右子树,打印 NULL,没有子树 ——> 递归返回 D,左子树遍历完成 ——> 递归返回 B,进入右子树,打印 NULL,没有子树 ——> 递归返回 B,左子树遍历完成 ——>
递归返回 A,进入右子树,打印 C,现在以 C 为根节点(根左右) ——> 进入左子树,打印 E,现在以 E 为根节点(根左右) ——> 进入左子树,打印 NULL,没有子树 ——> 递归返回 E,进入右子树,打印 NULL,没有子树 ——> 递归返回 E,左子树遍历完成 ——>
递归返回 C,进入右子树,打印 F,现在以 F 为根节点(根左右) ——> 进入左子树,打印 NULL,没有字数 ——> 递归返回 F,进入右子树,打印 NULL,没有子树 ——>
递归返回 F,右子树遍历完成 ——> 递归返回 C,右子树遍历完成 ——> 递归返回 A,遍历结束。
最终遍历结果:A——>B——>D——>NULL——>NULL——>NULL——>C——>E——>NULL——>NULL——>F——>NULL——>NULL
2. 代码
#include "Tree.h"
void PreOrder (BTNode* root) {
if (root == NULL ) {
printf ("NULL " );
return ;
}
printf ("%c " , root->data);
PreOrder(root->left);
PreOrder(root->right);
}
思路:(对一个简单 2 层的二叉树)
首先看整个二叉树的根节点是否为空,为空直接输出 NULL,结束;不为空,也是先打印根节点数据。
遍历完根节点,对左子树进行前序遍历,在函数内再次调用自己,将根节点的左孩子指针传参,进行递归调用。
前序遍历完左子树,开始对右子树进行前序遍历,再次调用自己(与左子树遍历并列),将根节点的右孩子指针传参,递归调用。
#include "Tree.h"
void test01 () {
BTNode* nodeA = buyNode('A' );
BTNode* nodeB = buyNode('B' );
BTNode* nodeC = buyNode('C' );
BTNode* nodeD = buyNode('D' );
BTNode* nodeE = buyNode('E' );
BTNode* nodeF = buyNode('F' );
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
PreOrder(nodeA);
}
int main () {
test01();
return 0 ;
}
2.2 中序遍历(左根右)
1. 概念 中序遍历,指的是跟根节点在中间打印,先遍历左子树,再遍历根节点,最后遍历右子树。----> 简记为'左根右'。
根据图解:首先铭记左根右规则。
从根节点 A 出发,进入左子树,现在以 B 为根节点—>进入左子树,现在以 D 为根节点—>进入左子树,打印 NULL,没有子树—>
开始递归返回 D,打印 D—>进入右子树,打印 NULL,没有子树—>递归返回 D,左子树遍历完成—>递归返回 B,进入右子树,打印 NULL,没有子树——>递归返回 B,左子树遍历完成—>
遍历返回 A,打印 A,进入右子树,现在以 C 为根节点—>进入左子树,现在以 E 为根节点—>进入左子树,打印 NULL,没有子树—>递归返回 E,打印 E—>进入右子树,打印 NULL,没有子树—>递归返回 E,左子树遍历完成—>
遍历返回 C,打印 C—>进入右子树,现在以 F 为根节点—>进入左子树,打印 NULL,没有子树—>递归返回 F,打印 F—>进入右子树,打印 NULL,没有子树—>递归返回 F,右子树遍历完成—>
递归返回 C,右子树遍历完成—>递归返回 A,结束。
最终遍历结果为:NULL——>D——>NULL——>B——>NULL——>A——>NULL——>E——>NULL——>C——>NULL——>F——>NULL
2. 代码
#include "Tree.h"
void InOrder (BTNode* root) {
if (root == NULL ) {
printf ("NULL " );
return ;
}
InOrder(root->left);
printf ("%c " , root->data);
InOrder(root->right);
}
void test01 () {
BTNode* nodeA = buyNode('A' );
BTNode* nodeB = buyNode('B' );
BTNode* nodeC = buyNode('C' );
BTNode* nodeD = buyNode('D' );
BTNode* nodeE = buyNode('E' );
BTNode* nodeF = buyNode('F' );
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
InOrder(nodeA);
}
int main () {
test01();
return 0 ;
}
思路:(对一个简单的二叉树)
首先也是先看整个二叉树的根节点是否为空,为空直接输出 NULL,结束;
不为空,对左子树进行中序遍历,再次调用自己,将根节点的左孩子指针传参,递归调用。
遍历完左子树,打印根节点数据。
最后对右子树进行中序遍历,也是在函数内再次调用自己,将根节点的右孩子指针传参,进行递归调用。
2.3 后序遍历
1. 概念 后序遍历,指的是跟根节点在最后打印,先遍历左子树,再遍历右子树,最后遍历根节点。----> 简记为'左右根'。
根据图解:首先铭记左右根规则。
从根节点 A 出发,进入左子树,现在以 B 为根节点——>进入左子树,现在以 D 为根节点——>进入左子树,打印 NULL,没有子树——>
开始递归返回 D,进入右子树,打印 NULL,没有子树——>递归返回 D,打印 D——>递归返回 B,进入右子树,打印 NULL,没有子树——>递归返回 B,打印 B——>
递归返回 A,进入右子树,现在以 C 为根节点——>进入左子树,现在以 E 为根节点——>进入左子树,打印 NULL,没有子树——>递归返回 E,进入右子树,打印 NULL,没有子树——>递归返回 E,打印 E——>递归返回 C,进入右子树,现在以 F 为根节点——>进入左子树,打印 NULL,没有子树——>递归返回 F,进入右子树,打印 NULL,没有子树——>
递归返回 F,打印 F——>递归返回 C,打印 C——>递归返回 A,打印 A,结束。
最终遍历结果为:NULL——>NULL——>D——>NULL——>B——>NULL——>NULL——>E——>NULL——>NULL——>F——>C——>A
2. 代码
void PostOrder (BTNode* root) {
if (root == NULL ) {
printf ("NULL " );
return ;
}
PostOrder(root->left);
PostOrder(root->right);
printf ("%c " , root->data);
}
#include "Tree.h"
void test01 () {
BTNode* nodeA = buyNode('A' );
BTNode* nodeB = buyNode('B' );
BTNode* nodeC = buyNode('C' );
BTNode* nodeD = buyNode('D' );
BTNode* nodeE = buyNode('E' );
BTNode* nodeF = buyNode('F' );
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
PostOrder(nodeA);
}
int main () {
test01();
return 0 ;
}
思路:(对一个简单 2 层的二叉树)
首先也是先看整个二叉树的根节点是否为空,为空,代表树为空,直接输出 NULL,结束;
不为空,对左子树进行中序遍历,在函数内再次调用自己,将根节点的左孩子指针传参,进行递归调用。
遍历完左子树,再次递归调用函数,将根节点的右孩子指针传参。
最后打印根节点数据。
三、其余接口实现(操作上的递归性)
3.1 构造二叉树
1. 概念 像图片这样,以所示二叉树为例:我们需要申请节点空间存储二叉树中所有有效数据节点,这就需要去动态开辟空间;其次,根据节点之间的父子关系通过指针链接彼此;
2. 代码
#include "Tree.h"
BTNode* buyNode (char x) {
BTNode* newnode = (BTNode*)malloc (sizeof (BTNode));
newnode->data = x;
newnode->left = newnode->right = NULL ;
return newnode;
}
BTNode* createTree () {
BTNode* nodeA = buyNode('A' );
BTNode* nodeB = buyNode('B' );
BTNode* nodeC = buyNode('C' );
BTNode* nodeD = buyNode('D' );
BTNode* nodeE = buyNode('E' );
BTNode* nodeF = buyNode('F' );
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeB->right = nodeE;
nodeC->left = nodeF;
return nodeA;
}
3.2 树的有效节点个数
1. 概念 遍历二叉树时,只要节点不为空,个数就 +1,为空则返回。
树的有效节点个数 = 左子树有效节点个数 + 右子树有效节点个数,根据这个思路就可以将求整体树的节点划分成求一个一个的子树节点在加和,这就是递归思想。
2. 代码
2.1 正确方法
#include "Tree.h"
int BinaryTreeSize (BTNode* root) {
if (root == NULL ) {
return 0 ;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
printf ("size:%d\n" , BinaryTreeSize(root));
printf ("size:%d\n" , BinaryTreeSize(root));
}
int main () {
test01();
return 0 ;
}
2.2 错误方法:创建全局变量/静态修饰
思路:(对一个简单 2 层的二叉树)
首先创建变量 size 统计节点个数,全局变量。
然后先判断根节点是否为空,为空代表没有有效节点,直接返回 0;不为空,那么根节点算一个有效节点,size++。
最后,递归调用函数遍历左子树、右子树。
int size = 0 ;
int BinaryTreeSize (BTNode* root) {
if (root == NULL ) {
return 0 ;
}
size++;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
return size;
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
printf ("size:%d\n" , BinaryTreeSize(root));
printf ("size:%d\n" , BinaryTreeSize(root));
}
int main () {
test01();
return 0 ;
}
错误原因:创建局部变量,每次调用函数,变量都被初始化,无法统计。
值得注意的是——>全局变量只被初始化一次,此后对变量的修改都会继承;多次调用函数,变量会继承在上次调用后变量的值。
–那么对变量进行 static 修饰呢? --> 也不行!
2.3 低效率方法:传参
#include "Tree.h"
/树的有效节点个数_方法 2
int BinaryTreeSize (BTNode* root, int * psize) {
if (root == NULL ) {
return 0 ;
}
(*psize)++;
BinaryTreeSize(root->left, psize);
BinaryTreeSize(root->right, psize);
}
void test01 () {
BTNode* root = createTree();
int TreeSize = 0 ;
BinaryTreeSize(root, &TreeSize);
printf ("size:%d\n" , TreeSize);
TreeSize = 0 ;
BinaryTreeSize(root, &TreeSize);
printf ("size:%d\n" , TreeSize);
}
int main () {
test01();
return 0 ;
}
解析:
将 size 每次当作参数传给函数,这样避免了变量累加的情况。但是这样接改变了函数的形式,破坏了接口一致性,使用起来不方便。并且在测试时,每次使用前都要手动将变量置零。
3.3 二叉树叶子节点个数 某节点的左、右孩子节点均为 NULL 时,就叫叶子节点。求二叉树总叶子节点数,就将左右子树的叶子节点加和,这就将大问题分成一个个小问题。
——>总的叶子节点个数 = 左子树叶子节点树 + 右子树叶子节点数。
2. 代码
#include "Tree.h"
int BinaryTreeLeafSize (BTNode* root) {
if (root == NULL ) {
return 0 ;
}
if (root->left == NULL && root->right == NULL ) {
return 1 ;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
printf ("leaf size:%d\n" , BinaryTreeLeafSize(root));
}
int main () {
test01();
return 0 ;
}
3.4 求第 k 层节点的个数
1. 概念 根据参数 k,遍历 1 层就–,到 k=1 时代表已经到了第 k 层。第 k 层节点个数 = 左子树第 k 层节点个数 + 右子树第 k 层节点个数。
2. 代码
思路:(对一个简单 2 层的二叉树)
首先判断根节点是否为空,为空直接返回结束;否则判断是否为 k 层,是 k 层则返回 1,不是就递推调用函数遍历左右孩子节点(左右子树)。
#include "Tree.h"
int BinaryTreeLevelKSize (BTNode* root, int k) {
if (root == NULL ) {
return 0 ;
}
if (k == 1 ) {
return 1 ;
}
return BinaryTreeLevelKSize(root->left, k - 1 ) + BinaryTreeLevelKSize(root->right, k - 1 );
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
int k = 0 ;
printf ("Input level:" );
scanf ("%d" , &k);
printf ("k level size:%d\n" , BinaryTreeLevelKSize(root, k));
}
int main () {
test01();
return 0 ;
}
3.5 求树的高度/深度
1. 概念 树的深度为有效节点的最大层次。总的高度 = 1(根节点独占 1 层) + (左右子树中高度最大的)。
2. 代码
思路:
判断根节点;提前保存左、右子树计算出的深度——>方便返回时使用三目操作符,不然需要递归四次,比较繁琐。整体来说,还是比较简单的。
#include "Tree.h"
int BinaryTreeDepth (BTNode* root) {
if (root == NULL ) {
return 0 ;
}
int leftDeptt = BinaryTreeDepth(root->left);
int rightDeptt = BinaryTreeDepth(root->right);
return 1 + (leftDeptt > rightDeptt ? leftDeptt : rightDeptt);
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
printf ("Tree Depth:%d\n" , BinaryTreeDepth(root));
}
int main () {
test01();
return 0 ;
}
3.6 查找指定数据的节点
思路:
判断根节点;然后对节点存储的数据进行匹配,符合就返回该节点,反之对左子树、右子树进行遍历匹配。要注意的是,一旦左子树匹配成功,无需再对右子树进行遍历。
#include "Tree.h"
BTNode* BinaryTreeFind (BTNode* root, BTDataType x) {
if (root == NULL ) {
return NULL ;
}
if (root->data == x) {
return root;
}
BTNode* leftFind = BinaryTreeFind(root->left, x);
if (leftFind) {
return leftFind;
}
BTNode* rightFind = BinaryTreeFind(root->right, x);
if (rightFind) {
return rightFind;
}
return NULL ;
}
#include "Tree.h"
void test01 () {
BTNode* root = createTree();
if (BinaryTreeFind(root, 'G' )) {
printf ("找到了!" );
} else
printf ("不存在!" );
}
int main () {
test01();
return 0 ;
}
3.7 层序遍历 遍历分类 分类方式 前、中、后序遍历 深度优先 层序遍历 广度优先
思路:借队列–>根节点数据入队列,循环判断队列是否为空,不为空就取队头节点,然会将节点的左右孩子入队列。(循环进行)
要用到队列结构,那么先将前面实现的头文件、源文件添加到项目中,并包含上头文件。
在队列的头文件中,将存储数据的变量类型命名 int 改为为二叉树节点类型,事先声明。
typedef struct BinartNode* QDataType;
void LevelOrder (BTNode* root) {
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q)) {
BTNode* top = QueueFront(&q);
printf ("%c " , top->data);
QueuePop(&q);
if (top->left)
QueuePush(&q, top->left);
if (top->right)
QueuePush(&q, top->right);
}
QueueDestroy(&q);
}
3.8 判断是否为完全二叉树 –完全二叉树:除最后一层以外其它层节点个数全部达到最大,且节点全是从左到右依次排列。
思路:
先将树的根节点入队,使队不为空。
然后循环的进行取队头、出队头、将头节点的子节点入队(含空),直到取出的节点为空,跳出。
此时,对队列剩余的内容再进行取队头、出队头,判断是否有不为空的节点。
bool BinaryTreeComplete (BTNode* root) {
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q)) {
BTNode* top = QueueFront(&q);
QueuePop(&q);
if (top == NULL ) {
break ;
}
QueuePush(&q, top->left);
QueuePush(&q, top->right);
}
while (!QueueEmpty(&q)) {
BTNode* top = QueueFront(&q);
QueuePop(&q);
if (top != NULL ) {
QueueDestroy(&q);
return false ;
}
}
QueueDestroy(&q);
return true ;
}
总结 链式二叉树是递归思维的完美体现。在这棵自我相似的树中,每个节点都遵循相同的结构规则。通过前序、中序、后序三种递归遍历,我们能用简洁代码探索整棵树;通过节点统计、深度计算等操作,我们学会将复杂问题分解为相似子问题。掌握链式二叉树,不仅是学习数据结构,更是培养递归思维的关键一步,为理解更复杂的树形结构奠定基础。
相关免费在线工具 加密/解密文本 使用加密算法(如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