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

数据结构与算法(python版)-- 04 二叉树续

文章目录

  • 哈夫曼树
  • 编程实现哈夫曼树
  • 哈夫曼编码
    • 例子
  • 树的性质
  • 树的存储
  • 树与二叉树的转换
  • 森林与二叉树转换;
  • B树
  • B树的python实现
  • B+ 树
  • 练习题

哈夫曼树

  • 思想:使权值比重大的节点最靠近树根;权值越小越远离树根;
  • 二叉树中叶子节点的带权路径为 权重w i ∗ l i {w_i * l_i}wilil i {l_i}li为根到叶子的路径长度,w i {w_i}wi为叶子节点的比例(权重);
  • 树的带权路径长度 = 所有叶子节点的带权路径长度
  • 有n个叶子节点的二叉树中,带权路径长度最小的二叉树,为哈夫曼树, 是一种最优的二叉树
  • 哈夫曼树不是唯一的,但树WPL 【带权路径长度】是唯一的;

  • 哈夫曼树的构建步骤
    • 以每个权值创建只有一个根节点的二叉树,形成二叉树森林,并按照树根权值升序排序;

    • 在该森林中,每次取根节点权值最小的两棵树,合并成一棵新树,新树根的权值为左右子树(根)权值之和;

    • 从森林中删除使用过的旧树,添加新树,并重新排序;

    • 重复以上步骤,直到森林中只剩一棵树停止;

    • 利用如下权值,构造一棵哈夫曼树:

    • 哈夫曼树没有度为1的节点(即都是度为0或2);

    • 哈夫曼树 优化 if 分支的比较次数, if 条件先走比例最大的情况,elif 条件走剩余部分中比例最大的情况,依次处理;

编程实现哈夫曼树

  • 节点结构:数据、左子树指针、右子树指针,父节点指针
  • 构建以【7,5,2,4】为权值的哈夫曼树;
    • 以每个权值的叶子节点为根,构建只有根的二叉树,放入列表;
    • 列表升序排序(以根节点权值升序),并取出(根节点)权值最小的两棵树,分别为左右子树,新的根节点权值为左右子树权值之和;
    • 新的二叉树放入列表中,并重新排序(以根节点权值升序),并重复步骤2;直到列表中只有一棵树为止。
classNode:def__init__(self,data,left=None,right=None,height=1,parent=None):self.data=data self.left=left self.right=right# 可以加入统计树高self.height=height self.parent=parentdefbuild_huffman(alist):# 初始化只有一个根的二叉树的森林forest=[Node(v)forvinalist]whilelen(forest)>1:# 升序排序forest.sort(key=lambdai:i.data)left=forest.pop(0)right=forest.pop(0)# 合并树root_data=left.data+right.data root=Node(root_data)root.left=left root.right=right root.height+=max(left.height,right.height)left.parent=root right.parent=root# 新树入森林forest.append(root)returnforest[0]defpreorder_travel(root):ifrootisNone:returnprint(root.data,end=" ")preorder_travel(root.left)preorder_travel(root.right)# 中序inorder# 后序postorderif__name__=='__main__':weights=[7,5,2,4]root=build_huffman(weights)preorder_travel(root)# 先序遍历:18 7 11 5 6 2 4

哈夫曼编码

  • 编码,将字符信息转为0/1二进制串;
  • 为节约传输时间,使编码的二进制串尽可能短;-> 非等长编码
  • 任何字符的编码不能是其他编码的前缀,保证解码的唯一性;
  • 哈夫曼编码
    • 统计每个字符的概率,作为权重;
    • 按照权重,构建哈夫曼树;
    • 左分支的路径符号为0,右分支路径符号为1;
    • 从根到叶子节点的路径符号连接,即为该叶子对应字符的编码;

例子

  • 传输字符串为“ABCACCDAEAE”,每个字符概率为0.36、0.1、0.27、0.1、0.18,计算该字符串的哈夫曼编码结果。
  • 思路
    • 统计字符概率作为权重;
    • 以权重构建哈夫曼树;
    • 左子树路径标记为0, 右子树路径标记为1;
    • 每个叶子对应的字符,编码结果为路径符号连接;

  • 编程实现
