当前位置: 首页 > news >正文

数据结构:树与二叉树

📌目录

  • 🌳 一,树与二叉树的定义
    • (一)树的定义
    • (二)树的基本术语
    • (三)二叉树的定义
  • 🌰 二,案例引入
    • 场景1:文件系统的目录结构
    • 场景2:决策树模型
  • 📋 三,树与二叉树的抽象数据类型定义
    • 树的ADT定义
    • 二叉树的ADT定义
  • 🔑 四,二叉树的性质和存储结构
    • (一)二叉树的性质
    • (二)二叉树的存储结构
      • 1. 顺序存储结构
      • 2. 链式存储结构(二叉链表)
  • 🔍 五,遍历二叉树与线索二叉树
    • (一)遍历二叉树
      • 1. 先序遍历(Pre-order)
      • 2. 中序遍历(In-order)
      • 3. 后序遍历(Post-order)
      • 4. 层序遍历(Level-order)
    • (二)线索二叉树
  • 🌳 六,树与森林
    • (一)树的存储结构
      • 1. 双亲表示法
      • 2. 孩子表示法
      • 3. 孩子兄弟表示法(二叉树表示法)
    • (二)森林与二叉树的转换
    • (三)树和森林的遍历
  • 📊 七,哈夫曼树及其应用
    • (一)哈夫曼树的基本概念
    • (二)哈夫曼树的构造算法
      • 算法步骤
      • 示例(权值3、4、5、6)
      • 代码实现(基于最小堆)
    • (三)哈夫曼编码
      • 编码规则
      • 示例(字符A(3)、B(4)、C(5)、D(6))
      • 优势
      • 解码过程
  • 🛠️ 八,案例分析与实现
    • 案例:哈夫曼编码压缩系统
      • 实现步骤
      • 核心代码
      • 效果分析
  • 📝 章结


🌳 一,树与二叉树的定义

树是一种非线性数据结构,它通过层次化的关系组织数据,广泛应用于文件系统、数据库索引、人工智能等领域。与线性表的“一对一”关系不同,树的核心是“一对多”的层级关系。

(一)树的定义

树(Tree)是由n(n≥0)个节点组成的有限集合。当n=0时,称为空树;当n>0时,满足:

  • 有且仅有一个特定的节点称为根(Root),它没有前驱节点;
  • 其余节点可分为m(m≥0)个互不相交的有限集合T₁,T₂,…,Tₘ,每个集合本身又是一棵树,称为根的子树(Subtree)

示例:一个家族树中,祖父是根节点,父亲和叔叔是祖父的子节点,而父亲的子女是其下的子树节点。树的定义具有递归性——子树也是树。

(二)树的基本术语

  • 节点的度(Degree):节点拥有的子树数量。例如,根节点若有3个子树,则度为3。
  • 叶子节点(Leaf):度为0的节点(无子女),又称终端节点。
  • 分支节点:度不为0的节点,又称非终端节点(根节点除外的分支节点称为内部节点)。
  • 树的度:所有节点中度的最大值。
  • 层次(Level):从根开始计数,根为第1层,根的子女为第2层,以此类推。
  • 深度(Depth):树中节点的最大层次数(从根到最远叶子的层数)。
  • 有序树与无序树:若子树的顺序有意义(如左子树≠右子树),则为有序树;否则为无序树。
  • 森林:m(m≥0)棵互不相交的树的集合(可理解为“去掉根的树”)。

(三)二叉树的定义

二叉树(Binary Tree)是一种特殊的树,它的每个节点最多有两棵子树,且子树有左右之分(左子树和右子树,顺序不可颠倒)。

二叉树的递归定义:

  • 空树是二叉树;
  • 若存在一个节点,其左子树和右子树都是二叉树,则该结构为二叉树。

特殊二叉树类型

  • 满二叉树:除叶子节点外,每个节点都有左右两棵子树,且所有叶子节点在同一层次(如深度为3的满二叉树有7个节点)。
  • 完全二叉树:深度为k的二叉树,前k-1层为满二叉树,第k层的节点从左到右连续排列(缺一不可),适合顺序存储。

🌰 二,案例引入

场景1:文件系统的目录结构

