一、什么是树
1.1 树的定义
树是 n(n ≥ 0)个节点的有限集合。当 n = 0 时称为空树。任意非空树满足:
-
有且仅有一个根节点
-
其余节点可分为 m 个互不相交的子树
现实中的例子:文件系统、公司组织架构、网页DOM树。
1.2 树的术语
画一棵树来理解:
text
A ← 根节点
/ | \
B C D ← 子树
/ \ |
E F G ← 叶子节点
| 术语 | 说明 | 示例 |
|---|---|---|
| 根节点 | 树的最顶层节点 | A |
| 父节点 | 直接上层节点 | A是B、C、D的父节点 |
| 子节点 | 直接下层节点 | B、C、D是A的子节点 |
| 兄弟节点 | 同一父节点的节点 | B、C、D互为兄弟 |
| 叶子节点 | 没有子节点的节点 | E、F、G |
| 度 | 节点拥有的子节点个数 | A的度是3,B的度是2 |
| 树的度 | 所有节点度的最大值 | 树的度是3 |
| 深度 | 从根到该节点的层数(根深度为1) | B的深度是2 |
| 高度 | 从该节点到最远叶子的距离 | A的高度是3 |
二、二叉树
2.1 二叉树的定义
二叉树是每个节点最多有两个子树的树结构,子树分为左子树和右子树,左右有序。
c
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
2.2 二叉树的特点
-
每个节点最多有两棵子树
-
左右子树有顺序,不能颠倒
-
二叉树可以是空树
2.3 特殊的二叉树
满二叉树:所有分支节点都有左右子树,且所有叶子节点都在同一层。
text
1
/ \
2 3
/ \ / \
4 5 6 7
完全二叉树:除了最后一层,其他层都是满的,且最后一层的叶子节点都靠左排列。
text
1
/ \
2 3
/ \ /
4 5 6
2.4 二叉树的性质
| 性质 | 内容 |
|---|---|
| 性质1 | 第 i 层最多有 2^(i-1) 个节点 |
| 性质2 | 深度为 k 的二叉树最多有 2^k - 1 个节点 |
| 性质3 | 叶子节点数 = 度为2的节点数 + 1 |
| 性质4 | n 个节点的完全二叉树深度为 ⌊log₂n⌋ + 1 |
性质3的推导:
-
设 n0 为叶子节点数,n1 为度为1的节点数,n2 为度为2的节点数
-
总节点数:n = n0 + n1 + n2
-
总边数:n - 1 = n1 + 2n2
-
相减得:n0 = n2 + 1
三、二叉树的顺序存储
3.1 存储方式
用数组存储二叉树,按完全二叉树的编号规则,将节点放入数组对应位置。
编号规则(根节点编号为1):
-
编号 i 的左孩子:2i
-
编号 i 的右孩子:2i + 1
-
编号 i 的父节点:⌊i/2⌋
text
1(0)
/ \
2(1) 3(2)
/ \ /
4(3) 5(4) 6(5)
数组存储:[0, 1, 2, 3, 4, 5, 6]
下标: 0 1 2 3 4 5 6
3.2 顺序存储的优缺点
优点:
-
随机访问,通过下标快速定位节点
-
适合满二叉树和完全二叉树
缺点:
-
空间浪费:非完全二叉树会留下大量空位
-
插入删除不方便
3.3 代码实现
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int size; // 实际节点个数
} SeqBinaryTree;
// 初始化
void initTree(SeqBinaryTree *tree) {
for (int i = 0; i < MAX_SIZE; i++) {
tree->data[i] = 0; // 0表示空节点
}
tree->size = 0;
}
// 设置根节点
void setRoot(SeqBinaryTree *tree, int value) {
tree->data[1] = value;
tree->size = 1;
}
// 设置左孩子(父节点索引为p,从1开始)
void setLeft(SeqBinaryTree *tree, int parent, int value) {
int idx = parent * 2;
if (idx >= MAX_SIZE) {
printf("索引越界\n");
return;
}
tree->data[idx] = value;
if (idx > tree->size) tree->size = idx;
}
// 设置右孩子
void setRight(SeqBinaryTree *tree, int parent, int value) {
int idx = parent * 2 + 1;
if (idx >= MAX_SIZE) {
printf("索引越界\n");
return;
}
tree->data[idx] = value;
if (idx > tree->size) tree->size = idx;
}
// 获取左孩子
int getLeft(SeqBinaryTree *tree, int parent) {
int idx = parent * 2;
if (idx > tree->size) return 0;
return tree->data[idx];
}
// 获取右孩子
int getRight(SeqBinaryTree *tree, int parent) {
int idx = parent * 2 + 1;
if (idx > tree->size) return 0;
return tree->data[idx];
}
// 获取父节点
int getParent(SeqBinaryTree *tree, int child) {
if (child <= 1) return 0;
int idx = child / 2;
return tree->data[idx];
}
// 打印树(按层)
void printTree(SeqBinaryTree *tree) {
printf("二叉树数组存储(下标从1开始):\n");
for (int i = 1; i <= tree->size; i++) {
if (tree->data[i] != 0) {
printf("data[%d]=%d", i, tree->data[i]);
// 打印父节点信息
if (i > 1) {
printf(" (父节点: %d)", getParent(tree, i));
}
printf("\n");
}
}
}
int main() {
SeqBinaryTree tree;
initTree(&tree);
// 构建一个完全二叉树
setRoot(&tree, 1);
setLeft(&tree, 1, 2);
setRight(&tree, 1, 3);
setLeft(&tree, 2, 4);
setRight(&tree, 2, 5);
setLeft(&tree, 3, 6);
printTree(&tree);
printf("\n节点2的左孩子: %d\n", getLeft(&tree, 2));
printf("节点3的右孩子: %d\n", getRight(&tree, 3));
printf("节点6的父节点: %d\n", getParent(&tree, 6));
return 0;
}
运行结果:
text
二叉树数组存储(下标从1开始):
data[1]=1
data[2]=2 (父节点: 1)
data[3]=3 (父节点: 1)
data[4]=4 (父节点: 2)
data[5]=5 (父节点: 2)
data[6]=6 (父节点: 3)
节点2的左孩子: 4
节点3的右孩子: 0
节点6的父节点: 3
四、满二叉树与完全二叉树的性质
4.1 满二叉树
| 性质 | 说明 |
|---|---|
| 节点数 | 深度为k,节点数 = 2^k - 1 |
| 叶子节点 | 都在最后一层,叶子数 = 2^(k-1) |
| 度为1的节点 | 0个 |
4.2 完全二叉树
| 性质 | 说明 |
|---|---|
| 节点数 | 深度为k,节点数在 [2^(k-1), 2^k-1] 之间 |
| 叶子节点 | 只能在最后两层 |
| 顺序存储 | 适合用数组存储,没有空间浪费 |
完全二叉树的顺序存储特点:
-
按层序遍历的顺序存入数组
-
下标 i 的节点,左孩子下标 2i,右孩子下标 2i+1
-
如果 2i > n,则没有左孩子
-
如果 2i+1 > n,则没有右孩子
五、二叉树与树的区别
| 对比项 | 树 | 二叉树 |
|---|---|---|
| 节点度数 | 无限制 | 最多2 |
| 子树顺序 | 无序 | 有序(左右区分) |
| 空树 | 通常不允许 | 允许 |
六、小结
这一篇我们学习了树和二叉树的基础概念:
| 要点 | 说明 |
|---|---|
| 树术语 | 根、叶子、度、深度、高度 |
| 二叉树 | 每个节点最多两个子节点,左右有序 |
| 满二叉树 | 所有层满,节点数 2^k-1 |
| 完全二叉树 | 最后一层靠左排列,适合数组存储 |
| 顺序存储 | 用数组下标模拟父子关系:2i, 2i+1 |
核心公式:
-
第i层最多节点数:2^(i-1)
-
深度k的二叉树最多节点数:2^k - 1
-
叶子节点数 = 度为2的节点数 + 1
下一篇我们讲二叉树的链式存储和四种遍历方式。
七、思考题
-
一棵完全二叉树有100个节点,它的叶子节点有多少个?
-
一棵二叉树的叶子节点数为20,度为2的节点数是多少?
-
顺序存储中,如何判断一个节点是否有左孩子?如何判断是否有右孩子?
-
为什么完全二叉树适合用数组存储,而普通二叉树不适合?
欢迎在评论区讨论你的答案。