classNode:def__init__(self,data,left=None,right=None,height=1,parent=None):self.data=data self.left=left self.right=right self.height=height self.parent=parent# 父节点defbuild_huffman(alist):# 初始化只有一个根的二叉树的森林forest=[Node(v)forvinalist]origin_leafs=forest.copy()whilelen(forest)>1:# 升序排序forest.sort(key=lambdai:i.data)left=forest.pop(0)right=forest.pop(0)# 合并树root_data=left.data+right.data root=Node(root_data)root.left=left root.right=right root.height+=max(left.height,right.height)left.parent=root right.parent=root# 新树入森林forest.append(root)returnforest[0],origin_leafsdefcount_weight(s):""" 统计每个字符的概率,概率越大,编码越短 """dict_={}n=len(s)forcins:ifcindict_:dict_[c]+=1else:dict_[c]=1char_list=[]weights=[]fork,vindict_.items():char_list.append(k)weights.append(round(v/n,3))returnchar_list,weightsdefhuffman_encode(leafs,root):num=len(leafs)# 每个叶子节点的编码node_encode=[""]*numforiinrange(num):node=leafs[i]whilenodeisnotroot:# 从叶子节点开始,向根节点查找ifnode.parent.leftisnode:node_encode[i]="0"+node_encode[i]else:node_encode[i]="1"+node_encode[i]node=node.parentreturnnode_encodeif__name__=='__main__':s="ABCACCDAEAE"char_list,weights=count_weight(s)print(char_list)print(weights)# 创建huffman树root,leafs=build_huffman(weights)preorder_travel(root)# 1.001 0.364 0.182 0.182 0.091 0.091 0.637 0.273 0.364# 获取每个叶子节点对应的符号的 编码result=huffman_encode(leafs,root)print("\n",result)# 字符 ['A', 'B', 'C', 'D', 'E']# 频数 [4, 1, 3, 1, 2] , 计算树的WPL 时使用频数计算;# 权重 [0.364, 0.091, 0.273, 0.091, 0.182]# 先序遍历1.001 0.364 0.182 0.182 0.091 0.091 0.637 0.273 0.364# 编码 ['11', '010', '10', '011', '00']
  • 计算该huffman树的唯一WPL = 2 * 4 + 3 * 1 + 2 * 3 + 3*1 + 2 * 2 = 24, 即传输该字符串需要的总比特数
  • 每个字符的平均码长 =W P L 总字符数 \frac {WPL} {总字符数}总字符数WPL=24 11 \frac {24} {11}1124= 2.18
  • 等长编码,即每个字符的编码bit 一样长,一个bit位可以表示2 1 2^121x xx个bit位可以表示n个字符,2 x = n 2^x=n2x=n,则x = l o g 2 n x=log_2nx=log2n向上取整;
  • 压缩率(节省率) = (等长编码总位数 − 哈夫曼编码总位数) / 等长编码总位数 × 100%。

树的性质

  • 除了根节点,每个节点都有一个前驱节点【一条边】,所以 n个节点的树中有n − 1 {n-1}n1条边;
  • 度为k的树中,第i层最多有k i − 1 {k^{i-1}}ki1个节点
  • 度为k的、高为h的树中,至多有( k h − 1 ) / ( k − 1 ) {(k^h - 1) /( k -1)}(kh1)/(k1)个节点;
  • n个节点的度为k的树中,最小高度为l o g k ( n ∗ ( k − 1 ) + 1 ) {log_k(n*(k-1) + 1) }logk(n(k1)+1)向上取整

树的存储

  • 双亲表示法,每个节点存储数据、父节点的编号(或地址),便于查找父节点;
  • 孩子节点表示法,每个节点存储数据、孩子的编号(或地址),便于查找孩子节点;
  • 数组+单链表,节点存储数据和链表的头节点,链表表示(从左到右的)孩子节点;
  • 长子兄弟表示法,长子作为左子节点,兄弟作为右子节点;

树与二叉树的转换

  • 每个树都可以转为唯一的二叉树

  • 基于左孩子右兄弟表示法;

  • 树转为二叉树,二叉树的右子树一定为空

    • 以树根为二叉树的根节点;
    • 最左侧孩子作为左子树,每个节点的右兄弟作为自己的右子树 【形成右链】;
    • 删除线,每个父节点只保留最左侧孩子的连线,删除与其他孩子的连线;
    • 加线,兄弟之间加右链线;
    • 递归处理每个孩子节点的转换【从左到右顺序】;
  • 二叉树还原树;

    • 以二叉树的根为树根;
    • 二叉树的左孩子作为树的第一个最左侧的孩子,二叉树左孩子的右链中恢复树的其他孩子;
    • 删除原二叉树中双亲节点与右孩子的连线;
    • 递归处理二叉树中其他节点的转换【从左到右的顺序】;

森林与二叉树转换;

  • 森林转为二叉树;

    • 将森林中的每棵树,转为唯一二叉树;
    • 将依次转换的每棵二叉树的根连线,形成右链,即后面的二叉树作为其前面的二叉树的右子树;
    • 以第一棵二叉树的根为整个二叉树的树根;
  • 二叉树转为森林;

    • 将根节点与右孩子、右孩子的右孩子,… 之间的连线断开,形成多棵二叉树;
    • 每棵二叉树还原树,得到初始的森林;


B树

  • B树是多路平衡查找树,是多叉树;

  • 所有叶子节点在同一层,保证平衡;

  • 叶子节点不存储信息,表示查找失败;

  • 终端节点表示最后一排具有关键字的节点;

  • 左子树的值都小于父节点中的值,父节点的值都小于右子树的值;

  • B树的表示每个节点的子节点数中的最大值,一般设置为偶数;

  • B树的,表示每个节点中存储的数据个数;

  • B树用于磁盘的存储、查询,如文件系统、数据库索引;

  • m阶B树的性质:

    • 根节点至少一个键,至少两个子树,最多m个子树(m-1个键);
    • 每个节点中的值升序 或者降序;
    • 除根以外的节点,至少m/2 【向上取整】棵子树,至多m棵子树(m阶);
    • 除根以外的节点,至少m/2 - 1【向上取整】个键,至多m-1个键;
    • 所有叶子节点在同一层(保证平衡);
    • 树矮,磁盘IO少,查询效率高;
    • 中序遍历有序性;
    • 计算如下B树的阶数(注意叶子节点没有画出)4阶
  • m阶(如6阶)B树的插入:

    • 取一个待插入的值,若当前B树为空或仅有一个根节点,则直接插入根节点,否则就与根中的键比较大小,走进对应的子树节点中插入,并排序(一般升序,也可降序);
    • 若插入后,当前节点键数超出上限(m-1),则触发分裂(否则本次插入完成),将中间的键值提取到父节点中,左边的键值形成左节点,右边的键值形成右节点;若父节点键值超限,则触发分裂,同样方式处理分裂;
    • 如下为1到17之间的序列数值构建的B树;
  • m阶B树的查找,从根节点中,依次比较每个关键字,找到则结束,未找到 则在关键字区间内的子树中继续查找;若走到叶子节点,则查找失败;

  • m阶(如6阶)B树的删除

    • 叶子节点的键删除后,若剩余关键字数量大于等于最小阈值,则结束;否则删除后【键个数不满足节点最少键数】,则向左右兄弟借,若向左兄弟借,则左兄弟最大键值进入父节点,父节点中被夹持的键值借给键不足的节点;当左右兄弟都不够借时,当前节点需要与左或右兄弟进行合并(先将父节点中被夹持的键值拉下来,在一起合并到兄弟节点中)
    • 非叶子节点的删除,将左边子树的最大值【前驱节点】或者右边子树的最小值【后驱节点】与待删除的值对换,然后在页节点中删除该值;删除键后不满足最小关键字数,则向兄弟节点借。
    • 如下为5阶B树,尝试删除键3、25;

      删除3【删除后向右兄弟借,父下来,兄上去】:

      删除25【叶子节点中直接删除】:

    删除 38 非叶子节点:

B树的python实现

pending

B+ 树

  • B+树的根节点至少两个键,两个子树;
  • m阶 B+树的非根非叶子节点的子树返回【m/2 上取整, m】,节点关键字与子树数量保持一致;
  • 叶子节点存储数据,并且叶子之间存在链表关系;
  • 非叶子节点中不存储数据,只存储导航信息【数据的索引】;

练习题


答案:

  1. D, A
  2. D
  3. A
  4. D

B树构建
基于如下的数据,以插入方式构建3阶B树,画出构建过程:
{15, 5, 20, 3, 10, 25, 8, 12, 18, 22, 28}
1.插入15,空树直接创建根节点;
2.插入5,只有一个根节点时直接插入根 [5 | 15]
3.插入20,[5 | 15 | 20] 关键字超限,触发分裂

4.插入3, 3 <15 则到关键字15的左侧子树中插入

5.插入10, [3 | 5 | 10] 触发分裂

6.插入25

7.插入8,8在5-15关键字之间,所以到这个区间下的子树中插入;

8.插入12,[8 | 10 | 12] 触发分裂,提取中间节点到父节点中,再次触发父节点的分裂;

9.插入18,[18 | 20 | 25]

10.插入22;

11.插入28,形成最终的B树:

http://www.gsyq.cn/news/1563397.html

相关文章:

  • Recoil未来展望:PHP 8+新特性对协程编程的终极影响
  • M68HC705PICS开发工具包:从硬件连接到软件调试的完整指南
  • 手撕CNN:从卷积计算到工程落地的全链路解析
  • PVZ Toolkit完整指南:植物大战僵尸终极修改器使用教程
  • 嵌入式音频与网络驱动开发实战:基于DSP5685x的TDC1与IDC驱动解析
  • MapLibre Native样式表达式:让地图“活“起来的魔法公式
  • 解锁Linux新体验:bilibili-linux项目全面解析
  • LaserGRBL终极指南:从零开始掌握免费激光雕刻软件
  • 终极指南:用SMU Debug Tool解锁AMD Ryzen处理器的隐藏性能
  • Rizz构建系统:CMake配置与多平台编译的完整指南
  • 嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用
  • JSON Schema数据生成瓶颈的架构化解决方案:JSON-Schema Faker的技术价值深度解析
  • 企业级Kafka监控平台架构设计与部署方案
  • pg_query_go最佳实践:企业级SQL解析和处理的完整解决方案
  • Google AI Studio 300美元额度的真相与实战指南
  • SwiftSoup:构建高性能Swift网络数据采集工具的完整指南
  • CANN/cannbot-skills NPU图DFX分诊评估
  • Adaboost代码实现-葡萄酒实例
  • Netcat正反向Shell攻防:内网渗透与纵深防御实战解析
  • 终极Avalonia实战指南:5大核心模块深度解析与跨平台UI开发秘籍
  • emWin图表与表格控件实战:GRAPH_SCALE与HEADER深度解析
  • 基于决策树算法的感冒预测3(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 【防水工艺科普】微创防水施工相比传统砸砖,优势体现在哪些方面 - 青岛防水品牌推荐
  • 智能革新:biliTickerBuy如何重新定义B站会员购抢票体验
  • HC08微控制器编程实战:MCUscribe工具核心功能与避坑指南
  • useEffectReducer完全指南:让你的React副作用代码更清晰、更可维护
  • 关于comfyui的xformers参数memory_efficient_attention.fa2F是unavailable(flash_attn)
  • AppleRa1n:5步免费解锁iOS 15-16设备激活锁的完整指南
  • 2026多AI工具稳定使用方案:四层隔离架构与故障自愈实践
  • 深度学习图像去雾:物理建模与数据驱动的协同工程