跳到主要内容数据结构初阶:链式二叉树的实现与遍历 | 极客日志C算法
数据结构初阶:链式二叉树的实现与遍历
介绍数据结构中的链式二叉树。涵盖树的定义、术语、性质,以及二叉树的形态、特殊类型(满二叉树、完全二叉树等)和遍历方式(前序、中序、后序、层序)。重点讲解使用二叉链表实现二叉树的方法,包括节点结构定义、递归遍历接口、查找、销毁及判断完全二叉树等功能的 C 语言代码实现。
云朵棉花糖25 浏览 数据结构初阶:链式二叉树
前言
本文继续探索新的数据结构——二叉树。通过本文学习,你将掌握二叉树的定义、性质、遍历方式以及使用 C 语言进行链式实现的完整代码。
树
什么是树?
树(Tree):是一种非线性的数据结构,它是由 n(n≥0)个有限节点组成一个具有层次关系的集合。
树的递归定义:树是由一个根节点和若干棵子树构成的。
一棵树中至少有一个节点,这个节点就是根节点。
当 n > 0 时,其余节点可分为 m(m≥0)个互不相交的有限集合 T1、T2、…、Tm,其中每个集合本身又是一棵树,并且称为根的子树。
例如:在一个表示公司组织架构的树中,公司的最高领导是根节点,其下的各个部门负责人是根节点的子树的根,每个部门又可以有自己的下属员工,形成更下一层的子树。
总结:从上面的定义中,我们可以知道树这种数据结构天然的和递归有关联。
树的基本术语有哪些?
关于节点的一些定义
- 节点:树中的每个元素称为节点。
- 根节点:树中没有父节点的节点,也是树的起始点。
- 子节点与父节点:一个节点的下一层连接的节点称为其子节点,该节点则是其子节点的父节点。
- 兄弟节点:具有相同父节点的节点互为兄弟节点。
- 堂兄弟节点:父节点互为兄弟节点的节点互为堂兄弟节点。
- 叶子节点:没有子节点的节点,也称为终端节点。
- 内部节点:除叶子节点外的其他节点,即至少有一个子节点的节点。
- 节点的祖先:从根节点到该节点所经路径上的所有节点。
- 节点的子孙:以某节点为根的子树中的所有节点。
关于树的一些定义
- 节点的度:一个节点拥有的子节点数量。
- 树的度:树中节点度的最大值。
- 路径:从一个节点到另一个节点所经过的节点序列。
- 路径长度:路径上的边的数量。
- 树的深度(高度):树中节点的最大层数。
- 层:节点的层次,根节点在第 1 层,其子节点在第 2 层,以此类推。
关于森林的定义
树的实现方式有哪些?
树结构相对线性表比较复杂,要存储表示起来比较麻烦,既要保存值域,也要保存结点和结点之间的关系。
实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int TDataType;
typedef struct {
TDataType data;
} TNode;
TreeNode
struct TreeNode* pfirstChild;
struct TreeNode* pNextBrother;
二叉树
什么是二叉树?
二叉树(Binary Tree):是一种特殊的树形结构,其特点是每个节点最多有两个子节点,分别称为左子节点和右子节点。
二叉树的每个节点最多有两个子节点,并且子节点有左右之分,顺序不能颠倒。
二叉树是一个有限节点的集合,该集合可以为空,或由一个根节点和两棵互不相交的子树组成,这两棵子树分别称为左子树和右子树。
二叉树与普通树的本质区别是什么?
| 特征 | 二叉树 | 普通树 |
|---|
| 最大分支数 | 每个节点≤2 个子节点 | 任意数量子节点 |
| 子节点顺序 | 严格区分左/右 | 通常无序 |
| 空树定义 | 允许空树(NULL) | 至少包含根节点 |
- 每个节点至多有两个子节点:即节点的度最大为 2。
- 子节点有明确的左右之分:左子树和右子树是不同的,即使只有一个子节点也必须区分左右。
- 递归结构:二叉树的左右子树本身也都是二叉树。
二叉树的五大基本形态是什么?
- 空树:没有任何节点。
- 只有根节点:根节点的左右子树均为空。
- 根节点只有左子树:右子树为空。
- 根节点只有右子树:左子树为空。
- 根节点同时有左右子树:左右子树均非空。
注意:对于任意的二叉树都是由上面的这五种情况复合而成的。
特殊的二叉树有哪些?
- 满二叉树(Full Binary Tree):所有层的节点数都达到最大值,则这个二叉树就是满二叉树。
- 完全二叉树(Complete Binary Tree):除了最后一层外,每一层的节点数都达到最大值,且最后一层的节点从左到右连续排列。
- 二叉搜索树(Binary Search Tree, BST):左子树的所有节点值小于根节点,右子树的所有节点值大于根节点。
- 平衡二叉树(Balanced Binary Tree):每个节点的左右子树高度差不超过 1(如:AVL 树、红黑树、B 树)。
注:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。所以:满二叉树是特殊的完全二叉树。
树和二叉树的性质有哪些?
- 连通性:树是一个连通的无向图,任意两个节点之间都有且仅有一条路径相连。
- 无环性:树中不存在回路或环,从根节点到任何其他节点都有唯一的路径。
- 层次关系:树具有明显的层次结构。
- 子树性质:树中的每个节点都可以看作是一棵子树的根。
- 节点与边的关系:对于一棵有 n 个节点的树,其边的数量为 n - 1。
- 节点和度的关系:总结点数 = 所有节点度数之和 + 1。
- 二叉树遍历的性质:已知前序 + 中序,或后序 + 中序可以唯一确定一棵二叉树,但前序 + 后序不能唯一确定二叉树(除非是满二叉树)。
- 层节点数与层数的关系:在满二叉树中,第 k 层的节点数为 2^(k-1)。扩展到二叉树:一棵非空二叉树的第 k 层上最多有 2^(k-1) 个结点。
- 总节点数与高度的关系:
- 在满二叉树中,高度为 h 的满二叉树的总节点数为 2^h - 1。
- 扩展到二叉树:一棵高度为 h 的二叉树最多有 2^h - 1 个结点。
- 在满二叉树中,具有 n 个结点的满二叉树的高度为 log2(n + 1)。
- 完全二叉树顺序特性:
- 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
- 双亲结点(父结点):若 i = 0,该结点为根结点,无双亲结点;若 i > 0,结点 i 的双亲结点序号为 (i-1)/2(向下取整)。
- 左孩子结点:若 2i + 1 >= n,则该结点无左孩子;若 2i + 1 < n,则左孩子序号为 2i + 1。
- 右孩子结点:若 2i + 2 >= n,则该结点无右孩子;若 2i + 2 < n,则右孩子序号为 2i + 2。
- 总节点数与高度的关系:具有 n 个节点的完全二叉树的高度为 floor(log2(n)) + 1。
对于任意非空二叉树:叶节点数 = 度为 2 的节点数 + 1。
树和二叉树的区别是什么?
| 特性 | 普通树 | 二叉树 |
|---|
| 节点最大度数 | 无限制 | 最多 2 |
| 子树顺序 | 一般不区分 | 严格区分左右 |
| 空树 | 有些定义不允许 | 明确允许 |
| 存储结构 | 通常使用孩子兄弟表示法 | 常用左右指针法(二叉链) |
| 遍历方式 | 无中序遍历 | 有中序遍历 |
二叉树怎么进行遍历?
二叉树的遍历:二叉树的遍历是指按照一定的规则和顺序,访问二叉树中的每个节点,且每个节点仅被访问一次。
根据节点访问顺序的不同,二叉树的遍历主要分为以下四类:
| 遍历方式 | 访问顺序 | 典型应用场景 |
|---|
| 前序遍历 | 根 → 左子树 → 右子树 | 复制树结构、前缀表达式(波兰式) |
| 中序遍历 | 左子树 → 根 → 右子树 | 二叉搜索树的有序输出(升序/降序) |
| 后序遍历 | 左子树 → 右子树 → 根 | 删除树结构、后缀表达式(逆波兰式) |
| 层序遍历 | 按层次从上到下、从左到右 | 计算树的高度、广度优先搜索(BFS) |
- 前序:1 2 3 N N N 4 5 N 6 N N 6 N N
- 中序:N 3 N 2 N 1 N 5 N 6 N 4 N 6 N
- 后序:N N 3 N 2 N N 6 5 N N 6 4 1
二叉树的实现方式有哪些?我们要选择哪种方式进行实现?
和之前我们学习的其他的数据结构一样,二叉树常见实现方式有顺序存储和链式存储。
那我们就先来看看树的顺序存储:
二叉树的顺序存储结构是用一组连续的存储单元依次自上而下、自左至右存储二叉树的节点元素。
其中普通的二叉树是不适合使用顺序存储的,也就是说不适合使用数组来存储二叉树的节点,这是因为对于一般二叉树,需要按照完全二叉树的形式来存储,可能会浪费一些存储空间。
而像完全二叉树就比较适合使用顺序结构存储。
其实我们通常会把'使用动态数组实现的完全二叉树'这种结构称为堆。
注意:这里的堆和内存四区中的堆是两回事,一个是一种数据结构,一个是操作系统中管理内存的一块区域分段。
typedef int HPDataType;
typedef struct Heap {
int capacity;
int size;
HPDataType* a;
} HP;
预告:由于堆这种数据结构有很多可讲的点,这篇博客中我们只是简单的了解一下,下篇博客再详细的介绍它。
接下来我们再看一看树的链式存储:
二叉树的链式存储结构,是借助链表来呈现二叉树形态,以链表的链接关系来体现节点间逻辑关联。
一般而言,链表中的每个节点包含三个域:数据域用于存储节点数据,左指针域指向该节点左孩子所在的链表节点,右指针域则指向右孩子所在的链表节点。
链式结构又分为:二叉链和三叉链,在数据结构初阶阶段,我们先学习使用二叉链来实现二叉树。
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType data;
struct BinTreeNode* parent;
struct BinTreeNode* left;
struct BinTreeNode* right;
} BTNode;
综上所述:顺序存储(数组):用数组按层次顺序存储节点,适合完全二叉树。
链式存储(链表):用节点(含数据域与左右指针域)以链式结构相连存储节点,适配任意二叉树。
使用二叉链实现二叉树
头文件
------------------------------BinaryTree.h------------------------------
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "Queue.h"
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
BTNode* BTCreatNode(int x);
void PrevOrder(BTNode* root);
void InOrder(BTNode* root);
void PostOrder(BTNode* root);
int BTSize(BTNode* root);
int BTLeafSize(BTNode* root);
int BTHeight(BTNode* root);
int BTLevelKSize(BTNode* root, int k);
BTNode* BTFind(BTNode* root, int x);
void BTDestroy(BTNode* root);
void LevelOrder(BTNode* root);
bool BTComplete(BTNode* root);
--------------------------------Queue.h--------------------------------
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct BinaryTreeNode BTNode;
typedef struct BinaryTreeNode* QDateType;
typedef struct QueueNode {
QDateType val;
struct QueueNode* next;
} QNode;
typedef struct Queue {
int size;
QNode* queHead;
QNode* queTail;
} Que;
void QueInit(Que* pque);
void QueDestroy(Que* pque);
void QuePush(Que* pque, QDateType x);
void QuePop(Que* pque);
QDateType QueFront(Que* pque);
QDateType QueBack(Que* pque);
bool QueEmpty(Que* pque);
int QueSize(Que* pque);
实现文件
------------------------------BinaryTree.c------------------------------
#define _CRT_SECURE_NO_WARNINGS 1
#include "BinaryTree.h"
BTNode* BTCreatNode(int x) {
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL) {
perror("malloc fail");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
void PrevOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
int BTSize(BTNode* root) {
if (root == NULL)
return 0;
else
return BTSize(root->left) + BTSize(root->right) + 1;
}
int BTLeafSize(BTNode* root) {
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
else
return BTLeafSize(root->left) + BTLeafSize(root->right);
}
int BTHeight(BTNode* root) {
if (root == NULL)
return 0;
int leftHeight = BTHeight(root->left);
int rightHeight = BTHeight(root->right);
return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}
int BTLevelKSize(BTNode* root, int k) {
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1);
}
BTNode* BTFind(BTNode* root, int x) {
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret = BTFind(root->left, x);
if (ret)
return ret;
ret = BTFind(root->right, x);
if (ret)
return ret;
return NULL;
}
void BTDestroy(BTNode* root) {
if (root == NULL)
return;
BTDestroy(root->left);
BTDestroy(root->right);
free(root);
}
void LevelOrder(BTNode* root) {
Que que;
QueInit(&que);
if (root) {
QuePush(&que, root);
}
while (!QueEmpty(&que)) {
BTNode* front = QueFront(&que);
QuePop(&que);
printf("%d ", front->data);
if (front->left)
QuePush(&que, front->left);
if (front->right)
QuePush(&que, front->right);
}
QueDestroy(&que);
}
bool BTComplete(BTNode* root) {
Que que;
QueInit(&que);
if (root)
QuePush(&que, root);
while (!QueEmpty(&que)) {
BTNode* front = QueFront(&que);
QuePop(&que);
if (front == NULL)
break;
QuePush(&que, front->left);
QuePush(&que, front->right);
}
while (!QueEmpty(&que)) {
BTNode* front = QueFront(&que);
QuePop(&que);
if (front != NULL) {
QueDestroy(&que);
return false;
}
}
QueDestroy(&que);
return true;
}
------------------------------Queue.c------------------------------
#include "Queue.h"
void QueInit(Que* pque) {
assert(pque);
pque->size = 0;
pque->queHead = NULL;
pque->queTail = NULL;
}
void QueDestroy(Que* pque) {
assert(pque);
pque->size = 0;
QNode* tmp = pque->queHead;
while (tmp != NULL) {
QNode* next = tmp->next;
free(tmp);
tmp = next;
}
pque->queHead = NULL;
pque->queTail = NULL;
}
void QuePush(Que* pque, QDateType x) {
assert(pque);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL) {
perror("malloc fail");
return;
}
newNode->val = x;
newNode->next = NULL;
if (pque->queTail == NULL) {
pque->queHead = pque->queTail = newNode;
}
else {
pque->queTail->next = newNode;
pque->queTail = newNode;
}
pque->size++;
}
void QuePop(Que* pque) {
assert(pque);
assert(pque->size > 0);
if (pque->queHead->next == NULL) {
free(pque->queHead);
pque->queHead = NULL;
pque->queTail = NULL;
}
else {
QNode* next = pque->queHead->next;
free(pque->queHead);
pque->queHead = next;
}
pque->size--;
}
QDateType QueFront(Que* pque) {
assert(pque);
assert(pque->size > 0);
return pque->queHead->val;
}
QDateType QueBack(Que* pque) {
assert(pque);
assert(pque->size > 0);
return pque->queTail->val;
}
bool QueEmpty(Que* pque) {
assert(pque);
return pque->size == 0;
}
int QueSize(Que* pque) {
assert(pque);
return pque->size;
}
测试文件
------------------------------Test.c------------------------------
#include "BinaryTree.h"
int main() {
BTNode* node1 = BTCreatNode(1);
BTNode* node2 = BTCreatNode(2);
BTNode* node3 = BTCreatNode(3);
BTNode* node4 = BTCreatNode(4);
BTNode* node5 = BTCreatNode(5);
BTNode* node6 = BTCreatNode(6);
BTNode* node7 = BTCreatNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
node5->right = node7;
printf("前序遍历:");
PrevOrder(node1);
printf("\n");
printf("中序遍历:");
InOrder(node1);
printf("\n");
printf("后序遍历:");
PostOrder(node1);
printf("\n");
printf("节点总数:%d\n", BTSize(node1));
printf("叶子节点数:%d\n", BTLeafSize(node1));
printf("树高度:%d\n", BTHeight(node1));
printf("第 3 层节点数:%d\n", BTLevelKSize(node1, 3));
printf("第 4 层节点数:%d\n", BTLevelKSize(node1, 4));
int target = 6;
BTNode* found = BTFind(node1, target);
if (found)
printf("找到节点%d\n", target);
else
printf("未找到节点%d\n", target);
printf("层序遍历:");
LevelOrder(node1);
printf("\n");
if (BTComplete(node1))
printf("该二叉树是完全二叉树\n");
else
printf("该二叉树不是完全二叉树\n");
BTDestroy(node1);
printf("二叉树已销毁\n");
return 0;
}
运行成功样例
程序编译运行后,控制台将输出遍历结果及树属性统计,验证二叉树功能的正确性。
相关免费在线工具
- 加密/解密文本
使用加密算法(如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