一、树的核心概念
- 定义:由 n 个节点构成的有限集,n=0 时为空树;非空树有且仅有一个根节点,除根节点外每个节点仅有一个前驱,所有节点可有 0 个或多个后继,且树中无环。
- 关键术语:
- 度:节点含子树的个数为节点度,树的度为所有节点度的最大值。
- 节点分类:度为 0 的是叶子节点(无子女),度不为 0 的是分支节点(有子女)。
- 节点关系:子树的根节点是当前节点的孩子节点,当前节点是孩子节点的双亲(父)节点;同双亲的节点为兄弟节点,双亲在同一层的节点为堂兄弟节点。
- 森林:m 棵互不相交的树组成的集合。
- 层数:根节点为第一层,逐层向下递增。
二、树的三种存储结构
(一)双亲表示法
-
存储方式:一维数组存储节点,每个元素为结构体(含 data 存储节点值、parent 存储父节点下标),根节点 parent 设为 - 1。
-
特点:查找父节点高效,直接通过 parent 下标定位;查找孩子节点需遍历整个数组,效率较低,适用于少找孩子、多找父亲的场景。
-
代码实现:
#include <stdio.h>
#include <string.h>// 定义双亲表示法的节点结构体
#define MAX_TREE_SIZE 100
typedef struct {
// 节点数据
char data;
// 父节点下标,根节点为-1
int parent;
} PTNode;// 树的双亲表示结构
typedef struct {
// 节点数组
PTNode nodes[MAX_TREE_SIZE];
// 节点数和根节点下标
int nodeNum, root;
} PTree;// 初始化树
void initPTree(PTree *tree) {
tree->nodeNum = 0;
tree->root = -1;
memset(tree->nodes, 0, sizeof(tree->nodes));
}// 添加节点
void addNode(PTree *tree, char data, int parentIndex) {
if (tree->nodeNum >= MAX_TREE_SIZE) {
printf("树已满,无法添加节点\n");
return;
}
tree->nodes[tree->nodeNum].data = data;
tree->nodes[tree->nodeNum].parent = parentIndex;
// 根节点
if (parentIndex == -1) {
tree->root = tree->nodeNum;
}
tree->nodeNum++;
}// 查找指定节点的父节点
int findParent(PTree *tree, int nodeIndex) {
if (nodeIndex < 0 || nodeIndex >= tree->nodeNum) {
return -2; // 无效节点
}
return tree->nodes[nodeIndex].parent;
}// 测试示例
int main() {
PTree tree;
initPTree(&tree);
// 添加根节点A(父节点-1)
addNode(&tree, 'A', -1);
// 添加节点B(父节点0,即A)
addNode(&tree, 'B', 0);
// 添加节点C(父节点0,即A)
addNode(&tree, 'C', 0);int parentIdx = findParent(&tree, 1); // 查找B的父节点 if (parentIdx == -1) { printf("节点是根节点\n"); } else if (parentIdx == -2) { printf("节点不存在\n"); } else { printf("节点%c的父节点是%c\n", tree->nodes[1].data, tree->nodes[parentIdx].data); } return 0;}
(二)孩子表示法
-
存储方式:结合顺序存储与链式存储,数组存储节点值及孩子链表头指针,链表存储孩子节点的下标。
-
特点:查找孩子节点高效,直接遍历对应链表即可;查找父节点需遍历所有孩子链表,可通过拓展结构体增加 parent 成员优化,适用于多找孩子的场景。
-
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MAX_TREE_SIZE 100
// 孩子链表节点结构
typedef struct ChildNode {
// 孩子节点在数组中的下标
int childIndex;
// 指向下一个孩子
struct ChildNode *next;
} ChildNode;// 树节点结构(数组元素)
typedef struct {
// 节点数据
char data;
// 孩子链表头指针
ChildNode *firstChild;
} CTNode;// 树的孩子表示结构
typedef struct {
// 节点数组
CTNode nodes[MAX_TREE_SIZE];
// 节点数和根节点下标
int nodeNum, root;
} CTree;// 初始化树
void initCTree(CTree *tree) {
tree->nodeNum = 0;
tree->root = -1;
for (int i = 0; i < MAX_TREE_SIZE; i++) {
tree->nodes[i].data = '\0';
tree->nodes[i].firstChild = NULL;
}
}// 添加孩子节点到指定父节点
void addChild(CTree *tree, int parentIndex, int childIndex) {
if (parentIndex < 0 || parentIndex >= tree->nodeNum || childIndex < 0 || childIndex >= tree->nodeNum) {
printf("父节点或子节点下标无效\n");
return;
}
ChildNode *newNode = (ChildNode *)malloc(sizeof(ChildNode));
newNode->childIndex = childIndex;
newNode->next = NULL;// 找到父节点孩子链表的末尾,插入新节点 ChildNode *p = tree->nodes[parentIndex].firstChild; if (p == NULL) { tree->nodes[parentIndex].firstChild = newNode; } else { while (p->next != NULL) { p = p->next; } p->next = newNode; }}
// 遍历指定节点的所有孩子
void traverseChildren(CTree *tree, int parentIndex) {
if (parentIndex < 0 || parentIndex >= tree->nodeNum) {
printf("节点不存在\n");
return;
}
printf("节点%c的孩子节点:", tree->nodes[parentIndex].data);
ChildNode *p = tree->nodes[parentIndex].firstChild;
while (p != NULL) {
printf("%c ", tree->nodes[p->childIndex].data);
p = p->next;
}
printf("\n");
}// 测试示例
int main() {
CTree tree;
initCTree(&tree);// 添加根节点A tree->nodes[0].data = 'A'; tree->root = 0; tree->nodeNum++; // 添加节点B tree->nodes[1].data = 'B'; tree->nodeNum++; // 添加节点C tree->nodes[2].data = 'C'; tree->nodeNum++; // 给A添加孩子B和C addChild(&tree, 0, 1); addChild(&tree, 0, 2); // 遍历A的孩子 traverseChildren(&tree, 0); return 0;}
(三)孩子兄弟表示法(重点)
-
存储方式:二叉链表结构,左指针指向第一个孩子,右指针指向同层下一个兄弟(核心口诀:左孩子右兄弟)。
-
优势:可将任意树转换为二叉树,能复用二叉树的算法进行操作,是考试高频考点,查找孩子和兄弟节点均较便捷。
-
代码实现:
#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;
}// 先序遍历(按树的逻辑遍历)
void preOrderTraverse(CSTree tree) {
if (tree == NULL) return;
// 访问当前节点
printf("%c ", tree->data);
// 遍历第一个孩子
preOrderTraverse(tree->firstChild);
// 遍历兄弟节点
preOrderTraverse(tree->nextSibling);
}// 测试示例
int main() {
// 构建树:A是根,A的孩子是B,B的兄弟是C;B的孩子是D
CSNode *A = createNode('A');
CSNode *B = createNode('B');
CSNode *C = createNode('C');
CSNode *D = createNode('D');A->firstChild = B; B->nextSibling = C; B->firstChild = D; printf("先序遍历结果:"); preOrderTraverse(A); // 输出:A B D C printf("\n"); return 0;}
三、二叉树的基础认知
- 定义:每个节点最多有两个子节点的树,是树结构的特殊形式。
- 特殊类型:
- 满二叉树:每一层节点数均达到最大值(每个非叶子节点都有两个子节点)。
- 完全二叉树:叶节点仅出现在最下层和次下层,且最下层节点集中在左侧,缺失节点仅可能在最下层右侧。
四、二叉树的四种遍历方法(核心重点,附 C 语言实现)
(一)先序遍历
-
遍历顺序:根节点 → 左子树 → 右子树(递归执行,分支节点需重复该逻辑)。
-
记忆技巧:"先见根",优先访问当前树的根节点,再逐层深入左子树,最后回溯访问右子树。
-
代码实现:
#include <stdio.h>
#include <stdlib.h>// 二叉树节点结构
typedef struct BiTNode {
char data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;// 创建二叉树(按先序序列,空节点用#表示)
void createBiTree(BiTree *T) {
char ch;
// 注意:前面加空格避免读取换行符
scanf(" %c", &ch);
if (ch == '#') {
*T = NULL;
} else {
*T = (BiTNode *)malloc(sizeof(BiTNode));
(*T)->data = ch;
// 构建左子树
createBiTree(&(*T)->lchild);
// 构建右子树
createBiTree(&(*T)->rchild);
}
}// 先序遍历
void preOrder(BiTree T) {
if (T == NULL) return;
// 访问根节点
printf("%c ", T->data);
// 遍历左子树
preOrder(T->lchild);
// 遍历右子树
preOrder(T->rchild);
}// 测试示例:输入序列 A B # D # # C # #
int main() {
BiTree T;
printf("请输入二叉树先序序列(空节点用#表示):");
createBiTree(&T);
printf("先序遍历结果:");
preOrder(T); // 输出:A B D C
printf("\n");
return 0;
}
(二)中序遍历
-
遍历顺序:左子树 → 根节点 → 右子树(递归执行)。
-
记忆技巧:"根在中间",先深入左子树最底层,再回溯访问根节点,最后访问右子树。
-
核心用途:结合先序遍历可唯一构造二叉树(先序找根,中序分左右子树)。
-
代码实现(基于上述二叉树节点结构):
// 中序遍历
void inOrder(BiTree T) {
if (T == NULL) return;
// 遍历左子树
inOrder(T->lchild);
// 访问根节点
printf("%c ", T->data);
// 遍历右子树
inOrder(T->rchild);
}// 测试:在main函数中调用
// printf("中序遍历结果:");
// inOrder(T); // 输入A B # D # # C # #,输出:B D A C
(三)后序遍历
-
遍历顺序:左子树 → 右子树 → 根节点(递归执行)。
-
记忆技巧:"最后见根",类比 "剪葡萄",先摘完左右子树的所有节点,最后访问根节点。
-
核心用途:后序遍历的最后一个节点为根节点,可辅助构造二叉树。
-
代码实现(基于上述二叉树节点结构):
// 后序遍历
void postOrder(BiTree T) {
if (T == NULL) return;
// 遍历左子树
postOrder(T->lchild);
// 遍历右子树
postOrder(T->rchild);
// 访问根节点
printf("%c ", T->data);
}// 测试:在main函数中调用
// printf("后序遍历结果:");
// postOrder(T); // 输入A B # D # # C # #,输出:D B C A
(四)层序遍历
-
遍历顺序:从上到下、逐层遍历,同一层内从左到右依次访问节点。
-
记忆技巧:"按层扫描",类似阅读文章的顺序,简单直观,无需递归(需借助队列实现)。
-
代码实现:
// 队列节点结构(用于层序遍历)
typedef struct QueueNode {
BiTree data;
struct QueueNode *next;
} QueueNode;typedef struct {
QueueNode *front, *rear;
} LinkQueue;// 初始化队列
void initQueue(LinkQueue *q) {
q->front = q->rear = (QueueNode *)malloc(sizeof(QueueNode));
q->front->next = NULL;
}// 入队
void enQueue(LinkQueue *q, BiTree t) {
QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));
newNode->data = t;
newNode->next = NULL;
q->rear->next = newNode;
q->rear = newNode;
}// 出队
int deQueue(LinkQueue *q, BiTree *t) {
if (q->front == q->rear) return 0; // 队空
QueueNode *p = q->front->next;
*t = p->data;
q->front->next = p->next;
if (q->rear == p) q->rear = q->front;
free(p);
return 1;
}// 层序遍历
void levelOrder(BiTree T) {
LinkQueue q;
initQueue(&q);
if (T == NULL) return;
// 根节点入队
enQueue(&q, T);
while (q.front != q.rear) {
BiTree node;
// 出队
deQueue(&q, &node);
// 访问节点
printf("%c ", node->data);
// 左孩子入队
if (node->lchild != NULL) enQueue(&q, node->lchild);
// 右孩子入队
if (node->rchild != NULL) enQueue(&q, node->rchild);
}
}// 测试:在main函数中调用
// printf("层序遍历结果:");
// levelOrder(T); // 输入A B # D # # C # #,输出:A B C D
五、学习技巧以及总结
学习技巧
- 存储结构:结合实例画图,对比三种存储方式的优缺点与适用场景。
- 遍历方法:通过动画演示 + 手动模拟,牢记 "根左右""左根右""左右根" 的核心顺序,多做遍历真题巩固。
- 构造二叉树:掌握 "先序找根、中序分左右""后序找根、先序分左右" 的核心逻辑,勤加练习。
2.总结
- 树的三种存储结构各有优劣:双亲表示法易找父节点,孩子表示法易找子节点,孩子兄弟表示法可将普通树转为二叉树处理,是最常用的存储方式。
- 二叉树遍历是核心考点:先序(根左右)、中序(左根右)、后序(左右根)基于递归实现,层序遍历需借助队列,不同遍历顺序可相互配合构造二叉树。