首页 > 编程语言 >【算法竞赛】二叉树和哈夫曼树

【算法竞赛】二叉树和哈夫曼树

时间:2024-09-21 15:21:09浏览次数:3  
标签:编码 遍历 哈夫曼 频次 算法 二叉树 节点

树是非线性数据结构,它能很好地描述数据的层次关系。

树这种结构的现实场景很常见,如文件目录、书本的目录就是典型的树形结构。二叉树是最常用的树形结构,特别适合编码,常常将一般的树转换为二叉树来处理。

本节介绍二叉树的定义和存储。

哈夫曼(Huffman)树是二叉树的一个应用。

二叉树的概念

二叉树的性质

二叉树的每个节点最多有两个子节点,分别称为左孩子、右孩子,以它们为根的子树称
为左子树、右子树。

二叉树的每层节点数以2的倍数递增,所以二叉树的第i层最多有2(i-1)个节点。如果
每层的节点数都是满的,称它为满二叉树。一个n层的满二叉树,一共有2n-1个节点。

如果满二叉树只在最后一层有缺失,并且缺失的约编号都在最后,则称为完全二叉树。

图1.3所示为一棵满二叉树和一棵完全二叉树。
在这里插入图片描述
1号节点是二叉树的,它是唯一没有父节点的节点。

从根到节点u的路径长度定义为u的深度,节点u到它的叶子节点的最大路径长度定义为节点u的高度。根的高度最大,称为树的高。

二叉树之所以应用广泛,得益于它的形态。高级数据结构大部分与二叉树有关,下面列
举二叉树的一些优势。

  1. 在二叉树上能进行极高效率的访问。
    一棵平衡的二叉树,如满二叉树或完全二叉树,每层的节点数量约为上一层数量的2倍。也就是说,一棵有N个节点的满二叉树,树的高度为O(log2N)。从根节点到叶子节点,只需要走log2N步,就能到达树中的任意节点.

例如,N=100万,树的高度仅为log2N=20,只需要20步就能到达这100万个节点中的任
意一个。但是,如果二叉树不是满的,而且很不平衡,甚至在极端情况下退化为一条"链",访
问效率会打折扣。维护二叉树的平衡是高级数据结构的主要任务之一.

  1. 二叉树很适合做从整体到局部、从局部到整体的操作。
    二叉树内的一棵子树可以看作整棵树的一个子区间,求区间最值、区间和、区间翻转、区间合并、区间分裂等,用二叉树都很快捷。例如,文本编辑器用二叉树实现效率很高。

  2. 基于二叉树的算法容易设计和实现。
    例如,二叉树用宽长度优先搜索(Breadth-FirstSearch,BFS)和深度优先搜索(Depth-First Search,DFS)处理都极为简便。二叉树可以一层一层地搜索,这是BFS。
    二叉树的任意一个子节点,是以它为根的一棵二叉树,这是一种递归结构,用DFS访问二叉树极容易编码。BFS和DFS是二叉树的绝配。

二叉树的存储结构

二叉树的一个节点的存储,包括节点的值、左右子节点,有动态和静态两种存储方法

  1. 动态二叉树
    数据结构中一般这样定义二叉树:
struct Node
{
	//节点的值,可以定义多个值
	
	int value;
	
	//指向左右子节点
	node * lson, * rson;
};

动态新建一个Node时,用new运算符动态申请一个节点。使用完毕后,应该用delete命令释放它,否则会内存泄漏。
动态二叉树的优点是不浪费空间,缺点是需要管理,不小心会出错。

  1. 用静态数组存储二叉树。
    在算法竞赛中,为了编码简单,加快速度,一般用静态数组实现二叉树。
    下面定义一个大小为N的结构体数组。
//静态二叉树
struct Node
{
	
	char value;
	
	//左右孩子,竞赛时把lson、rson简写为1、r
	int Ison, rson;
}tree[N];
//可以把tree简写为t

在这里插入图片描述
图1.4所示为一棵二叉树的静态存储,根是tree[5]。
编码时一般不用tree[0],因为0被用来表示空节点,如叶子节点tree[2]没有子节点,就把它的子节点赋值为Z=r=0。