电脑中的文件系统是典型的树结构:

  • 根目录(如Windows的C:\)是根节点;
  • 文件夹是分支节点,包含子文件夹或文件;
  • 具体文件是叶子节点,没有子节点。
    通过树结构,用户可高效地层级化管理文件(如“文档→2023→报告.docx”)。

场景2:决策树模型

在AI分类任务中,决策树通过层层判断实现分类:

  • 根节点是初始特征(如“天气是否晴朗”);
  • 分支节点是中间判断条件(如“温度是否高于25℃”);
  • 叶子节点是分类结果(如“适合户外活动”或“不适合”)。
    树的层次结构模拟了人类的决策逻辑。

这些案例表明,树结构擅长表达具有层级关系或决策流程的数据,其效率远高于线性表。

📋 三,树与二叉树的抽象数据类型定义

树的ADT定义

ADT Tree {数据:节点集合,根节点唯一,其余节点分属子树,存在层次关系。操作:1. InitTree(&T):初始化空树T。2. CreateTree(&T):按指定方式创建树T。3. Root(T):返回树T的根节点。4. Parent(T, x):返回节点x的父节点。5. Child(T, x, i):返回节点x的第i个子节点。6. InsertChild(&T, x, i, c):将子树c插入为x的第i个子树。7. DeleteChild(&T, x, i):删除x的第i个子树。8. TraverseTree(T):遍历树T的所有节点。9. DestroyTree(&T):销毁树T。
}

二叉树的ADT定义

ADT BinaryTree {数据:节点集合,每个节点最多有左、右两棵子树,子树有序。操作:1. InitBiTree(&T):初始化空二叉树T。2. CreateBiTree(&T):创建二叉树T。3. BiRoot(T):返回二叉树T的根节点。4. LeftChild(T, x):返回节点x的左子节点。5. RightChild(T, x):返回节点x的右子节点。6. InsertLeftChild(&T, x, c):为x插入左子树c。7. InsertRightChild(&T, x, c):为x插入右子树c。8. PreOrderTraverse(T):先序遍历二叉树。9. InOrderTraverse(T):中序遍历二叉树。10. PostOrderTraverse(T):后序遍历二叉树。11. LevelOrderTraverse(T):层序遍历二叉树。12. DestroyBiTree(&T):销毁二叉树T。
}

🔑 四,二叉树的性质和存储结构

(一)二叉树的性质

性质1:在二叉树的第i层上,最多有2ⁱ⁻¹个节点(i≥1)。

  • 示例:第1层最多1个节点(2⁰),第2层最多2个节点(2¹),第3层最多4个节点(2²)。

性质2:深度为k的二叉树最多有2ᵏ - 1个节点(k≥1)。

  • 满二叉树是该性质的特例(如深度为3的满二叉树有2³ - 1 = 7个节点)。

性质3:对任意二叉树,若叶子节点数为n₀,度为2的节点数为n₂,则n₀ = n₂ + 1。

  • 推导:总节点数n = n₀ + n₁ + n₂(n₁为度1的节点数),总边数n-1 = n₁ + 2n₂,联立得n₀ = n₂ + 1。

性质4:具有n个节点的完全二叉树的深度为⌊log₂n⌋ + 1(⌊x⌋表示向下取整)。

  • 示例:n=7时,深度为⌊log₂7⌋ + 1 = 2 + 1 = 3。

性质5:对完全二叉树中编号为i的节点(从1开始):

  • 若i>1,则父节点编号为⌊i/2⌋;
  • 左子节点编号为2i(若2i≤n);
  • 右子节点编号为2i+1(若2i+1≤n)。

(二)二叉树的存储结构

1. 顺序存储结构

适合完全二叉树,用数组按层序存储节点,通过性质5的公式计算父子节点位置。

  • 存储表示(C语言):

    #define MAXSIZE 100
    typedef char TElemType;
    typedef struct {TElemType data[MAXSIZE];  // 存储节点数据int length;               // 节点总数
    } SqBiTree;
    
  • 优势:空间紧凑,访问父子节点高效(O(1));

  • 劣势:非完全二叉树会浪费大量空间(需用特殊值填充空缺节点)。

2. 链式存储结构(二叉链表)

每个节点包含数据域和两个指针域(分别指向左、右子节点),适合任意二叉树。

  • 存储表示(C语言):

    typedef char TElemType;
    typedef struct BiTNode {TElemType data;                // 数据域struct BiTNode *lchild, *rchild; // 左、右子指针
    } BiTNode, *BiTree;
    
  • 示例:创建一个简单二叉树节点:

    BiTree CreateNode(TElemType x) {BiTNode *node = (BiTNode*)malloc(sizeof(BiTNode));node->data = x;node->lchild = NULL;node->rchild = NULL;return node;
    }// 构建一棵二叉树:    A
    //                  /   \
    //                 B     C
    BiTree BuildBiTree() {BiTree root = CreateNode('A');root->lchild = CreateNode('B');root->rchild = CreateNode('C');return root;
    }
    
  • 优势:空间利用率高,插入删除灵活;

  • 劣势:访问父节点需遍历(可扩展为三叉链表,增加父指针域)。

🔍 五,遍历二叉树与线索二叉树

(一)遍历二叉树

遍历是按某种规则访问二叉树的所有节点,且每个节点仅访问一次。二叉树的遍历基于递归定义,核心有4种方式:

1. 先序遍历(Pre-order)

顺序:根节点 → 左子树 → 右子树(根左右)。
递归实现

void PreOrderTraverse(BiTree T) {if (T == NULL) return;printf("%c ", T->data);       // 访问根节点PreOrderTraverse(T->lchild);  // 遍历左子树PreOrderTraverse(T->rchild);  // 遍历右子树
}

示例:对树 A(B(D,E),C(,F)) 先序遍历结果为:A B D E C F。

2. 中序遍历(In-order)

顺序:左子树 → 根节点 → 右子树(左根右)。
递归实现

void InOrderTraverse(BiTree T) {if (T == NULL) return;InOrderTraverse(T->lchild);   // 遍历左子树printf("%c ", T->data);       // 访问根节点InOrderTraverse(T->rchild);   // 遍历右子树
}

示例:上述树的中序遍历结果为:D B E A C F。

3. 后序遍历(Post-order)

顺序:左子树 → 右子树 → 根节点(左右根)。
递归实现

void PostOrderTraverse(BiTree T) {if (T == NULL) return;PostOrderTraverse(T->lchild); // 遍历左子树PostOrderTraverse(T->rchild); // 遍历右子树printf("%c ", T->data);       // 访问根节点
}

示例:上述树的后序遍历结果为:D E B F C A。

4. 层序遍历(Level-order)

顺序:按层次从左到右访问节点(需借助队列实现)。
算法步骤

  1. 根节点入队;
  2. 队不空时,出队一个节点并访问;
  3. 若该节点有左子节点,入队;
  4. 若该节点有右子节点,入队;
  5. 重复步骤2-4直至队空。

实现代码

void LevelOrderTraverse(BiTree T) {if (T == NULL) return;BiTNode *queue[MAXSIZE];  // 用数组模拟队列int front = 0, rear = 0;queue[rear++] = T;        // 根节点入队while (front != rear) {BiTNode *p = queue[front++];  // 出队printf("%c ", p->data);       // 访问节点if (p->lchild) queue[rear++] = p->lchild;  // 左子节点入队if (p->rchild) queue[rear++] = p->rchild;  // 右子节点入队}
}

示例:上述树的层序遍历结果为:A B C D E F。

(二)线索二叉树

二叉链表中存在大量空指针(n个节点有n+1个空指针),线索二叉树利用这些空指针存储遍历序列中的前驱/后继信息,提高遍历效率。

  • 线索:将空的左指针改为指向遍历前驱,空的右指针改为指向遍历后继,称为线索。
  • 线索二叉树结构:在二叉链表基础上增加两个标志位(ltag/rtag):
    • ltag=0:lchild指向左子树;ltag=1:lchild指向前驱;
    • rtag=0:rchild指向右子树;rtag=1:rchild指向后继。
typedef struct ThreadNode {TElemType data;struct ThreadNode *lchild, *rchild;int ltag, rtag;  // 线索标志位
} ThreadNode, *ThreadTree;
  • 优势:无需递归或栈即可实现遍历,节省空间,适合频繁遍历的场景。

🌳 六,树与森林

(一)树的存储结构

1. 双亲表示法

用数组存储节点,每个节点记录数据和父节点下标,适合查询父节点的场景。

#define MAX_TREE_SIZE 100
typedef struct {TElemType data;int parent;  // 父节点下标(-1表示无父节点)
} PTNode;typedef struct {PTNode nodes[MAX_TREE_SIZE];int r;  // 根节点下标int n;  // 节点总数
} PTree;

2. 孩子表示法

数组+链表结合:数组存储节点,每个节点通过链表指向所有子节点,适合查询子节点的场景。

3. 孩子兄弟表示法(二叉树表示法)

每个节点包含数据、第一个孩子指针和右兄弟指针,可将树转换为二叉树(左孩子右兄弟)。

typedef struct CSNode {TElemType data;struct CSNode *firstchild, *nextsibling;  // 第一个孩子、右兄弟
} CSNode, *CSTree;

(二)森林与二叉树的转换

森林与二叉树可相互转换,核心规则:

  • 树→二叉树:根节点的左子树为第一个孩子,右子树为兄弟节点;
  • 森林→二叉树:第一棵树转换为二叉树,其余树的根节点作为前一棵树的右子树依次连接;
  • 二叉树→森林:根节点的左子树为第一棵树,右子树递归转换为其余森林。

(三)树和森林的遍历

  • 树的遍历
    • 先根遍历:先访问根节点,再依次先根遍历各子树;
    • 后根遍历:先依次后根遍历各子树,再访问根节点。
  • 森林的遍历
    • 先序遍历:先序遍历第一棵树,再先序遍历其余森林;
    • 中序遍历:中序遍历第一棵树,再中序遍历其余森林。

📊 七,哈夫曼树及其应用

(一)哈夫曼树的基本概念

哈夫曼树(Huffman Tree)又称最优二叉树,是一类带权路径长度(WPL)最小的二叉树。

  • 关键术语
    • 路径:从一个节点到另一个节点的分支序列(如根节点到叶子节点的路径);
    • 路径长度:路径上的分支数(根到第k层节点的路径长度为k-1);
    • 节点的权:赋予节点的数值(如字符出现的频率);
    • 带权路径长度(WPL):树中所有叶子节点的权值与路径长度的乘积之和,公式为:
      WPL = Σ(ωᵢ × lᵢ)(ωᵢ为第i个叶子节点的权值,lᵢ为其路径长度)。

示例
假设有4个叶子节点,权值分别为3、4、5、6,两种二叉树结构的WPL计算如下:

  • 结构1(不平衡):WPL = 3×3 + 4×3 + 5×2 + 6×1 = 9 + 12 + 10 + 6 = 37;
  • 结构2(哈夫曼树):WPL = 3×2 + 4×2 + 5×2 + 6×2 = 6 + 8 + 10 + 12 = 36。
    显然结构2的WPL更小,为最优结构。

(二)哈夫曼树的构造算法

哈夫曼树的构造遵循贪心策略,核心是通过反复合并权值最小的子树,最终形成最优树。

算法步骤

  1. 初始化:将所有权值节点视为独立的单节点树(只有根节点,无左右子树),组成森林F;
  2. 合并子树:从F中选择两棵权值最小的树T₁和T₂,创建新节点作为它们的父节点,新节点的权值为T₁和T₂的权值之和;
  3. 更新森林:将T₁和T₂从F中移除,将新树加入F;
  4. 重复步骤2-3:直到F中只剩下一棵树,该树即为哈夫曼树。

示例(权值3、4、5、6)

  • 第1次合并:选择3和4,新节点权值7,森林变为{5,6,7};
  • 第2次合并:选择5和6,新节点权值11,森林变为{7,11};
  • 第3次合并:选择7和11,新节点权值18,森林只剩{18},构造完成。

代码实现(基于最小堆)

#include <stdio.h>
#include <stdlib.h>typedef struct {int weight;          // 节点权值int parent, lchild, rchild; // 父节点、左/右子节点下标
} HTNode, *HuffmanTree;// 构建哈夫曼树
void CreateHuffmanTree(HuffmanTree &HT, int n) {if (n <= 1) return;int m = 2 * n - 1;   // 哈夫曼树总节点数HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); // 下标从1开始for (int i = 1; i <= m; i++) { // 初始化HT[i].parent = 0;HT[i].lchild = 0;HT[i].rchild = 0;}for (int i = 1; i <= n; i++) { // 输入叶子节点权值scanf("%d", &HT[i].weight);}// 合并子树for (int i = n + 1; i <= m; i++) {// 选择两个权值最小的无父节点的节点int s1, s2;// 找到第一个最小节点s1for (int j = 1; j < i; j++) {if (HT[j].parent == 0) {s1 = j;break;}}for (int j = 1; j < i; j++) {if (HT[j].parent == 0 && HT[j].weight < HT[s1].weight) {s1 = j;}}// 找到第二个最小节点s2(s2≠s1)for (int j = 1; j < i; j++) {if (HT[j].parent == 0 && j != s1) {s2 = j;break;}}for (int j = 1; j < i; j++) {if (HT[j].parent == 0 && j != s1 && HT[j].weight < HT[s2].weight) {s2 = j;}}// 合并s1和s2HT[s1].parent = i;HT[s2].parent = i;HT[i].lchild = s1;HT[i].rchild = s2;HT[i].weight = HT[s1].weight + HT[s2].weight;}
}

(三)哈夫曼编码

哈夫曼编码是哈夫曼树的典型应用,用于数据压缩,通过变长编码实现无损压缩(频率高的字符用短编码)。

编码规则

  • 从哈夫曼树的根节点出发,左分支标记为“0”,右分支标记为“1”;
  • 每个叶子节点(对应字符)的编码为从根到该节点的路径上的标记序列。

示例(字符A(3)、B(4)、C(5)、D(6))

  • A的路径:根→7→3(左→左),编码为“00”;
  • B的路径:根→7→4(左→右),编码为“01”;
  • C的路径:根→11→5(右→左),编码为“10”;
  • D的路径:根→11→6(右→右),编码为“11”。

优势

  • 编码唯一:哈夫曼树中叶子节点的编码互不前缀(前缀编码特性),避免解码歧义;
  • 压缩高效:频率高的字符编码短,总编码长度最短。

解码过程

根据哈夫曼树,从根节点开始,按编码序列遍历:遇到“0”走左子树,遇到“1”走右子树,到达叶子节点即完成一个字符的解码,重复直至所有编码解析完毕。

🛠️ 八,案例分析与实现

案例:哈夫曼编码压缩系统

功能:对输入的文本进行哈夫曼编码压缩,并能通过编码和解码表还原文本。

实现步骤

  1. 统计文本中每个字符的出现频率(权值);
  2. 用频率构建哈夫曼树;
  3. 生成每个字符的哈夫曼编码;
  4. 用编码表将文本转换为二进制压缩串;
  5. 保存编码表和解码表,用于解压还原。

核心代码

#include <string.h>// 生成哈夫曼编码
void CreateHuffmanCode(HuffmanTree HT, char **&HC, int n) {HC = (char**)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;  // 编码从后往前存储int c = i;int f = HT[i].parent;while (f != 0) { // 从叶子节点回溯到根start--;if (HT[f].lchild == c) {cd[start] = '0'; // 左子树为0} else {cd[start] = '1'; // 右子树为1}c = f;f = HT[f].parent;}HC[i] = (char*)malloc((n - start) * sizeof(char)); // 分配编码空间strcpy(HC[i], &cd[start]); // 复制编码}free(cd);
}// 示例:压缩文本"ABACCD"
int main() {int n = 4; // 字符数:A、B、C、DHuffmanTree HT;CreateHuffmanTree(HT, n); // 构建哈夫曼树(权值3,4,5,6对应A,B,C,D)char **HC;CreateHuffmanCode(HT, HC, n); // 生成编码// 输出编码表printf("哈夫曼编码表:\n");printf("A: %s\n", HC[1]); // A: 00printf("B: %s\n", HC[2]); // B: 01printf("C: %s\n", HC[3]); // C: 10printf("D: %s\n", HC[4]); // D: 11// 压缩文本"ABACCD"char text[] = "ABACCD";printf("原文本:%s\n", text);printf("压缩编码:");for (int i = 0; i < strlen(text); i++) {switch (text[i]) {case 'A': printf("%s", HC[1]); break;case 'B': printf("%s", HC[2]); break;case 'C': printf("%s", HC[3]); break;case 'D': printf("%s", HC[4]); break;}}// 输出:压缩编码:000100101011// 释放内存free(HT);for (int i = 1; i <= n; i++) free(HC[i]);free(HC);return 0;
}

