一,树的基本概念
树是一种非线性的数据结构,数据逻辑中之所以会引入“树”这个东西,是借助了生活中树的“分支”的概念。逻辑数据中的树可用于描述一种类似组织结构的关系。 在一堆数据中包含一个称为”根”的节点,其他的节点再次组成一个新的“树”,树的定义是一种递归的定义。数据逻辑上的树一般习惯“倒着”,即:根在上,分支和叶子在下。
1.1 树的一般结构
1.2 树的相关术语
1) 结点: 树中的元素 及其子树
2.)孩子/双亲: 一个结点的后继结点叫该结点的孩子,该结点叫孩子的双亲结点。
3. 结点的度:该结点分支的数量
4. 叶子: 度为0的结点,
5. 结点的层次: 从根到该结点的层数(根的层次为1);
6. 树的高度(深度):树中结点层次的最大值
二,二叉树的基本概念
二叉树是每个结点最多只有2个两颗子树的一种“树”型结构。
2.1 二叉树的一般结构
2.2 满二叉树
一棵深度为k,且有2^k-1个结点的二叉树,称为满二叉树。这种树的特点是每一层上的结点数都是最大结点数。
通俗地讲,一棵满二叉树需要满足以下条件:
所有叶子节点都在同一层。
除了叶子节点,每个节点都有两个子节点。
简单来说,满二叉树是一个“完美”的三角形结构,每一层的节点都是满的。
2.3 完全二叉树
在一棵二叉树中,除最后一层外,若其余层都是满的,或者最后一层是满的,或者是最后一层在右边缺少连续若干结点,则此二叉树为完全二叉树
我们可以这样理解:
将节点从上到下、从左到右进行编号。
这棵树的节点编号必须是连续的,中间不能有任何间断。
换句话说,除了最后一层,其他各层节点数都达到最大,并且最后一层的节点都连续集中在左边。
例如这种就不是完全二叉树,因为它最后一层的结点不是都在左边连续。
2.4 满二叉树与完全二叉树之间的关系
1. 满二叉树一定是完全二叉树。
因为满二叉树的节点是满的,从上到下、从左到右编号一定是连续的,所以它完全符合完全二叉树的定义。
2. 完全二叉树不一定是满二叉树。
完全二叉树只要求最后一层的节点是连续靠左排列的,并不要求最后一层是满的。只有当一棵完全二叉树的最后一层也是满的时候,它才是一棵满二叉树。
总的来说核心关系:
满二叉树是完全二叉树的一个特例。
所有的满二叉树都是完全二叉树,但只有一部分完全二叉树是满二叉树。
可以把满二叉树看作是“最完美”的完全二叉树。
2.5 二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找(搜索)树(Binary Search Tree)。其定义为:
二叉排序树或者是空树,或者是满足如下性质的二叉树:
1.若它的左子树非空,则左子树上所有结点的值均小于根结点的值。
2.若它的右子树非空,则右子树上所有结点的值均大于根结点的值。
3.左、右子树本身又各是一棵二叉排序树。
简单概括就是 “左 < 根 < 右”。
一个重要的推论是: 对二叉排序树进行中序遍历(左-根-右),可以得到一个严格递增的有序序列。
这是一个典型的二叉排序树
可以看到,对于任意节点,其左边的所有后代都比它小,右边的所有后代都比它大。
三,二叉树操作
3.1 二叉树的创建

