一,二叉树
1.1. 二叉树的定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两颗互不相交的,分别称为根结点的左子树和右子树的二叉树组合。
如图:
特点:每个结点最多 2 子树,区分左右顺序
1.2. 二叉树的五种形态
- 1.空二叉树
- 2.只有一个根结点
- 3.根结点只有左子树
- 4.根结点只有右子树
- 5.根结点既有左子树又有右子树
- 如图:
1.3. 特殊二叉树
如下思维导图:
- 注意:1.满二叉树一定是一颗完全二叉树,但完全二叉树不一定是满二叉树
1.4. 二叉树的性质
性质一:第 i 层最多 2^(i-1) 个结点(i≥1)
性质二:深度为 k 的二叉树最多 2^k-1 个结点。
性质三:终端结点数 n₀ = 度为 2 的结点数 n₂ + 1(n₀ = n₂ + 1)。终端结点数 n₀ = 度为 2 的结点数 n₂ + 1(n₀ = n₂ + 1)。
性质四:具有n个结点的完全二叉树的深度为【log2^n】+1 ([X]表示不大于x的最大整数)
1.5. 二叉树的存储结构
1.5.1 二叉树顺序存储结构
- 二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系
- 对于不存在的结点设置为‘^’而已,例如:
1.5.2 二叉链表
二叉树的每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表
- 其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
以下是我们二叉链表的结点结构定义代码
/*二叉树的二又链表结点结构定义*/
typedefstruct BiTNode /*结点结构
{
TElemType data;/*结点数据*/
struct BiTNode *lchild,*rchild;/*左右孩子指针 */
}BiTNode,*Biree;
结构如下图:
二,遍历二叉树及其算法
2.1. 二叉树遍历原理
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次,且仅被访问一次
- 这里的关键词为:访问和次序
2.2. 二叉树的遍历方法
2.2.1. 前序遍历
规则:
若二叉树为空,则空操作返回
否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
遍历顺序为:ABDGHCEIF
2.2.2. 中序遍历
规则:
若二叉树为空,则空操作返回
否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树
2.2.3. 后序遍历
规则:
若二叉树为空,则空操作返回
否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点
2.2.4. 层序遍历
规则:
若二叉树为空,则空操作返回
否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
2.3 遍历算法
二叉树的定义是用递归的方式,所有,遍历算法也采用递归
代码示例:
//二叉树结点定义
//#include <stdio.h>
//#include <stdlib.h>
//
定义二叉树结点结构
//typedef char TElemType; // 结点数据类型(示例为字符型,可替换为其他类型)
//typedef struct BiTNode {
// TElemType data; // 数据域
// struct BiTNode* lchild; // 左孩子指针
// struct BiTNode* rchild; // 右孩子指针
//} BiTNode, * BiTree;
//前序遍历算法(根→左→右)
/*算法思想:
1.若二叉树为空,直接返回。
2.访问根结点(打印或处理数据)。
3.递归遍历左子树。
4.递归遍历右子树。*/
// 前序遍历递归算法
//void PreOrderTraverse(BiTree T)
//{
// if (T == NULL) return; // 空树返回
// printf("%c ", T->data); // 访问根结点(此处为打印,可替换为其他操作)
// PreOrderTraverse(T->lchild); // 递归遍历左子树
// PreOrderTraverse(T->rchild); // 递归遍历右子树
//}
//中序遍历算法(左→根→右)
/*
算法思想:
若二叉树为空,直接返回。
递归遍历左子树。
访问根结点(打印或处理数据)。
递归遍历右子树。*/
// 中序遍历递归算法
//void InOrderTraverse(BiTree T) {
// if (T == NULL) return; // 空树返回
// InOrderTraverse(T->lchild); // 递归遍历左子树
// printf("%c ", T->data); // 访问根结点
// InOrderTraverse(T->rchild); // 递归遍历右子树
//}
//后序遍历算法
/*若二叉树为空,直接返回。
递归遍历左子树。
递归遍历右子树。
访问根结点(打印或处理数据)*/
// 后序遍历递归算法
//void PostOrderTraverse(BiTree T)
//{
// if (T == NULL) return; // 空树返回
// PostOrderTraverse(T->lchild); // 递归遍历左子树
// PostOrderTraverse(T->rchild); // 递归遍历右子树
// printf("%c ", T->data); // 访问根结点
//}
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树结点结构
typedef char TElemType; // 结点数据类型(示例为字符型,可替换为其他类型)
typedef struct BiTNode {
TElemType data; // 数据域
struct BiTNode* lchild; // 左孩子指针
struct BiTNode* rchild; // 右孩子指针
} BiTNode, * BiTree;
// 函数声明
void CreateBiTree(BiTree* T);
void PreOrderTraverse(BiTree T);
void InOrderTraverse(BiTree T);
void PostOrderTraverse(BiTree T);
void DestroyBiTree(BiTree* T);
// 创建二叉树(按扩展前序序列输入,#表示空结点)
void CreateBiTree(BiTree* T) {
TElemType ch;
scanf_s(" %c", &ch); // 注意空格处理换行符
if (ch == '#') {
*T = NULL;
}
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch;
CreateBiTree(&(*T)->lchild); // 递归创建左子树
CreateBiTree(&(*T)->rchild); // 递归创建右子树
}
}
// 前序遍历递归算法
void PreOrderTraverse(BiTree T) {
if (T == NULL) return; // 空树返回
printf("%c ", T->data); // 访问根结点
PreOrderTraverse(T->lchild); // 递归遍历左子树
PreOrderTraverse(T->rchild); // 递归遍历右子树
}
// 中序遍历递归算法
void InOrderTraverse(BiTree T) {
if (T == NULL) return; // 空树返回
InOrderTraverse(T->lchild); // 递归遍历左子树
printf("%c ", T->data); // 访问根结点
InOrderTraverse(T->rchild); // 递归遍历右子树
}
// 后序遍历递归算法
void PostOrderTraverse(BiTree T) {
if (T == NULL) return; // 空树返回
PostOrderTraverse(T->lchild); // 递归遍历左子树
PostOrderTraverse(T->rchild); // 递归遍历右子树
printf("%c ", T->data); // 访问根结点
}
// 释放二叉树内存
void DestroyBiTree(BiTree* T) {
if (*T != NULL) {
DestroyBiTree(&(*T)->lchild); // 递归释放左子树
DestroyBiTree(&(*T)->rchild); // 递归释放右子树
free(*T); // 释放当前结点
*T = NULL; // 防止野指针
}
}
int main() {
BiTree T = NULL;
printf("输入扩展前序序列(#表示空结点): ");
CreateBiTree(&T); // 例如输入:AB#D##C##(对应书中示例树)
printf("\n前序遍历结果: ");
PreOrderTraverse(T);
printf("\n中序遍历结果: ");
InOrderTraverse(T);
printf("\n后序遍历结果: ");
PostOrderTraverse(T);
// 释放内存
DestroyBiTree(&T);
printf("\n二叉树内存已释放\n");
return 0;
}
三,线索二叉树
1.定义:指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应地二叉树就称为线索二叉树
由于这样,我们分不清lchild是指向前继还是左孩子,rchild是指向后继还是右孩子
故我们增加新的两个标志域ltag和rtag,用来存放布尔型变量。如下图:
2.线索二叉树结构实现
#include <stdio.h>
#include <stdlib.h>
// 定义线索二叉树结点结构
typedef char TElemType;
typedef struct BiThrNode {
TElemType data; // 数据域
struct BiThrNode* lchild, * rchild; // 左右指针
int ltag, rtag; // 左右标志(0:孩子,1:线索)
} BiThrNode, * BiThrTree;
BiThrTree pre = NULL; // 全局前驱结点指针
// 创建普通二叉树(按扩展前序序列输入,#表示空结点)
void CreateBiTree(BiThrTree* T) {
TElemType ch;
scanf_s(" %c", &ch); // 注意空格处理前导空白
if (ch == '#') {
*T = NULL;
}
else {
*T = (BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data = ch;
(*T)->ltag = 0; // 初始默认指向孩子
(*T)->rtag = 0;
CreateBiTree(&(*T)->lchild); // 递归创建左子树
CreateBiTree(&(*T)->rchild); // 递归创建右子树
}
}
// 中序线索化二叉树(递归函数)
void InThreading(BiThrTree T) {
if (T) {
InThreading(T->lchild); // 递归线索化左子树
// 处理当前结点的前驱线索
if (!T->lchild) {
T->ltag = 1;
T->lchild = pre;
}
// 处理前驱结点的后继线索
if (pre && !pre->rchild) {
pre->rtag = 1;
pre->rchild = T;
}
pre = T; // 更新前驱结点
InThreading(T->rchild); // 递归线索化右子树
}
}
// 创建中序线索二叉树(调用入口)
BiThrTree CreateInThreadedBiTree() {
BiThrTree T, head;
// 创建头结点
head = (BiThrNode*)malloc(sizeof(BiThrNode));
head->ltag = 0; // lchild指向根结点
head->rtag = 1; // rchild指向遍历的最后一个结点
head->rchild = head; // 初始时指向自己
printf("请输入扩展前序序列(#表示空结点): ");
CreateBiTree(&T); // 创建普通二叉树
if (!T) { // 空树处理
head->lchild = head;
}
else {
head->lchild = T; // 头结点lchild指向根
pre = head;
InThreading(T); // 中序线索化
// 处理最后一个结点
pre->rchild = head; // 修正:添加缺失的分号
pre->rtag = 1; // 修正:添加缺失的分号
head->rchild = pre; // 头结点的rchild指向中序遍历的最后一个结点
}
return head;
}
// 中序遍历线索二叉树(非递归)
void InOrderTraverse_Thr(BiThrTree T) {
BiThrTree p = T->lchild; // p指向根结点
while (p != T) { // 循环直到回到头结点
// 找到最左结点(中序遍历的第一个结点)
while (p->ltag == 0) {
p = p->lchild;
}
printf("%c ", p->data); // 访问结点
// 沿后继线索访问后续结点
while (p->rtag == 1 && p->rchild != T) {
p = p->rchild;
printf("%c ", p->data);
}
p = p->rchild; // 转向右子树
}
}
// 释放线索二叉树的内存
void DestroyBiTree(BiThrTree* T) {
if (*T && (*T)->ltag == 0) { // 如果不是头结点且有左孩子
DestroyBiTree(&(*T)->lchild); // 递归释放左子树
}
if (*T && (*T)->rtag == 0) { // 如果不是头结点且有右孩子
DestroyBiTree(&(*T)->rchild); // 递归释放右子树
}
free(*T); // 释放当前结点
*T = NULL;
}
int main() {
BiThrTree T;
T = CreateInThreadedBiTree(); // 创建中序线索二叉树
printf("\n中序遍历线索二叉树结果: ");
InOrderTraverse_Thr(T); // 中序遍历线索二叉树
DestroyBiTree(&T); // 释放内存
printf("\n二叉树内存已释放。\n");
return 0;
}
四,赫尔曼树及其应用
1.原理
基本概念
赫尔曼树(Huffman Tree)是一种最优二叉树,其核心目标是最小化带权路径长度(WPL)。带权路径长度定义为所有叶子节点的权值(如字符频率)与其到根节点路径长度的乘积之和。构建步骤
初始化:将每个字符视为一个节点,权值为其频率,构成森林。
贪心合并:重复选择权值最小的两个节点,合并为新节点(权值为二者之和),直到只剩一棵树。
生成结构:最终形成的树中,权值高的节点靠近根,权值低的远离根。
特性
最优性:生成的树具有最小的带权路径长度。
前缀编码:字符编码唯一且无歧义(任一编码不是另一编码的前缀)。
不唯一性:相同权值节点合并顺序不同可能导致不同树结构,但WPL相同。
2.应用
数据压缩
哈夫曼编码:将高频字符用短编码表示,低频字符用长编码,显著减少存储空间(如ZIP、GIF、JPEG等格式)。
文本压缩:适用于重复性高的文本文件,压缩效率高且无损。
通信传输
编码优化:在网络协议或通信中减少数据传输量(如HTTP/2的头部压缩)。
实时传输:结合动态哈夫曼编码适应数据流变化(需频率统计更新)。
文件系统
存储优化:在文件系统中压缩重复数据,提升存储利用率。
其他领域
图像/音频压缩:如PNG(图像)、MP3(音频)的部分压缩阶段。
决策树设计:在机器学习中构建高效分类路径。
3.优缺点
优点:编码效率高、实现简单、适用于静态数据。
缺点:需预先统计频率(静态编码),动态数据需额外处理(如自适应哈夫曼编码)。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
typedef struct HTNode {
int weight;
int parent, lchild, rchild;
} HTNode, * HuffmanTree;
typedef char** HuffmanCode;
void Select(HuffmanTree HT, int n, int* s1, int* s2) {
int min1 = INT_MAX, min2 = INT_MAX;
*s1 = *s2 = 0;
// 寻找第一个最小值
for (int i = 1; i <= n; ++i) {
if (HT[i].parent == 0 && HT[i].weight < min1) {
min1 = HT[i].weight;
*s1 = i;
}
}
// 寻找第二个最小值(排除已选节点)
for (int i = 1; i <= n; ++i) {
if (HT[i].parent == 0 && i != *s1 && HT[i].weight < min2) {
min2 = HT[i].weight;
*s2 = i;
}
}
}
void CreateHuffmanTree(HuffmanTree* HT, int* w, int n) {
if (n <= 1) return;
int m = 2 * n - 1;
*HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));
// 初始化叶子节点
for (int i = 1; i <= n; ++i) {
(*HT)[i].weight = w[i - 1];
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
// 初始化非叶子节点
for (int i = n + 1; i <= m; ++i) {
(*HT)[i].weight = 0;
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
// 构建树
for (int i = n + 1; i <= m; ++i) {
int s1, s2;
Select(*HT, i - 1, &s1, &s2);
(*HT)[s1].parent = i;
(*HT)[s2].parent = i;
(*HT)[i].lchild = s1;
(*HT)[i].rchild = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode* HC, int n) {
*HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
char* cd = (char*)malloc(n * sizeof(char));
cd[n - 1] = '\0';
for (int i = 1; i <= n; ++i) {
int start = n - 1;
for (int c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent) {
cd[--start] = (HT[f].lchild == c) ? '0' : '1';
}
(*HC)[i] = (char*)malloc((n - start) * sizeof(char));
strcpy_s((*HC)[i],n-start, &cd[start]); // 改用标准strcpy
}
free(cd);
}
int WPL(HuffmanTree HT, int n) {
int wpl = 0;
for (int i = 1; i <= n; ++i) {
int depth = 0;
for (int c = i; HT[c].parent != 0; c = HT[c].parent, ++depth);
wpl += HT[i].weight * depth;
}
return wpl;
}
void Decode(HuffmanTree HT, char* code, int root, char* result) {
int p = root, index = 0;
size_t len = strlen(code);
for (size_t i = 0; i < len; ++i) {
p = (code[i] == '0') ? HT[p].lchild : HT[p].rchild;
// 检查节点有效性
if (p < 1 || p > root) {
fprintf(stderr, "Invalid code at position %zu\n", i);
result[0] = '\0';
return;
}
if (HT[p].lchild == 0 && HT[p].rchild == 0) {
result[index++] = 'A' + (p - 1);
p = root; // 重置到根节点继续解码
}
}
result[index] = '\0';
}
void PrintHuffmanTree(HuffmanTree HT, int m) {
printf("\nHuffman Tree Structure (index starts from 1):\n");
printf("Index\tWeight\tParent\tLeft\tRight\n");
for (int i = 1; i <= m; ++i) {
printf("%d\t%d\t%d\t%d\t%d\n",
i, HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild);
}
}
int main() {
int w[] = { 5, 9, 12, 13, 16 };
int n = sizeof(w) / sizeof(w[0]);
HuffmanTree HT;
HuffmanCode HC;
CreateHuffmanTree(&HT, w, n);
PrintHuffmanTree(HT, 2 * n - 1);
CreateHuffmanCode(HT, &HC, n);
printf("\nHuffman Codes:\n");
for (int i = 1; i <= n; ++i) {
printf("Char %c: %s\n", 'A' + i - 1, HC[i]);
}
printf("\nWPL = %d\n", WPL(HT, n));
char code[] = "0110";
char result[100];
Decode(HT, code, 2 * n - 1, result);
printf("\nDecode '%s' => %s\n", code, result);
// 释放内存
for (int i = 1; i <= n; ++i) free(HC[i]);
free(HC);
free(HT);
return 0;
}