完全理解二叉树(下)——平衡二叉树、二叉排序树、哈夫曼树

2023-11-07

完全理解二叉树(上)——二叉树的概念、遍历、构造以及线索化
完全理解二叉树(中)——二叉树与树、森林的转化及遍历

1. 平衡二叉树

二叉树可以用于查找元素,对于如下这颗二叉树:
在这里插入图片描述
对其的遍历相当于对链表的遍历,因此找到元素5需要从头开始,查找5次,但是如果树的形状是这样:
在这里插入图片描述
从根结点出发只需要查找3次即可找到元素5。

1.1 定义

左右子树高度差不超过1的二叉树称为平衡二叉树(AVL树),注意是对于树中的任意结点。
例如:
在这里插入图片描述
在这里插入图片描述
都不是平衡二叉树。
结点的平衡因子 = 左子树高 - 右子树高,平衡因子的绝对值不超过1,如下图所示:
在这里插入图片描述

1.2 存储结构

typedef struct AVLTreeNode* avltree;
struct AVLTreeNode{
    int data;
    int balance;
    avltree lnode,rnode; 
};

1.3 左旋与右旋操作

在结点的插入过程中,无论插入的位置在哪,新结点的加入必定会影响平衡因子,可能查找路径上的所有平衡因子都会被影响,因此需要找到一种方法,在整体平衡因子改动最小的基础上,依然满足AVL树的定义。
最小失衡子树:在插入一个结点后,那么这个结点向上查找,第一个出现左右不平衡的树就是最小失衡子树,即找到平衡因子的绝对值先超过 1 的结点。

  • 左旋
    对于这样一颗树
    在这里插入图片描述
    插入结点 6 后,变成
    在这里插入图片描述
    显然,第一个被破坏的平衡因子就是结点 2,则需要对结点 2 进行左旋转。步骤为:
  1. 该结点的右孩子代替该结点的位置(结点3代替结点2成为根结点)
  2. 右孩子的左孩子变成该结点的右孩子(结点4变成结点2的右孩子)
  3. 该节点变成右孩子结点的左孩子(结点2变成结点3的左孩子)
    得到的新平衡二叉树为
    在这里插入图片描述
  • 右旋
    对于这样一颗树
    在这里插入图片描述
    插入结点 1 后,变成
    在这里插入图片描述
    跟左旋操作类似。第一个被破坏平衡的是结点5,步骤为:
  1. 该结点的左孩子代替该结点的位置(结点4代替结点5成为根结点)
  2. 左孩子的右孩子变成该结点的左孩子(结点3变成结点5的左孩子)
  3. 该结点变成左孩子结点的右孩子(结点5变成结点4的右孩子)
    得到的新平衡二叉树为
    在这里插入图片描述
插入方式 结点的左孩子的左子树插入结点 结点的右孩子的右子树插入结点 结点的左孩子的右子树插入结点 结点的右孩子的左子树插入结点
操作 右旋 左旋 先左旋后右旋 先右旋后左旋

1.4 平衡调整策略

  • 结点的左孩子的右子树插入结点
    对于这样一颗平衡二叉树
    在这里插入图片描述
    E的左子树插入结点F:
    在这里插入图片描述
    对A的左孩子进行左旋操作:
    在这里插入图片描述
    然后再对A进行右旋操作:
    在这里插入图片描述
    这样就使得二叉树再次平衡。
  • 结点的右孩子的左子树插入结点
    对于这样一颗平衡二叉树:
    在这里插入图片描述
    给D结点插入F:
    在这里插入图片描述
    对A的右孩子进行右旋:
    在这里插入图片描述
    再对A进行左旋:
    在这里插入图片描述
    就得到了新的平衡二叉树。

2. 二叉排序树

2.1 定义

满足:
(1) 若左子树不空,则 左子树 上所有结点的值 均小于 它的根结点的值;
(2) 若右子树不空,则 右子树 上所有结点的值 均大于 它的根结点的值;
(3) 左、右子树又分别为二叉排序树 。
的二叉树就是二叉排序树。

2.2 存储结构

二叉排序树的存储结构和普通二叉树一样:

typedef struct BsTreeNode* bstree;
struct BsTreeNode{
        ElementType value;
        bstree lnode;
        bstree rnode;
};

2.3 二叉排序树的建立

现有如下一组数:

18 37 55 2 61 4 99 13 25 73 48