建立图示二叉树的核心代码:
// 定义二叉树节点结构
// 对应右图中的一个小方块,包含数据区和两个指针区
typedef struct TreeNode {
char data; // 节点存储的数据 (A, B, C...)
struct TreeNode *left; // 指向左子树的指针
struct TreeNode *right; // 指向右子树的指针
} TreeNode;
// 创建新节点的函数 (辅助函数)
// 功能:分配内存并初始化一个新节点
TreeNode* createNode(char data) {
// 使用 malloc 动态分配一个 TreeNode 大小的内存空间
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = NULL; // 新节点的左右孩子默认为空 (对应图中的 ^)
newNode->right = NULL;
return newNode; // 返回创建好的新节点的地址
}
// 创建根节点 A
TreeNode* root = createNode('A');
// 创建第二层节点 B 和 C
root->left = createNode('B');
root->right = createNode('C');
// 创建第三层节点 D, E, F
// B 是 D 和 E 的父节点
root->left->left = createNode('D');
root->left->right = createNode('E');
// C 是 F 的父节点
root->right->right = createNode('F');
// 创建第四层节点 G, H
// E 是 G 和 H 的父节点
root->left->right->left = createNode('G');
root->left->right->right = createNode('H');
3.2 二叉树(BST)的插入
插入操作遵循以下规则:
从根节点(root)开始:待插入的值 19 与根节点 20 比较。
19 < 20:因为 19 小于 20,所以我们向左子树移动,来到节点 15。
19 > 15:因为 19 大于 15,所以我们向右子树移动,来到节点 17。
19 > 17:因为 19 大于 17,所以我们尝试向 17 的右子树移动。
找到插入位置:发现节点 17 的右孩子为空(NULL),这就是 19 应该被插入的位置。
完成插入:创建一个值为 19 的新节点,并将其作为 17 的右孩子。
#include <stdio.h>
#include <stdlib.h>
// 1. 定义二叉树节点结构
typedef struct TreeNode {
int data; // 节点存储的数据
struct TreeNode *left; // 指向左子树的指针
struct TreeNode *right; // 指向右子树的指针
} TreeNode;
// 2. 创建新节点的函数 (辅助函数)
TreeNode* createNode(int data) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 3. 核心:实现二叉排序树的插入函数 (递归版本)
TreeNode* insertNode(TreeNode* root, int data) {
// 基本情况:如果树为空 (找到了插入位置),则创建新节点并返回
if (root == NULL) {
return createNode(data);
}
// 递归步骤:根据值的大小决定向左还是向右
if (data < root->data) {
// 待插入的值小于当前节点,应插入到左子树
// 将返回的新子树根节点连接到当前节点的左指针
root->left = insertNode(root->left, data);
} else if (data > root->data) {
// 待插入的值大于当前节点,应插入到右子树
// 将返回的新子树根节点连接到当前节点的右指针
root->right = insertNode(root->right, data);
}
// 如果 data == root->data,则什么也不做 (不允许重复值)
// 返回 (可能未经修改的) 节点指针
return root;
}
// 4. 中序遍历函数 (用于验证)
// (左 -> 根 -> 右)
void inOrderTraversal(TreeNode* root) {
if (root == NULL) {
return;
}
inOrderTraversal(root->left);
printf("%d ", root->data);
inOrderTraversal(root->right);
}
// 5. 释放二叉树内存的函数
void freeTree(TreeNode* root) {
if (root == NULL) {
return;
}
freeTree(root->left);
freeTree(root->right);
free(root);
}
// 6. 主函数
int main() {
TreeNode* root = NULL;
// 注意:插入顺序会影响树的最终形状,但只要是二叉排序树,中序遍历结果不变
root = insertNode(root, 20);
insertNode(root, 15);
insertNode(root, 25);
insertNode(root, 10);
insertNode(root, 17);
insertNode(root, 28);
insertNode(root, 16);
printf("Original Tree (in-order): ");
inOrderTraversal(root);
printf("\n");
// --- 执行插入操作 ---
int newValue = 19;
printf("\nInserting new value: %d\n\n", newValue);
insertNode(root, newValue);
// --- 验证结果 ---
printf("Tree after insertion (in-order): ");
inOrderTraversal(root);
printf("\n");
// --- 释放内存 ---
freeTree(root);
return 0;
}
运行结果:
3.3 二叉树遍历
1. 前序遍历
前序遍历通俗的说就是从二叉树的根结点出发,先输出根结点数据,然后输出左结点,最后输出右结点的数据。
对于上图的二叉树,从根结点出发,则第一次到达结点A,故输出A; 继续向左访问,第一次访问结点B,故输出B; 按照同样规则,输出D,输出H; 当到达叶子结点H,返回到D,此时已经是第二次到达D,故不在输出D,进而向D右子树访问,D右子树不为空,则访问至I,第一次到达I,则输出I; I为叶子结点,则返回到D,D左右子树已经访问完毕,则返回到B,进而到B右子树,第一次到达E,故输出E; 向E左子树,故输出J; 按照同样的访问规则,继续输出C、F、G。
前序遍历输出为: ABDHIEJCFG
2. 中序遍历
中序遍历通俗的说就是从二叉树的根结点出发,先输出左结点数据,然后输出根结点,最后输出右结点的数据。
对于上图的二叉树,从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;继续到达D,H; 到达H,H左子树为空,则返回到H,此时第二次访问H,故输出H; H右子树为空,则返回至D,此时第二次到达D,故输出D; 由D返回至B,第二次到达B,故输出B; 按照同样规则继续访问,输出J、E、A、F、C、G。
中序遍历输出为: HDIBJEAFCG
3. 后序遍历
后序遍历通俗的说就是从二叉树的根结点出发,先输出左结点数据,然后输出右结点,最后输出根结点的数据。
对于上图的二叉树,从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;继续到达D,H; 到达H,H左子树为空,则返回到H,此时第二次访问H,不输出H; H右子树为空,则返回至H,此时第三次到达H,故输出H; 由H返回至D,第二次到达D,不输出D; 继续访问至I,I左右子树均为空,故第三次访问I时,输出I; 返回至D,此时第三次到达D,故输出D; 按照同样规则继续访问,输出J、E、B、F、G、C,A。
后序遍历输出为:HIDJEBFGCA
4. 程序实现前序遍历,中序遍历,后序遍历
#include <stdio.h>
#include <stdlib.h>
// 1. 定义二叉树节点结构
typedef struct TreeNode {
char data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 2. 创建新节点的辅助函数
TreeNode* createNode(char data) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 3. 实现前序遍历 (根 -> 左 -> 右)
void preOrderTraversal(TreeNode* root) {
if (root == NULL) {
return;
}
printf("%c ", root->data); // 访问根节点
preOrderTraversal(root->left); // 递归遍历左子树
preOrderTraversal(root->right); // 递归遍历右子树
}
// 4. 实现中序遍历 (左 -> 根 -> 右)
void inOrderTraversal(TreeNode* root) {
if (root == NULL) {
return;
}
inOrderTraversal(root->left); // 递归遍历左子树
printf("%c ", root->data); // 访问根节点
inOrderTraversal(root->right); // 递归遍历右子树
}
// 5. 实现后序遍历 (左 -> 右 -> 根)
void postOrderTraversal(TreeNode* root) {
if (root == NULL) {
return;
}
postOrderTraversal(root->left); // 递归遍历左子树
postOrderTraversal(root->right); // 递归遍历右子树
printf("%c ", root->data); // 访问根节点
}
// 6. 释放二叉树内存的函数 (使用后序遍历)
void freeTree(TreeNode* root) {
if (root == NULL) {
return;
}
freeTree(root->left);
freeTree(root->right);
free(root);
}
// 7. 主函数:构建树并执行遍历
int main() {
// --- 手动构建二叉树 ---
TreeNode* root = createNode('A');
root->left = createNode('B');
root->right = createNode('C');
root->left->left = createNode('D');
root->left->right = createNode('E');
root->right->left = createNode('F');
root->right->right = createNode('G');
root->left->left->left = createNode('H');
root->left->left->right = createNode('I');
root->left->right->left = createNode('J');
printf("Tree created based on the image.\n\n");
// --- 执行并输出三种遍历的结果 ---
printf("Pre-order Traversal : ");
preOrderTraversal(root);
printf("\n");
printf("In-order Traversal : ");
inOrderTraversal(root);
printf("\n");
printf("Post-order Traversal: ");
postOrderTraversal(root);
printf("\n\n");
// --- 释放内存 ---
freeTree(root);
printf("Tree memory has been freed.\n");
return 0;
}
运行结果:
3.4 二叉树的查找
例如查找图中的结点16。
1.从根结点出发,
2.如果比根节点小,那么就去其左子树找
3.如果比根节点大就去其右子树找
4.找到叶子都没找到,就代表查找失败
所以对于上图查找,查找过程严格遵循二叉排序树的 “左 < 根 < 右” 特性:
从根节点(root)开始:将目标值 16 与根节点 20 比较。
16 < 20:因为 16 小于 20,所以查找路径转向左子树,来到节点 15。
16 > 15:因为 16 大于 15,所以查找路径转向右子树,来到节点 17。
16 < 17:因为 16 小于 17,所以查找路径转向左子树,来到节点 16。
16 == 16:目标值与当前节点值相等,查找成功!
示例程序:
#include <stdio.h>
#include <stdlib.h>
// 1. 定义二叉树节点结构
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 2. 创建新节点的函数 (辅助函数)
TreeNode* createNode(int data) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 插入函数,用于构建树
TreeNode* insertNode(TreeNode* root, int data) {
if (root == NULL) {
return createNode(data);
}
if (data < root->data) {
root->left = insertNode(root->left, data);
} else if (data > root->data) {
root->right = insertNode(root->right, data);
}
return root;
}
// 3. 核心:实现二叉排序树的查找函数 (递归版本)
// 功能:在以 root 为根的树中查找值为 key 的节点
// 返回:如果找到,返回该节点的指针;否则,返回 NULL
TreeNode* searchNode(TreeNode* root, int key) {
// 基本情况 1:树为空,或者找到了目标值
if (root == NULL || root->data == key) {
return root;
}
// 递归步骤:根据值的大小决定向左还是向右查找
if (key < root->data) {
// 目标值小于当前节点,去左子树查找
return searchNode(root->left, key);
} else {
// 目标值大于当前节点,去右子树查找
return searchNode(root->right, key);
}
}
// 4. 释放二叉树内存的函数
void freeTree(TreeNode* root) {
if (root == NULL) {
return;
}
freeTree(root->left);
freeTree(root->right);
free(root);
}
// 5. 主函数
int main() {
TreeNode* root = NULL;
// --- 构建图中所示的树 ---
root = insertNode(root, 20);
insertNode(root, 15);
insertNode(root, 25);
insertNode(root, 10);
insertNode(root, 17);
insertNode(root, 28);
insertNode(root, 16);
printf("Binary Search Tree has been created.\n\n");
// --- 执行查找操作 ---
// 示例 1: 查找存在的节点 16
int key_to_find_1 = 16;
printf("Searching for %d...\n", key_to_find_1);
TreeNode* result1 = searchNode(root, key_to_find_1);
if (result1 != NULL) {
printf("Found! The value is %d.\n", result1->data);
} else {
printf("Value %d not found in the tree.\n", key_to_find_1);
}
printf("\n");
// 示例 2: 查找不存在的节点 99
int key_to_find_2 = 99;
printf("Searching for %d...\n", key_to_find_2);
TreeNode* result2 = searchNode(root, key_to_find_2);
if (result2 != NULL) {
printf("Found! The value is %d.\n", result2->data);
} else {
printf("Value %d not found in the tree.\n", key_to_find_2);
}
// --- 释放内存 ---
freeTree(root);
return 0;
}
输出结果:
3.5 二叉树的结点删除
删除操作的逻辑分析
在二叉排序树中,删除节点需要分三种情况讨论,以确保删除后仍然满足“左 < 根 < 右”的性质。
情况一:被删除的节点是叶子节点(没有孩子)
最简单的情况,直接将其父节点的对应指针(左或右)设为 NULL,然后释放该节点即可。
情况二:被删除的节点只有一个孩子(只有左孩子或只有右孩子)
也比较简单,将其父节点的指针“越过”被删除的节点,直接指向其唯一的孩子,然后释放该节点。
情况三:被删除的节点有两个孩子(既有左孩子又有右孩子)
这是最复杂的情况,也是图中展示的情况。
问题所在:不能直接删除该节点(如节点 15),否则会造成其左右子树都与父节点断开连接,树的结构就被破坏了。
解决方案:必须在不破坏二叉排序树性质的前提下,找到一个“替身”节点来取代被删除节点的位置。这个“替身”有两个选择:
前驱节点 (In-order Predecessor):被删除节点左子树中最大的节点。
后继节点 (In-order Successor):被删除节点右子树中最小的节点。
图片选择前驱节点作为“替身”的策略:
① 确定目标:要删除的节点是 15,它有两个孩子 (10 和 19)。
② 寻找替身:
进入 15 的左子树(根为 10)。
在这个子树中寻找值最大的节点。根据二叉排序树的性质,最大值一定在该子树的“最右边”。
查找路径是 10 -> 11。节点 11 就是 15 的前驱节点 (tmp)。
③ 执行替换与删除 :
将“替身”节点 11 的值复制到要被删除的节点 15 的位置。现在原来的 15 节点存储的值变成了 11。
此时,问题就转化成了“在左子树中删除原来的替身节点 11”。
删除节点 11 是一个更简单的情况(叶子节点或只有一个孩子的节点),可以通过递归调用删除函数来完成。
示例程序(删除图中的结点15):
#include <stdio.h>
#include <stdlib.h> //malloc 和 free
// 1. 定义二叉树节点结构
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 辅助函数:创建新节点
TreeNode* createNode(int data) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = newNode->right = NULL;
return newNode;
}
// 辅助函数:插入节点以构建树
TreeNode* insertNode(TreeNode* root, int data) {
if (root == NULL) return createNode(data);
if (data < root->data)
root->left = insertNode(root->left, data);
else if (data > root->data)
root->right = insertNode(root->right, data);
return root;
}
// 辅助函数:寻找子树中最大的节点 (前驱)
TreeNode* findMaxNode(TreeNode* node) {
TreeNode* current = node;
while (current && current->right != NULL) {
current = current->right;
}
return current;
}
// 核心函数:删除二叉排序树中的节点
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == NULL) {
return root;
}
if (key < root->data) {
root->left = deleteNode(root->left, key);
} else if (key > root->data) {
root->right = deleteNode(root->right, key);
} else {
if (root->left == NULL) {
TreeNode* temp = root->right;
free(root);
return temp;
} else if (root->right == NULL) {
TreeNode* temp = root->left;
free(root);
return temp;
}
TreeNode* temp = findMaxNode(root->left);
root->data = temp->data;
root->left = deleteNode(root->left, temp->data);
}
return root;
}
// 中序遍历函数 (用于验证)
void inOrderTraversal(TreeNode* root) {
if (root != NULL) {
inOrderTraversal(root->left);
printf("%d ", root->data);
inOrderTraversal(root->right);
}
}
// 采用后序遍历(左 -> 右 -> 根)的顺序来确保安全释放
void freeTree(TreeNode* root) {
// 递归的基准情况
if (root == NULL) {
return;
}
// 1. 先释放左子树
freeTree(root->left);
// 2. 再释放右子树
freeTree(root->right);
// 3. 最后释放根节点
free(root);
}
// =======================================================
// 主函数
int main() {
TreeNode* root = NULL;
// --- 构建树 ---
root = insertNode(root, 20);
insertNode(root, 15);
insertNode(root, 25);
insertNode(root, 10);
insertNode(root, 19);
insertNode(root, 28);
insertNode(root, 6);
insertNode(root, 11);
printf("Original Tree (in-order): ");
inOrderTraversal(root);
printf("\n");
// --- 执行删除操作 ---
int key_to_delete = 15;
printf("\nDeleting node: %d\n\n", key_to_delete);
root = deleteNode(root, key_to_delete);
// --- 验证结果 ---
printf("Tree after deletion (in-order): ");
inOrderTraversal(root);
printf("\n\n");
// --- 补充完整的内存释放操作 ---
printf("Starting to free the entire tree memory...\n");
freeTree(root);
root = NULL; // 将根指针置为NULL,防止成为野指针
printf("All tree memory has been freed successfully.\n");
return 0;
}
运行结果: