一、树的存储结构
1.1 双亲表示法
每个节点存储数据和父节点下标,适合找父节点的场景。
c
#define MAX_SIZE 100
typedef struct {
int data;
int parent; // 父节点下标
} PNode;
typedef struct {
PNode nodes[MAX_SIZE];
int root; // 根节点下标
int size;
} PTree;
缺点:找孩子需要遍历整个数组。
1.2 孩子表示法
每个节点用链表存储所有孩子,适合找孩子的场景。
c
typedef struct ChildNode {
int childIndex;
struct ChildNode *next;
} ChildNode;
typedef struct {
int data;
ChildNode *firstChild;
} CNode;
缺点:找父节点不方便。
1.3 孩子兄弟表示法(重点)
每个节点存储:第一个孩子、右兄弟。
c
typedef struct CSNode {
int data;
struct CSNode *firstChild; // 第一个孩子
struct CSNode *nextSibling; // 右兄弟
} CSNode, *CSTree;
这种表示法把树 变成了二叉树------左指针指向孩子,右指针指向兄弟。
画个图:
text
原树: 孩子兄弟表示法:
A A
/ | \ /
B C D B
| \
E C
\
D
/
E
二、树与二叉树的转换
2.1 转换规则(左孩子右兄弟)
| 原树关系 | 转换后二叉树关系 |
|---|---|
| 第一个孩子 | 左孩子 |
| 下一个兄弟 | 右孩子 |
口诀:左孩子,右兄弟。
2.2 手动转换示例
原树:
text
A
/ | \
B C D
/ \
E F
转换步骤:
-
A的第一个孩子是B → A的左孩子是B
-
B的兄弟是C → B的右孩子是C
-
C的兄弟是D → C的右孩子是D
-
C的第一个孩子是E → C的左孩子是E
-
E的兄弟是F → E的右孩子是F
转换后的二叉树:
text
A
/
B
\
C
/ \
E D
\
F
2.3 转换后的特点
-
根节点没有右孩子(因为根没有兄弟)
-
左孩子是原树的第一个孩子
-
右孩子是原树的下一个兄弟
2.4 代码实现
c
#include <stdio.h>
#include <stdlib.h>
// 树节点(孩子兄弟表示法)
typedef struct CSNode {
char data;
struct CSNode *firstChild;
struct CSNode *nextSibling;
} CSNode, *CSTree;
// 创建节点
CSNode* createNode(char data) {
CSNode *node = (CSNode*)malloc(sizeof(CSNode));
node->data = data;
node->firstChild = NULL;
node->nextSibling = NULL;
return node;
}
// 手动构建一棵树
// A
// / | \
// B C D
// / \
// E F
CSTree buildTree() {
CSNode *A = createNode('A');
CSNode *B = createNode('B');
CSNode *C = createNode('C');
CSNode *D = createNode('D');
CSNode *E = createNode('E');
CSNode *F = createNode('F');
A->firstChild = B;
B->nextSibling = C;
C->nextSibling = D;
C->firstChild = E;
E->nextSibling = F;
return A;
}
// 二叉树的前序遍历(验证转换结果)
void preorder(CSNode *root) {
if (root == NULL) return;
printf("%c ", root->data);
preorder(root->firstChild);
preorder(root->nextSibling);
}
// 二叉树的中序遍历
void inorder(CSNode *root) {
if (root == NULL) return;
inorder(root->firstChild);
printf("%c ", root->data);
inorder(root->nextSibling);
}
int main() {
CSTree tree = buildTree();
printf("转换后的二叉树(前序): ");
preorder(tree);
printf("\n");
printf("转换后的二叉树(中序): ");
inorder(tree);
printf("\n");
return 0;
}
运行结果:
text
转换后的二叉树(前序): A B C E F D
转换后的二叉树(中序): B E F C D A
三、森林与二叉树的转换
3.1 森林的定义
森林是 m(m ≥ 0)棵互不相交的树的集合。
3.2 森林转二叉树
规则:
-
将森林中的每棵树分别转换成二叉树(左孩子右兄弟)
-
将每棵树的根节点视为兄弟关系,用右指针连接
步骤:
-
第一棵树的根作为转换后二叉树的根
-
第二棵树的根作为第一棵树根的右孩子
-
第三棵树的根作为第二棵树根的右孩子
-
以此类推...
text
森林: 转换后的二叉树:
Tree1 Tree2 Tree1根
↓ ↓ \
Tree1根 Tree2根 Tree2根
\
Tree3根
3.3 二叉树转森林
判断条件:如果二叉树的根节点有右孩子,说明它是由森林转换来的。
规则:
-
将根节点和左子树作为第一棵树
-
将右子树作为剩下的森林,递归处理
3.4 代码实现
c
#include <stdio.h>
#include <stdlib.h>
typedef struct CSNode {
char data;
struct CSNode *firstChild;
struct CSNode *nextSibling;
} CSNode, *CSTree;
CSNode* createNode(char data) {
CSNode *node = (CSNode*)malloc(sizeof(CSNode));
node->data = data;
node->firstChild = NULL;
node->nextSibling = NULL;
return node;
}
// 构建森林:三棵树
// 树1: A 树2: D 树3: G
// / \ | |
// B C E H
// | |
// F I
CSTree buildForest() {
// 树1
CSNode *A = createNode('A');
CSNode *B = createNode('B');
CSNode *C = createNode('C');
A->firstChild = B;
B->nextSibling = C;
// 树2
CSNode *D = createNode('D');
CSNode *E = createNode('E');
CSNode *F = createNode('F');
D->firstChild = E;
E->firstChild = F;
// 树3
CSNode *G = createNode('G');
CSNode *H = createNode('H');
CSNode *I = createNode('I');
G->firstChild = H;
H->firstChild = I;
// 连接成森林(根节点用右兄弟连接)
A->nextSibling = D;
D->nextSibling = G;
return A;
}
// 森林转二叉树(其实构建森林时已经按规则连接好了)
// 这里只是验证
// 二叉树转森林:按右兄弟断开
void forestToTree(CSTree root) {
if (root == NULL) return;
printf("树根: %c\n", root->data);
// 递归处理左子树(孩子)
if (root->firstChild) {
printf(" %c 的孩子: ", root->data);
CSNode *child = root->firstChild;
while (child) {
printf("%c ", child->data);
child = child->nextSibling;
}
printf("\n");
forestToTree(root->firstChild);
}
// 递归处理右子树(下一棵树)
if (root->nextSibling) {
forestToTree(root->nextSibling);
}
}
void preorder(CSTree root) {
if (root == NULL) return;
printf("%c ", root->data);
preorder(root->firstChild);
preorder(root->nextSibling);
}
int main() {
CSTree forest = buildForest();
printf("森林转换后的二叉树(前序): ");
preorder(forest);
printf("\n\n");
printf("从二叉树还原森林:\n");
forestToTree(forest);
return 0;
}
运行结果:
text
森林转换后的二叉树(前序): A B C D E F G H I
从二叉树还原森林:
树根: A
A 的孩子: B C
树根: B
树根: C
树根: D
D 的孩子: E
树根: E
E 的孩子: F
树根: F
树根: G
G 的孩子: H
树根: H
H 的孩子: I
树根: I
四、树与二叉树的遍历对应关系
| 树的遍历 | 对应二叉树的遍历 |
|---|---|
| 先根遍历 | 前序遍历 |
| 后根遍历 | 中序遍历 |
验证:以前面的树为例
原树先根遍历:A B C E F D
对应二叉树前序:A B C E F D ✓
原树后根遍历:B E F C D A
对应二叉树中序:B E F C D A ✓
五、三种存储方式对比
| 存储方式 | 找孩子 | 找父节点 | 空间开销 | 适用场景 |
|---|---|---|---|---|
| 双亲表示法 | 慢 | 快 | 小 | 并查集 |
| 孩子表示法 | 快 | 慢 | 中 | 树形结构遍历 |
| 孩子兄弟表示法 | 快 | 慢 | 中 | 树转二叉树 |
六、小结
这一篇我们学习了树、森林与二叉树的转换:
| 要点 | 说明 |
|---|---|
| 孩子兄弟表示法 | firstChild + nextSibling |
| 树→二叉树 | 左孩子,右兄弟 |
| 森林→二叉树 | 先每棵树转二叉树,再根节点用右兄弟连接 |
| 遍历对应 | 树先根=二叉树前序,树后根=二叉树中序 |
核心口诀:
-
左孩子:第一个孩子
-
右兄弟:下一个兄弟
-
森林转:根连根
下一篇我们讲哈夫曼树与哈夫曼编码。
七、思考题
-
一棵树转换成的二叉树,它的右子树可能为空吗?什么情况下为空?
-
森林转换成的二叉树,根节点的右子树可能为空吗?什么情况下为空?
-
如果已知一棵二叉树的先序和中序遍历序列,如何判断它是由树还是由森林转换来的?
-
尝试用孩子兄弟表示法实现树的先根遍历和后根遍历。
欢迎在评论区讨论你的答案。