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

数据结构进阶 第六章 树与二叉树

第六章 树与二叉树

6.1 树、二叉树基本术语与性质

6.1.1 树的基本概念

树的特点
  • 层次结构:树具有层次结构,是一对多(1:m)的关系
  • 基本术语
    • 叶子(终端)结点:度为0的结点
    • 分支(非终端)结点:度不为0的结点
    • 层次、高度:结点的层次从根开始定义,根为第1层
    • 孩子、双亲、兄弟、祖先、子孙:结点间的关系
    • 有序树、无序树:是否考虑子树的顺序
树的基本性质
  1. 树中的结点数等于所有结点的度数加1n = B + 1
  2. 度为m的树中第i层上至多有m^(i-1)个结点(i≥1)
  3. 高度为h的m叉树至多有(m^h-1)/(m-1)个结点
  4. 具有n个结点的m叉树的最小高度为⌈log_m(n(m-1)+1)⌉

6.1.2 二叉树的定义与性质

二叉树的定义
  • 二叉树不是度为2的树:二叉树是有序树,左右子树有区别
  • 每个结点最多有两个孩子
  • 二叉树是有序树
二叉树的5个基本性质

性质1:在二叉树的第i层上至多有2^(i-1)个结点(i≥1)

性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)

性质3:对任意一棵二叉树T,若终端结点数为n₀,而其度数为2的结点数为n₂,则n₀ = n₂ + 1

证明过程

  • 设二叉树总结点数为n,度为1的结点数为n₁
  • 则:n = n₀ + n₁ + n₂
  • 边数关系:B = n - 1 = 2n₂ + n₁
  • 由此可得:n₀ = n₂ + 1

性质4:具有n个结点的完全二叉树的深度为⌊log₂n⌋ + 1

性质5:对于具有n个结点的完全二叉树,如果按照从上到下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点有:

  • 如果i = 1,则序号为i的结点是根结点,无双亲结点;如果i > 1,则序号为i的结点的双亲结点序号为⌊i/2⌋
  • 如果2×i > n,则序号为i的结点无左孩子;如果2×i ≤ n,则序号为i的结点的左孩子结点的序号为2×i
  • 如果2×i+1 > n,则序号为i的结点无右孩子;如果2×i+1 ≤ n,则序号为i的结点的右孩子结点的序号为2×i+1
满二叉树与完全二叉树

满二叉树:除最后一层外,每一层上的所有结点都有两个孩子结点

完全二叉树:除最后一层外,每一层都是满的,最后一层的结点都集中在左边

重要推论:完全二叉树上,度为1的结点个数是0或1个

  • 总结点个数为偶数,度为1的结点个数为1
  • 总结点个数为奇数,度为1的结点个数为0

6.1.3 练习题解析

题目1:一棵树的度为4,其中度为1,2,3,4的结点分别为5,4,2,2个,则该树中有(15)个叶子结点。

解答

  • 度为k的树中叶子结点个数公式:n₀ = (k-1)nₖ + (k-2)nₖ₋₁ + … + n₂ + 1
  • n₀ = 3×2 + 2×2 + 1×4 + 0×5 + 1 = 6 + 4 + 4 + 0 + 1 = 15

题目2:一棵完全二叉树,共666个结点。该二叉树共有(333)个叶子结点,(1)个度为1的结点,(332)个度为2的结点。

解答

  • 由于666是偶数,所以度为1的结点个数为1
  • n₀ = n₂ + 1,n₀ + n₁ + n₂ = 666
  • 得:n₀ = 333,n₁ = 1,n₂ = 332

6.2 二叉树存储及遍历

6.2.1 二叉树的存储结构

顺序存储
  • 将二叉树补为完全二叉树,从上到下,从左到右依次存储每个结点
  • 利用性质5来计算结点之间的关系
  • 空间效率问题:对于一般二叉树可能造成空间浪费
链式存储

二叉链表