特别地,用数组实现完全二叉树,访问非常便捷,此时连lson、rson都不需要定义。一棵节点总数量为k的完全二叉树,设1号节点为根节点,有以下性质:

  1. 编号i>1的节点,其父节点编号是i/2;
  2. 如果2i>k,那么节点i没有左孩子;如果2i+1>k,那么节点i没有右孩子;
  3. 如果节点i有孩子,那么它的左孩子是节点2i,右孩子是节点2i+1。

多叉树转换成二叉树

多叉树有B一树、B+树等。
把多叉树转化为二叉树的应用场景不多见,简单方法是将第1个孩子作为左孩子,将其兄弟节点作为右孩子。这样做做的缺点是可能导致树退化为一条长链,如图1.5所示。
在这里插入图片描述

二叉树的遍历

宽度优先遍历

有时需要按层次一层层从上到下遍历二叉树。例如,在图1.6中,需要按E-BG-ADFI-CH的顺序访问,此时用宽度优先搜索(BFS)是最合适的。
在这里插入图片描述

深度优先遍历

用深度优先搜索(DFS)遍历二叉树,代码很简单,而且产生了很多应用。

按深度搜索的顺序访问二叉树,对父节点、左孩子、右孩子进行组合,有先(父)序遍历、中(父)序遍历、后(父)序遍历这3种访问顺序,这里默认左孩子在右孩子前面。

  1. 先序遍历(按父节点、左孩子、右孩子的顺序访问)

在图1.6中,先序遍历输出的顺序是EBADCGFIH,先序遍历的第1个节点是根。先序遍历的伪代码如下:
在这里插入图片描述

  1. 中序遍历(按左孩子、父节点、右孩子的顺序访问)

在图1.6中,中序遍历输出的顺序是ABCDEFGHI。

读者可能注意到,ABCDEFGHI刚好是字典序,这不是巧合,是因为图示的是一个二叉搜索树。在二叉搜索树中,中序遍历实现了排序功能,返回的结果是一个有序排列。

中序遍历还有一个特征:如果已知根节点,那么在中序遍历的结果中,排在根节
点左边的点都在左子树上,右边的点都在右子树上。例如,E是根,E左边的ABCD在它的左子树上;再如,在子树ABCD上,B是子树的根,那么A在它它的左子树上,CD在它的右子树上。任意子树都符合这个特征。

中序遍历的伪代码如下:
在这里插入图片描述
3. 后序遍历,按左孩子、右孩子、父节点的顺序访问。
在图图1.6中,后序遍历输出的顺序是ACDBFHIGE。后序遍历的最后一个节点是根。
后序遍历的伪代码如下:
在这里插入图片描述

如果已知某棵二叉树的中序遍历和另一种遍历,可以报这棵树构造出来,即"中序遍历+先序遍历"或"中序遍历+后序遍历",都能确定一棵树。

但是,如果不知道中序遍历,只有先序遍历+后序遍历,则不能确定一棵二叉树。例如,图1.7所示的两棵不同的二叉树,它们的先序遍历都是"123",后序遍历都是"321"。

在这里插入图片描述

哈夫曼树与哈夫曼编码

哈夫曼树是一类带权路径长度最短的最优树,是贪心思想在二叉树上的应用。哈夫曼树的一个经典应用是哈夫曼编码。

二叉树上两个节点之间的路径长度是指这条路径经过的边的数量。

树的路径长度是从根到每个节点的路径长度之和。

显然,二叉树越平衡,从根到其他节点的路径越短,树的路径长度也越短。完全二叉树的路径长度是最短的。

把上述概念推广到带权节点。从根到一个带权节点的带权路径长度,是从根到该节点的路径长度与节点权值的乘积。树的带权路径长度是所有叶子节点的带权路径长度之和。

因为节点有权值,所以一棵平衡的二叉树并不一定有最小的带权路径长度。
给定几个权值,构造一棵有几个叶子节点的二叉树,每个叶子节点对应一个权值。有很多种构造方法,把其中带权路径长度最小的二叉树称为哈夫曼树,或者最优二叉树。

如何构造一棵哈夫曼树?容易想到一种贪心方法:把权值大的节点放在离根节点近的层次上,权值小的节点放在离根节点远的层次上

哈夫曼算法步骤如下:

1.把每个权值构造成一棵只有一个节点的树,n个权值构成了n棵树,记为集合F={T1,T2,……,Tn}。

2.在F中选择权值最小的两棵树Ti和Tj,合并为一棵新的二叉树Tx,它的权值等于Ti与Tj的权值之和,左右子树分别为Ti和Tj

3.在F中删除Ti和Tj,并把Tx加入F。

4.重复步骤(2)和步骤(3),直到F中只含有一棵树,这棵树就是哈夫曼树。
下面以哈夫曼编码为例介绍算法的执行步骤。

哈夫曼编码

哈夫曼树的一个经典应用是哈夫曼编码,这是一种"前缀"最优编码。
什么是编码?给定一段字符串,这段字符串包含很多字符。,每种字符出现次数不一样,有的频次高,有的频次低。现在把这段字符串存储在计算机中,因为数据在计算机中都是用二进制码表示的,所以需要把每个字符编码成一个二进制数。
最简单的编码方法是把每个字符都用相同长度的二进制数表示。例如,给出一段字符串,它只包含A、B、C、D、E这5种字符,编码方案如下。
在这里插入图片描述
这种编码方法简单实用,但是不节省空间。
由于每个字符出现频次不同,可以想到用变长编码:出现次数多的字符用短码表示,出现少的用长码表示。编码方案如下:
在这里插入图片描述
第2种方法相当于对第1种方法进行了压缩,压缩比为156/112~1.39

编码方法的基本要求:编码后得到的二进制串能唯一地进行行解码还原。
上面第1种简单方法显然是正确的,每3位二进制数对应一个字符。第2种方法也是正确的,如11001111001101,解码后唯一得到 ABDEC。

如果胡乱设定编码方案,很可能是错误的,例如下面的编码方案:
在这里插入图片描述
上述编码方案看起来似乎每个字符都有不同的编码,编码后的总长度也更短,只有3×3+9×2+6×2+15×1+19×1=73,但是编码无法解码还原。
例如,编码100,是解码为A、BE还是DEE呢?

错误的原因是某个编码是另一个编码的前缀(Prefix),即这两个编码有包含关系,导致了混淆。

有没有比第2种编码方法更好的方法?

这引出了一个常见问题:给定一个字符串,如何编码能使编码后的总长度最小?即如何得到一个最优解?

哈夫曼编码是前缀编码的一种最优算法。
如何设计编码方法?由于编码是二进制,容易想到用二叉树构造编码。例如,上面第2种编码方案的二叉树实现如图1.8所示。
在这里插入图片描述

在二叉树的每个分支,左边是0,右边是1。二叉树末端的叶子节点就是编码。把编码放在叶子节点上,可以保证符合"前缀不包含"的要求。

出现频次最高的字符E在最靠近根节点的位置,编码最短;出现频次最低的A在二叉树最深处,编码最长。

这棵编码二叉树是如何构造的?下面用哈夫曼树构造哈夫曼编码
首先对所有字符按出现频次排序,如下所示。
在这里插入图片描述
然后,从出现频次最低的字符开始,利用贪心思想安排在二叉树上。按哈夫曼算法执行如图1.9所示的步骤。
在这里插入图片描述
每个节点圆圈内的数字是这个子树下字符出现的频次之和。
贪心的过程是按出现频次从底层向顶层生成二叉树。贪心过程保证了出现频次低的字符被放在树的底层,编码更长;出现频次高的字符被放在顶层,编码更短。可以证明,哈夫曼算法符合贪心法的"最优子结构性质"和"贪心选择性质",编码的结果是最优的。

下面给出一道例题:
在这里插入图片描述
本题正常的解题过程是首先统计字符出现的频次,然后用哈夫曼算法编码,最后计算编码后的总长度。
不过,由于只需要输出编码总长度,而不要求输出每个字符的编码,所以可以跳过编码过程,利用图1.9所描述的哈夫曼编码思想(圆圈内的数字为出现频次)直接计算编码的总长度。
下面的代码使用了STL的优先队列,在每个贪心步骤,从优先队列中取出最少的两个频次。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
int main(){
    priority_queue<int, vector<int>, greater<int>>q;
    string s;
    while(getline(cin, s) && s!= "END"){
        sort(s.begin(), s.end());
        int num = 1;
        for(int i = 1; i <= s.length(); i++){
            if(s[i]!=s[i - 1]){
                q.push(num);
                num = 1;
            }
            else{
                num++;
            }
        }
        int ans = s.length();
        if (q.size()==1){
            while(q.size()>1){
                int a = q.top();
                q.pop();
                int b = q.top();
                q.pop();
                ans += a + b;
                q.push(a + b);
            }
        }
        q.pop();
        printf("%d %d %.1f\n", s.length() * 8, ans, (double)s.length() * 8 / (double)ans);
    }
    return 0;
}