先将 18 作为根结点,接下来判断 37,37>18,则放入18的右子树,继续判断55,55>18且右子树已经有37,继续判断 55 和 37,55>37,则放入37的右子树,判断 2,2<18,放入左子树。以此类推,即保证一个数在插入过程中始终满足二叉排序树的定义,最后建立的树如下:
在这里插入图片描述
这颗二叉树的中序遍历结果为:2 4 13 18 25 37 48 55 61 73 99,刚好就是排序后的数列。

2.4 查找、插入、删除操作

二叉排序树的查找不需要进行中序遍历,根据树的结构,直接从根结点进入,然后比较结点的值,以此决定进入左子树还是右子树,代码如下:

bstree BsSearch(bstree root, int key) 
{ 
	 if (root == NULL || root->data == key) 
 		return root; 
 
 	if (root->key < key) 
		 return BsSearch(root->rnode, key); 
	 
 	return BsSearch(root->lnode, key); 
}

插入算法也不难理解,同样使用递归解决:

bstree BsInsert(bstree node, int key) 
{ 
    if (node == NULL)
    	return NewCreate(key); 
  
    if (key < node->data) 
        node->lnode = BsInsert(node->lnode, key); 
    else if (key > node->data) 
        node->rnode = BsInsert(node->rnode, key);    
        
    return node; 
} 

删除操作稍微复杂一点,分为以下情况:

  • 被删除的结点是叶子结点:不会影响树的结构,直接删除。
  • 被删除的结点仅有一个孩子:无论是左孩子还是右孩子,都只需要把孩子结点连接到要删除结点的父结点上即可。
  • 被删除的结点左右孩子都存在:假设要删除的是根结点,根据中序遍历顺序的不变性,以及二叉排序树的结构,可以得出左子树的最右下结点就是直接前驱,根据递归,可以推出任何结点的左子树的最右下结点都是这个结点的直接前驱,故只要找到直接前驱,替换它和待删除结点的值,然后删除该前驱结点,并把它的剩余部分连接回它的父结点,就可以完成删除。当然也可以找到后继结点,这里不作赘述。
bstree BsDelete(bstree root, int key)
{
 	// 如为空,直接返回
 	if (root == NULL)
 		return root;

 	// 如果删除的值小于 root 的值,则递归遍历左子树
 	if (key < root->data)
  		root->lnode = BsDelete(root->lnode, key);

 	// 如果删除的值大于 root 的值,则递归的遍历右子树
 	else if (key > root->data)
  		root->rnode = BsDelete(root->rnode, key);

 	// 如果值相等,则删除结点
 	else
 	{
  		if (root->lnode == NULL)    //删除结点没有左孩子,拼接右孩子
  		{
   			bstree temp = root;
   			root = root->rnode;
   			free(temp);
  		}
  		else if (root->rnode == NULL)    //删除结点没有右孩子,拼接左孩子
  		{
   			bstree temp = root;
   			root = root->lnode;
   			free(temp);
  		}

  		// 左右孩子均不为空,获取删除结点中序遍历的直接前驱结点 
  		bstree q = root;
		bstree s = root->lnode;
		
		while(s->rnode)		// 转左,然后向右走到尽头,找到最右下结点
		{
			q = s;
			s = s->rnode;
		}
		root->data = s->data;
		if( q != root )				// 判断是否执行上述while循环
			q->rnode = s->lnode;	// 说明到了最右下结点,s可能有左子树,因此s的父结点重接右子树	
		else
			q->lnode = s->lnode;	// 说明s没有右子树,但可能有左子树,因此root重接左子树
		free(s);
 	}
 	return root;
}

3. 哈夫曼树

3.1 背景

考虑这个问题,把某个班同学百分制的成绩转换成5分制,规则如下,

90~100     5
80~90      4
70~80      3
60~70      2
<60        1

用条件语句实现:

 if (score < 60)
     grade = 1;
 else if(score < 70)
     grade = 2;
 else if(score < 80)
     grade = 3;
 else if(score < 90)
     grade = 4;
 else
     grade = 5;

但是如果这个班的同学大多数都取得了90分以上的好成绩,那么前面4步的判断在面对大量数据的时候是比较浪费时间的。现在做如下优化,假定我们目前已经知道了这个班成绩的分布律

成绩:
<60
60~70
70~80
80~90
90~100
比例:
0.05
0.15
0.33
0.27
0.20

用比例乘上判断的次数,算法的效率是(最后一档不用比较,因此也是4次):0.05 * 1 + 0.15 * 2 + 0.33 * 3 + 0.27 * 4 + 0.20 * 4 = 3.22
稍微修改一下算法,先判断比例最大的:

if(score < 80)
{
	if(score < 70)
	{
		if(score < 60)
            grade = 1;
        else
            grade = 2;
	}
    else
   		 grade = 3;
}
else if(score < 90)
	grade = 4;
else
	grade = 5;

改良后的算法效率:0.05 * 3 + 0.15 * 3 + 0.33 * 2 +0.27 * 2 + 0.2 * 2 = 2.2
很明显,改良后的算法效率增加了很多。
这样的条件语句可以画出如下的树:
​​​​在这里插入图片描述
哈夫曼树就是为了优化这类数据查找效率的算法。

3.2 定义

给定n权值作为n个叶子节点,构造一棵二叉树,若这棵二叉树的带权路径长度达到最小,则称这样的二叉树为最优二叉树,也称为Huffman树。

3.3 构建

对于 3.1 节给出的数据,定义权值为不同分值出现的频率,则对于 1~5 的5分制,这里用 ABCDE 代表结点的值,用 1~5 代表结点的权,权值 1 2 3 4 5 对应了 3.1 中的频率:0.05、0.15、0.20、0.27、0.33,对应的结点为 ABEDC,其构建过程为:

  1. 选择最小的两个,即 1 和 2,合并,权值之和为 3
  2. 从刚才合并好的 3 和剩下的 3,4,5 里选择两个最小的,即 3 和 3,合并,权值之和为 6
  3. 从 6,4,5 里选择两个最小的,即 4 和 5,合并,6 在往上一层,因此选择了 4 和 5 并不会和 6 处在一层,而是和 3 在同一层,权值之和为 9
  4. 将 6 和 9 合并,权值之和为 15

构建的树如下:
在这里插入图片描述
其实以上过程就是在不断重复找出字符中权值最小的两个,小的在左边,大的在右边,组成二叉树。在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和 这个过程。

3.4 性质

由上面构建出的哈夫曼树,可以得到带权路径长度为:(1+2) * 3 + (3+4+5) * 2 = 9+24 = 33,实际上,一种带权路径长度最短的二叉树,也称为最优二叉树。
给树的路径上,向左标 0,向右标 1,则会得到:
在这里插入图片描述
对于每个结点,都能得到唯一的哈夫曼编码:

A:000
B:001
C:11
D:10
E:01

可见,编码越短,离根结点越近,出现的频率越高。
哈夫曼树(最优二叉树)在构造的时候保证长编码的不与短编码的字母冲突,因为哈夫曼树的它的字母都在叶子节点上,因此不会出现一个字母的编码为另一个字母编码左起子串的情况。意思就是说读到当前位置已经能确定是什么字母时不能因为再读取一位或几位让这个编码能表示另外的字母。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

完全理解二叉树(下)——平衡二叉树、二叉排序树、哈夫曼树 的相关文章

  • 我应该把 try/catch 和“using”语句放在哪里? [复制]

    这个问题在这里已经有答案了 可能的重复 try catch using 正确的语法 https stackoverflow com questions 4590490 try catch using right syntax 我想try c
  • 元组在 VS2012 中如何工作?

    Visual Studio 2012 功能 tuples但不是可变参数模板 这是如何完成的 如何在不使用可变模板的情况下实现元组 简而言之 微软做了与之前在 NET 中实现类似元组的数据类型完全相同的事情 创建许多版本 每个版本都有固定数量
  • c# 从另一个类中的另一个静态事件引发事件

    需要帮助从另一个班级调用事件 我有已声明事件的课程 public class MxPBaseGridView GridView public event AddNewItemsToPopUpMenuEventHandler AddNewIt
  • XPATH 查询、HtmlAgilityPack 和提取文本

    我一直在尝试从名为 tim new 的类中提取链接 我也得到了解决方案 给出了解决方案 片段和必要的信息here https stackoverflow com questions 2982862 extracting a table ro
  • 叮当错误?命名空间模板类的朋友

    以下代码在 clang 下无法编译 但在 gcc 和 VS 下可以编译 template
  • 并行化斐波那契序列生成器

    我正在学习并行化 在一项练习中 我得到了一些我应该提高性能的算法 其中之一是斐波那契数列生成器 array 0 0 array 1 1 for q 2 q lt MAX q array q array q 1 array q 2 我怀疑 这
  • 进程退出后 POSIX 名称信号量不会释放

    我正在尝试使用 POSIX 命名信号量进行跨进程同步 我注意到进程死亡或退出后 信号量仍然被系统打开 在进程 打开它 死亡或退出后是否有办法使其关闭 释放 早期的讨论在这里 当将信号量递减至零的进程崩溃时 如何恢复信号量 https sta
  • 如何以编程方式播放 16 位 pcm 数组 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我有一个包含 16 位 pcm 值的短 数组 我希望能够在不添加任何标题 也不将任何文件保存到内存的情况下播放它 我知道我可能需要一个提供
  • C++ 将联合强制转换为其成员类型之一

    以下对我来说似乎完全符合逻辑 但不是有效的 C 联合不能隐式转换为其成员类型之一 有人知道为什么不这样做的充分理由吗 union u int i char c function f int i int main u v v i 6 f v
  • 带有运算符语法的错误消息,但不带有函数语法的错误消息

    为什么我在调用 unary 时收到错误消息 使用运算符语法 如果我用函数语法调用它就可以了 现场演示 https godbolt org z j7AbeQ template
  • 为什么需要数字后缀?

    C 语言 我确信还有其他语言 需要在数字文字末尾添加后缀 这些后缀指示文字的类型 例如 5m是一个小数 5f是一个浮点数 我的问题是 这些后缀真的有必要吗 或者是否可以从上下文中推断出文字的类型 例如 代码decimal d 5 0应该推断
  • C# 编译器数字文字

    有谁知道 C 编译器数字文字修饰符的完整列表 默认情况下 声明 0 使其成为 Int32 声明 0 0 使其成为 Double 我可以在末尾使用文字修饰符 f 来确保某些内容被视为 Single 例如像这样 var x 0 x is Int
  • 有什么方法可以重载 C# 中的扩展方法吗?

    我有以下模型模式 public abstract class PARENTCLASS public class CHILD A CLASS PARENTCLASS public static class EXTENSION public s
  • 用于连接 DataTable 上的动态列的动态 LINQ

    我目前遇到的情况不确定如何继续 我有两个从数据库填充的数据表 我还有一个可用的列名称列表 可用于将这两个数据表连接在一起 我希望编写一组 LINQ 查询 这些查询将 显示两个数据表中的行 内部联接 用于从一个数据表更新另一个数据表 显示一个
  • 为什么不能调用带有 auto& 参数的 const mutable lambda?

    include
  • 如何使用 CSI.exe 脚本参数

    当你运行csi exe 安装了 Visual Studio 2015 update 2 您将得到以下语法 Microsoft R Visual C Interactive Compiler version 1 2 0 51106 Copyr
  • 使用多线程进行矩阵乘法?

    我应该使用线程将两个矩阵相乘 有两件事 当我运行程序时 我不断得到 0 我还收到消息错误 对于每个错误 它在粗体行上显示 警告 从不兼容的指针类型传递 printMatrix 的参数1 我尝试打印输出 还要注意 第一个粗体块 这是我解决问题
  • 无法在 C# 中为 EventArgs 分配使用派生类型的事件处理程序

    所以我有一个事件声明如下 public event EventHandler OnChangeDetected 然后我有以下处理程序被分配给该事件 myObject OnChangeDetected OnTableChanged 我的理解是
  • 浮点字节序?

    我正在为实时海上模拟器编写客户端和服务器 并且由于我必须通过套接字发送大量数据 因此我使用二进制数据来最大化可以发送的数据量 我已经了解整数字节顺序以及如何使用htonl and ntohl为了规避字节顺序问题 但我的应用程序与几乎所有模拟
  • 在哪里可以下载没有 Visual Studio 2010 的 C# 4.0 编译器?

    我知道 CTP VS 2010 映像 但我可以只下载 NET Framework 4 0 和 C 编译器吗 AFAIK VS 2010 CTP 仅作为 VM 映像提供 我不相信 Microsoft 发布了 VS 的安装程序 其中一个绝对不适

随机推荐

  • android四大组件(详细总结)

    Android四大组件分别为activity service content provider broadcast receiver 一 android四大组件详解 1 activity 1 一个Activity通常就是一个单独的屏幕 窗口
  • springboot mybaties Invalid bound statement (not found) 错误

    错误信息 org apache catalina connector RequestFacade 5151cc1f org apache ibatis binding BindingException Invalid bound state
  • No module named ‘rospy‘

    1 rospkg未安装 pip install rospkg 或 conda install rospkg 2 rospkg已安装 参考 为pycharm设置搜索路径 sunshine drizzle的博客 CSDN博客 以下适用于 pyc
  • 补码的除法运算

    补码的除法运算是将两个数都使用补码的形式来进行计算 和原码的除法相比 补码的除法运算中被除数 除数以及余数都采用双符号位的形式参与计算 最后得到的余数符号位就代表着最终结果的符号位 加减交替法 题目 假设机器字长为5位 x 0 1000 y
  • C++类中的三大函数(构造,析构,拷贝)

    下面一段话与大家共勉 每个人的一生都会遇到很多边界 有些边界可以突破 有些则不能 那些无法突破的边界就是你的极限 而划分边界的标准就是 阈值 每次突破阈值之后 人生轨迹就会发生剧烈变化 其间需要你做出很多思考和判断 直到最后找到自己的极限
  • 类模板特例化

    参考来源 C primer 中文版第5版 P626 1 举个例子 为标准库hash模板定义一个特例化版本 可以用它来将Sales data对象保存在无序容器中 默认情况下 无序容器使用hash
  • Rancher持续集成部署K8S集群的脚本

    文章目录 1 概要 2 安装 2 1 Rancher CLI安装 2 2 kubectl 安装 3 Rancher CLI 配置API Key 3 1 添加API key 3 2 测试API key 3 3 配置部署镜像的脚本 3 3 1
  • gcc常用命令的使用

    一 简介 gcc提供了30多条警告信息和3个警告级别 使用它们有助于增强程序的稳定性和可移植性 此外 gcc还对标准的C和C 语言进行了大量的扩展 提高了程序的执行效率 有助于编译器进行代码优化 能够减轻编程的工作量 二 gcc常用的编译选
  • 高性能MySQL实战(一):表结构

    最近因需求改动新增了一些数据库表 但是在定义表结构时 具体列属性的选择有些不知其所以然 索引的添加也有遗漏和不规范的地方 所以我打算为创建一个高性能表的过程以实战的形式写一个专题 以此来学习和巩固这些知识 一 实战 我使用的 MySQL 版
  • 计算机外部设备IO接口

    计算机外部设备IO接口 常见接口 术语 常见接口 接口 特点 USB 通用串行总线 Universal Serial Bus 高速率 热插拔 雏菊链 最新版本 USB 4 SCSI 小型计算机系统接口 Small Computer Syst
  • rocketmq安装、启动

    1 下载 gt wget http mirror bit edu cn apache rocketmq 4 4 0 rocketmq all 4 4 0 source release zip gt unzip rocketmq all 4
  • 做一个合格的开发,从玩转Apipost开始

    前言 也是有一段时间没更文了 最近忙于跟生活对线 今天给大家带来的是一个宝贝 Apipost 这东西做啥用 这东西做啥用 这东西做啥用 这东西做啥用 在了解这个apipost的作用之前 先听我说 谢谢你因为有你 温暖了四季 身为后端研发的我
  • 全栈之前端

    关注回复 学习交流群 加入 安全开发运维 答疑交流群 原文连接 全栈之前端 2 CSS3基础知识之选择器学习本章将主要介绍CSS选择器类型 id 类 属性伪类和伪元素及关系选择器 它是CSS规则的第一部分 常常用于元素和其他部分组合起来告诉
  • 弹球小游戏

    创建loginBall类实现开始游戏界面 package 弹球小游戏 import javax swing import java awt public class loginBall JFrame JF new JFrame 弹球小游戏
  • MySQL行转列与列转行(实现过程)

    最近工作用到了好几次列转行 做个小总结 顺道也总结一下行转列 行转列 转换之前的表格 第三 四列分别为特征和数值 图1 首先看第一次的执行sql select id name case 特征 when 年龄 then 数值 else 0 e
  • java每五分钟执行_java关于Timer schedule执行定时任务 1、在应用开发中,经常需要一些周期性的操作,比如每5分钟执行某一操作等...

    1 在应用开发中 经常需要一些周期性的操作 比如每5分钟执行某一操作等 对于这样的操作最方便 高效的实现方式就是使用java util Timer工具类 private java util Timer timer timer new Tim
  • TARS-PHP,TarsPHP: TARS-PHP是针对php使用tars二进制协议,以及tars平台整体运维、RPC等一系列能力的解决方案...

    TARS PHP TARS PHP是针对php使用tars二进制协议 以及tars平台整体运维 RPC等一系列能力的解决方案 它主要由如下的几个部分组成 如果你想要快速的体验tars server 请进入examples目录 里面有详尽的三
  • 七十三.JAVA典型的数组处理

    public class LianXi public static void main String args 声明数组 int a 创建数组 a new int 10 初始化数组 for int i 0 i lt 10 i a i 0 数
  • git在Windows 下和Linux下换行符问题

    git config
  • 完全理解二叉树(下)——平衡二叉树、二叉排序树、哈夫曼树

    完全理解二叉树 上 二叉树的概念 遍历 构造以及线索化 完全理解二叉树 中 二叉树与树 森林的转化及遍历 1 平衡二叉树 二叉树可以用于查找元素 对于如下这颗二叉树 对其的遍历相当于对链表的遍历 因此找到元素5需要从头开始 查找5次 但是如