效果分析

原文本“ABACCD”共6个字符,若用ASCII编码需6×8=48位;
哈夫曼编码后为“000100101011”,共12位,压缩率达75%,体现了哈夫曼编码的高效性。

📝 章结

树与二叉树作为非线性数据结构的核心,以层次化关系突破了线性表的“一对一”限制,为表达复杂数据关系提供了强大工具:

  • 树与二叉树的本质:树通过“一对多”的层级关系建模现实中的层级结构(如文件系统),而二叉树通过限制子树数量(最多两棵)简化了操作实现,成为树结构的研究重点。二叉树的性质(如节点数与深度的关系)为存储和遍历提供了理论基础,链式存储(二叉链表)则是实现二叉树的主流方式。

  • 遍历的核心价值:先序、中序、后序遍历通过递归思想实现对二叉树的完整访问,层序遍历借助队列实现层次化访问,不同遍历方式适用于不同场景(如中序遍历可用于二叉搜索树的有序输出)。线索二叉树通过利用空指针存储前驱/后继信息,优化了遍历效率。

  • 树与森林的扩展:树的存储结构(双亲表示法、孩子兄弟表示法)需根据操作需求选择,而森林与二叉树的转换则架起了树与二叉树的桥梁,使得二叉树的算法可迁移到树和森林中。

  • 哈夫曼树的应用:哈夫曼树通过贪心策略构造最优二叉树,其衍生的哈夫曼编码利用变长编码实现高效数据压缩,体现了数据结构与算法结合解决实际问题的典型思路。

从层级建模到高效编码,树与二叉树的思想渗透在计算机科学的各个领域。理解它们的定义、性质和算法,不仅能解决具体问题,更能培养“层次化思维”和“优化意识”——这正是非线性数据结构的核心价值所在。

http://www.dtcms.com/a/326581.html

相关文章:

  • ARM基础概念 day51
  • easyExcel嵌套子集合导出Excel
  • 2025第十六届蓝桥杯大赛青少组省赛C++真题(初级组和中级组)
  • MCU的设计原理
  • SNMP入门教程:Windows下编译
  • Linux811 YUM;SHELL:if else fi,for
  • 进程线程切换的区别
  • 【k近邻】 K-Nearest Neighbors算法k值的选择
  • 第4节 大模型推理内存与计算优化
  • 【FreeRTOS】任务间通讯6: 任务通知- Task Notification
  • python+requests+yaml实现接口自动化用例
  • 【软件分享】格式工厂FormatFactory便携版本(解压即用)
  • 介绍一下jQuery的AJAX异步请求
  • Salesforce案例:零售企业会员积分体系
  • 新人如何简化学习Vue3文件
  • LangChain框架之 invoke() 方法
  • 【每日一错】PDB之间数据迁移
  • 7.Java的继承
  • 分享一款基于STC8H8K32U-45I-LQFP48单片机的4路数字量输入输出模块
  • 多重时间聚合算法(MAPA)改进需求预测模型
  • Redis实现排行榜
  • C++11 auto关键字:智能类型推导指南
  • 字符串相关例题(查询子串在主串中的个数)
  • GB17761-2024标准与电动自行车防火安全的技术革新
  • 编译GCC-12.1.0
  • Linux内核网络设备框架及其注册流程分析
  • Cursor 实用技巧与常见难题解析:从入门到进阶的技术实践
  • Pytest项目_day11(fixture、conftest)
  • OSPF IP FRR 理论和实验
  • 当C#遇上Notepad++:实现GCode可视化编辑的跨界实践