标签:编码,遍历,哈夫曼,频次,算法,二叉树,节点
From: https://blog.csdn.net/Sakura_ding/article/details/142415150

相关文章

  • 初识数据结构和算法
    说在前面:⭐看到这篇文章的友友你好啊,在学习的路途中欢迎你的私信、留言,交流互动啊,我们一起学习、一起进步呀!⭐目录数据和结构解释含义数据的属性划分数据和算法的关系算法中复杂度时间复杂度空间复杂度数据和结构解释含义......
  • PHP抽奖算法
    一、初始化奖品id奖品的idpid奖品的自定义idtype奖品类型,1、虚拟奖品2、实物奖品3、礼包码待扩充name奖品名称total奖品总数chance获奖概率/抽奖基数10000daynum每日数量限制pay充值限制$prize=[['id'=>1,'pid'=>11,'type'=>1,'name'=>'典藏......
  • c++算法 枚举———百钱白鸡问题
    前言枚举,是一种最基本的算法思想,通过穷举枚举出所有的可能,再加以比较。枚举算法适用于问题规模较小、解空间可穷举的情况。它的优点是简单直观,不需要复杂的数学推导,易于实现。但是,对于问题规模较大的情况,枚举算法的时间复杂度可能会非常高,效率较低。接下来会介绍两个百钱白......
  • (LeetCode 热题 100) 199. 二叉树的右视图(递归、深度优先搜索dfs)
    199.二叉树的右视图思路:递归每次都优先右边子树,然后才是左子树。/***Definitionforabinarytreenode.*structTreeNode{*intval;*TreeNode*left;*TreeNode*right;*TreeNode():val(0),left(nullptr),right(nullptr){}......
  • 稳定排序算法
    一、什么是不稳定性算法?具有相同关键字的纪录经过排序后, 相对位置发生改变, 这样的算法是不稳定性算法。一、不稳定排序算法有哪些1、堆排序2、希尔排序3、快速排序4、选择排序口诀:一堆(堆)希尔(希尔)快(快速)选(选择)二、常见排序算法稳定性分析1、堆排序堆的结构是节点i的孩子为2*i......
  • 基于Spark的温布尔登特色赛赛事数据分析预测及算法实现_718p9405
    目录技术栈和环境说明python语言解决的思路具体实现截图框架介绍技术路线操作可行性性能/安全/负载方面python-flask核心代码部分展示python-django核心代码部分展示详细视频演示源码获取技术栈和环境说明结合用户的使用需求,本系统采用运用较为广泛的Python语言,DJAN......
  • 基础算法模板
    P3372【模板】线段树1_linkP3374【模板】树状数组1_linkP3366【模板】最小生成树_linkP4779【模板】单源最短路径(标准版)_link(dijkstra)P3379【模板】最近公共祖先(LCA)_linkP3865【模板】ST表&&RMQ问题_linkP3375【模板】KMP_link......
  • 无人机集群路径规划:麻雀搜索算法(Sparrow Search Algorithm, SSA)​求解无人机集群路
     一、单个无人机路径规划模型介绍无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径,使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一,它可以通过算法和模型来确定无人机的航迹,以避开障碍物、优化飞行时间和节省能量消耗。二、无人......
  • 无人机集群路径规划:​北方苍鹰优化算法(Northern Goshawk Optimization,NGO)​求解无人机
     一、单个无人机路径规划模型介绍无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径,使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一,它可以通过算法和模型来确定无人机的航迹,以避开障碍物、优化飞行时间和节省能量消耗。二、无人......
  • 数据结构:二叉树(2)
    ps:爆更第二期前言普通的树的实用价值比较小,将树更一步特殊化成二叉树,将获得更多的特殊性质。例如搜索二叉树,红黑树等。这篇博文主要介绍二叉树的基础知识,进阶版高级二叉树,后续会持续更新。二叉树的概念一棵二叉树是结点的一个有限集合,该集合:或者为空由一个根节点......