typedef struct BiTNode {ElemType data;struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

三叉链表

typedef struct TriTNode {ElemType data;struct TriTNode *lchild, *rchild, *parent;
} TriTNode, *TriTree;

空链域分析

  • n个结点的二叉树用二叉链表表示,共有n+1个空链域
  • n个结点的二叉树用三叉链表表示,共有n+2个空链域

6.2.2 二叉树遍历

遍历方法
  • DLR(先序遍历):根→左→右
  • LDR(中序遍历):左→根→右
  • LRD(后序遍历):左→右→根
  • 层次遍历:逐层从左到右
递归遍历算法

中序遍历(LDR)

void LDR(BiTree root) {if (root != NULL) {LDR(root->LChild);Visit(root->data);LDR(root->RChild);}
}
层次遍历算法
int LayerOrder(BiTree bt) {Queue Q; BiTree p;InitQueue(&Q);if(bt == NULL) return ERROR;EnterQueue(&Q, bt);while (!IsEmpty(&Q)) {DeleteQueue(&Q, &p);visit(p->data);if(p->LChild) EnterQueue(&Q, p->LChild);if(p->RChild) EnterQueue(&Q, p->RChild);}return OK;
}
非递归遍历算法

先序遍历(非递归)

void preOrder(BiTree root) {Stack S;BiTree p = root;InitStack(S);while(p != NULL || !IsEmpty(S)) {if(p != NULL) {visit(p->data);  // 访问根结点Push(S, p);p = p->lchild;} else {Pop(S, &p);p = p->rchild;}}
}

中序遍历(非递归)

void InOrder(BiTree root) {Stack S;BiTree p = root;InitStack(S);while(p != NULL || !IsEmpty(S)) {if(p != NULL) {Push(S, p);p = p->lchild;} else {Pop(S, &p);visit(p->data);  // 出栈时访问p = p->rchild;}}
}

6.2.3 二叉树遍历的应用

重要应用场景
  1. 某二叉树采用三叉链表存放,但在建树时,忘记了给双亲域parent赋值:编写算法,给每个结点的parent赋值
  2. 假设Huffman树采用二叉链表存放:编写算法求该Huffman树的带权路径长度
  3. 设计算法,实现二叉链表存储的二叉树自上而下、从右到左的层次遍历
  4. 某二叉树采用顺序存储,编写算法对其进行后序遍历
  5. 判断某个顺序存储的完全二叉树是否为大根堆
  6. 给定一个先序和中序遍历序列,编写算法构建二叉链表存储的二叉树
由遍历序列构造二叉树

已知先序和中序序列构造二叉树的方法

  1. 先序序列的第一个元素是根结点
  2. 在中序序列中找到根结点,左边是左子树,右边是右子树
  3. 递归构造左子树和右子树
判断二叉搜索树
  • 方法:LDR遍历,如果得到递增序列,则是二叉搜索树
判断完全二叉树
  • 方法:层次遍历,空孩子也进队;遇到出队为空,跳出循环。如果队列中都为空结点,则是完全二叉树;否则不是
求二叉树最大宽度
  • 方法:层次遍历,统计每层结点个数

6.2.4 表达式树

表达式与二叉树的关系
  • 中缀表达式相当于对二叉树表达式进行中序遍历
  • 后缀表达式相当于对二叉树表达式进行后序遍历
中缀表达式转二叉树

算法思想

  1. 从当前传入的表达式data[start…end]中找出不在括号内、最靠后、优先级最低的一个运算符,作为二叉树的根节点
  2. 分别构建该二叉树的左子树data[start…mid-1]和右子树data[mid+1…end]
二叉树转中缀表达式(加括号)

算法思想:当根结点运算符优先级大于左子树或右子树根结点运算符时,相应左或右子树前需要加括号

void InOrder2(BiTree T) {int tag = 0;if (T == NULL) return;else if (T->LChild == NULL && T->RChild == NULL) cout << T->data;else {if (isOp(T->LChild->data) && compare(T->data, T->LChild->data) == 1) {cout << "("; tag = 1;}InOrder2(T->LChild);if (tag) cout << ")";cout << T->data;tag = 0;if (isOp(T->RChild->data) && compare(T->data, T->RChild->data) == 1) {cout << "("; tag = 1;}InOrder2(T->RChild);if (tag) cout << ")";}
}

6.3 线索二叉树

6.3.1 线索二叉树的概念

为什么需要线索?
  • 在二叉链表中,有大量的空指针域
  • 可以利用这些空指针域存储结点的前驱和后继信息
线索二叉树的结构
typedef struct ThreadNode {ElemType data;struct ThreadNode *lchild, *rchild;int ltag, rtag;  // 线索标志
} ThreadNode, *ThreadTree;

标志位说明

  • ltag = 0:lchild指向左孩子
  • ltag = 1:lchild指向前驱
  • rtag = 0:rchild指向右孩子
  • rtag = 1:rchild指向后继

6.3.2 线索二叉树中查找前驱后继

中序线索二叉树中找前驱
  1. 如果ltag = 1,则lchild就是前驱
  2. 如果ltag = 0,则前驱是左子树中最右边的结点
中序线索二叉树中找后继
  1. 如果rtag = 1,则rchild就是后继
  2. 如果rtag = 0,则后继是右子树中最左边的结点

6.3.3 线索化算法

void InThreadTree(ThreadTree root) {if (root != NULL) {InThreadTree(root->lchild);  // 线索化左子树// 处理当前结点if (root->lchild == NULL) {root->lchild = pre;root->ltag = 1;}if (pre != NULL && pre->rchild == NULL) {pre->rchild = root;pre->rtag = 1;}pre = root;  // 当前结点成为下一个结点的前驱InThreadTree(root->rchild);  // 线索化右子树}
}

6.4 树与二叉树的转换

6.4.1 树的存储形式

三种存储形式
  1. 双亲表示法:每个结点存储其双亲的位置
  2. 孩子链表示法:每个结点的所有孩子用链表连接
  3. 孩子-兄弟表示法:每个结点只保存第一个孩子和右兄弟

6.4.2 树和二叉树之间的转换

转换规则:左孩子右兄弟
  • 每个结点的第一个孩子作为其左孩子
  • 每个结点的兄弟结点作为其右孩子
遍历对应关系
  • 树的先根遍历二叉树的先序遍历
  • 树的后根遍历二叉树的中序遍历

6.4.3 森林和二叉树之间的转换

转换步骤
  1. 将森林中的每棵树转换为二叉树
  2. 将各二叉树的根结点视为兄弟关系,用右指针连接
遍历对应关系
  • 森林的先序遍历二叉树的先序遍历
  • 森林的中序遍历二叉树的中序遍历
  • 森林的后序遍历二叉树的后序遍历

6.4.4 练习题解析

题目:如图的二叉树是一个森林转化而来,则该森林中有(5)个叶子结点。

解答方法

  1. 将二叉树转换回森林
  2. 在森林中,叶子结点是那些既没有孩子又没有右兄弟的结点
  3. 在对应的二叉树中,这些结点的左右指针都为空

6.5 Huffman树与编码

6.5.1 基本概念

相关术语
  • 结点路径长度:从某结点到其子孙结点所经边的个数
  • 树的路径长度PL:所有结点的路径长度之和
  • 结点带权路径长度:从根结点到该结点的路径长度与该结点权值的乘积
  • 树的带权路径长度WPL:所有叶子结点的带权路径长度之和
Huffman树的定义
  • Huffman树是WPL最短的树
  • 前缀码:任意一个编码都不是其他编码的前缀
  • Huffman编码是最优前缀码
  • 编码规则:在Huffman树上按左0右1或左1右0的规则进行编码

6.5.2 Huffman树的构造算法

构造步骤
  1. 将所有权值作为叶子结点,构成森林
  2. 在森林中选择两个权值最小的根结点合并为一个新树
  3. 重复步骤2,直到森林中只有一棵树
重要性质
  • n个不同权值构成的Huffman树中没有度为1的结点
  • 树中两个最小的权值结点一定是兄弟结点
  • 任一非叶结点的权值一定不小于下一层任一结点的权值

6.5.3 Huffman编码

编码过程
  1. 构造Huffman树
  2. 从根到叶子的路径确定编码(左0右1)
  3. 每个字符的编码就是从根到该字符叶子结点的路径编码
译码过程
  • 从根结点开始,根据编码中的0和1沿着对应的左右分支走
  • 到达叶子结点时,输出该字符,然后回到根结点继续

6.5.4 练习题解析

题目1:以给定权值{1,3,6,7,9,14,20}作为叶子,构造WPL最短的二叉树,并计算其WPL。

解答

  1. 按照Huffman算法构造树
  2. 计算WPL = 1×4 + 3×4 + 6×3 + 7×3 + 9×2 + 14×2 + 20×1 = 122

题目2:一棵二叉哈夫曼树共有301个结点,对其进行哈夫曼编码,共能得到(151)种编码。

解答

  • Huffman树只有度为0和度为2的结点
  • 设叶子结点数为n₀,度为2的结点数为n₂
  • n₀ = n₂ + 1,n₀ + n₂ = 301
  • 得:n₀ = 151,n₂ = 150

6.5.5 应用实例

切割木板问题

问题:将长度为100的木板切割成长度为{2, 10, 15, 20, 25, 28}的木板,每次切割的代价是被切割木板的长度,求最小代价。

解法:使用Huffman树的思想,反向思考为合并问题

priority_queue<int, vector<int>, greater<int>> q;
for(int i = 0; i < n; i++) {cin >> t; q.push(t);
}
sum = 0;
while(q.size() > 1) {t1 = q.top(); q.pop();t2 = q.top(); q.pop();t = t1 + t2; sum += t; q.push(t);
}
cout << sum << endl;

6.6 并查集

6.6.1 并查集的概念

应用场景

经常对若干集合进行如下操作时,可采用并查集:

  • 合并两个子集
  • 查找某个元素属于哪个子集
  • 查找两个元素是否属于同一子集
存储方式
  • 将每个子集组织成树的形式,谁当根(家长)都可以
  • 若干个子集组织成森林的形式
  • 按照树的双亲表示法存储每一棵树

6.6.2 并查集的基本操作

数据结构
typedef struct {ElemType data;int parent;
} UFSTree;
Find操作(查找)
int Find_1(UFSet *S, int x) {while(S->tree[x].parent >= 0) {x = S->tree[x].parent;}return x;
}
Union操作(合并)
int Merge_1(UFSet *S, int root1, int root2) {if (root1 < 0 || root1 > S->nodenum-1) return ERROR;if (root2 < 0 || root2 > S->nodenum-1) return ERROR;S->tree[root2].parent = root1;return OK;
}

6.6.3 并查集的优化

路径压缩

在Find操作中,将查找路径上的所有结点都直接连到根结点上,减少树的高度。

int Find_2(UFSet *S, int x) {if (S->tree[x].parent < 0) return x;elsereturn S->tree[x].parent = Find_2(S, S->tree[x].parent);
}
按秩合并

总是将较小的树合并到较大的树上,避免树退化成链。

6.6.4 并查集的应用

  • 判断无向图的连通分量个数
  • 判断无向图是否连通
  • 判断无向图是否有环
  • Kruskal算法(最小生成树)

本章总结

重要知识点回顾

  1. 二叉树性质(推广到树)与存储(顺序、链式)
  2. 二叉树遍历算法(递归、非递归)
  3. 线索二叉树(线索化、找前驱后继)
  4. 二叉树与树、森林的转化
  5. 哈夫曼树(二叉、k叉)
  6. 并查集

考试重点

  • 二叉树的5个基本性质及其应用
  • 完全二叉树的结点编号规律
  • 各种遍历算法的实现(递归与非递归)
  • 线索二叉树的构造和应用
  • 树与二叉树的转换规则
  • Huffman树的构造和编码
  • 并查集的基本操作和优化

算法复杂度分析

  • 二叉树遍历:时间复杂度O(n),空间复杂度O(h),其中h为树的高度
  • Huffman树构造:时间复杂度O(nlogn)
  • 并查集操作:经过路径压缩和按秩合并优化后,平均时间复杂度接近O(1)

常见题型

  1. 根据遍历序列构造二叉树
  2. 完全二叉树相关计算题
  3. Huffman编码的构造和应用
  4. 树与森林的转换
  5. 并查集的应用场景识别

相关文章:

  • 地质公园网站建设怎么做seo网站关键词优化
  • 韩国男女做游戏视频网站不用流量的地图导航软件
  • 进网站后台显示空白竞价被恶意点击怎么办
  • 新钥匙建站雏鸟app网站推广
  • 1高端网站建设北京网络推广
  • 建设网站需要什么技术营销推广的方法有哪些
  • MongoDB 相关知识文档
  • YOLOv13:目标检测的全面攻略与实战指南
  • 进程和线程的区别?
  • 组织策略性陪伴顾问
  • 认识Jacobian
  • Java 大视界 -- Java 大数据机器学习模型在卫星通信信号干扰检测与智能抗干扰中的应用(323)
  • 【机器学习第一期(Python)】梯度提升决策树 GBDT
  • 2D写实交互数字人如何重塑服务体验?
  • 4.2_1朴素模式匹配算法
  • DevSecOps时代下测试工具的全新范式:从孤立到融合的质变之路
  • gitlab https链接转为ssh链接
  • 数栈 × AWS EMR On EC2 适配实践:打造出海企业可落地的云上数据中台解决方案
  • ​​深入详解单片机中的输入阻抗与输出阻抗​
  • Android Studio flutter项目运行、打包时间太长
  • 在Visual Studio使用Qt的插件机制进行开发
  • QT Creator构建失败:-1: error: Unknown module(s) in QT: serialport
  • 优化通信,Profinet转Ethernet IP网关在数字化工厂发挥实效显神通
  • 每日算法刷题Day38 6.25:leetcode前缀和3道题,用时1h40min
  • ✨【CosyVoice2-0.5B 实战】Segmentation fault (core dumped) 终极解决方案 (保姆级教程)
  • CMS系统插件更新后服务器异常排查指南:快速恢复网站运行!