哈夫曼树与哈夫曼编码、集合

如题所述

第1个回答  2022-06-30

什么是哈夫曼树(Huffman Tree)
eg:将百分制的考试成绩转换为五分制的成绩
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;
建立判定树,如果考虑到学生成绩的分布概率,我们发现得3分和得4分的同学占了绝大多数的比例,直接用上述判别流程建立判定树 显然效率不够高。
修改判定树:
if ( score < 80 )
{
if ( score < 70 )
if ( score < 60 ) grade = 1 ;
else grade = 2 ;
} else if ( score < 90 ) grade = 4 ;
else grade = 5;
按照这种判别流程建立的判定树 效率得到显著提高。
如何根据结点不同的查找频率 构造更有效的搜索树?

带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值(即频率)Wk,从根结点到每个叶子结点的长度为Lk,则每个叶子结点的带权路径长度之和为 WPL=W1 L1+W2 L2+ …… +Wn*Ln
目标:将WPL降到最低。最优二叉树或哈夫曼树就是WPL最小的二叉树

每次把权值最小的两棵二叉树合并,得到的新结点的权值为两棵树的权值的和,再由新结点与剩下的结点中再挑出两个权值最小的树 再次合并,直到最后所有树连接在一起
如何选取两个最小的树?利用堆
typedef struct TreeNode *HuffmanTree ;
struct TreeNode {
int Weight ;
HuffmanTree Left, Right ;
}
HuffmanTree Huffman ( MinHeap H )
{
// 假设 H -> Size 个权值已经存在 H -> Element [ ]-> Weight 里
int i ; HuffmanTree T ;
BuildMinHeap ( H ) ; // 将 H -> Element [ ]按权值调整为最小堆
for ( i = 1 ; i < H -> Size ; i ++ ) { // 做 H -> Size - 1 次合并
T = malloc ( sizeof ( struct TreeNode ) ) ; // 建立新结点
// 从最小堆中删除一个结点 作为新树的左子结点
T -> Left = DeleteMin ( H ) ;
// 从最小堆中删除一个结点 作为新树的右子结点
T -> Right = DeleteMin ( H ) ;
// 计算新权值
T -> Weight = T -> Left -> Weight + T -> Right -> Weight ;
Insert ( H , T ) ; // 将新树插入最小堆
}
T = DeleteMin ( H ) ;
return T ;
}
整体复杂度为 O( N logN )

没有度为1的结点
n个叶结点的哈夫曼树共有2n-1个结点: n0:叶结点总数,n1:只有一个儿子的结点总数,n2:有两个儿子的结点总数; 可知:n2=n0-1;
哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树;
对同一组权值,存在不同构的两棵哈夫曼树,不过WPL值是一样的

给定一段字符串,如何对字符串进行编码,可以使得该字符串的编码存储空间最少?
假设有一段文本 包含58个字符,并且由以下7个字符构成:a,e,i,s,t,空格(sp),换行(nl);这7个字符出现的次数不同。如何对这7个字符进行编码 使得总编码空间最少?
分析:
(1)用等长ASCII编码:58 x 8=464位;
(2)用等长3位编码:58 x 3=174位;(因为只有7个字符,3位编码可编8个字符)
(3)不等长编码:出现频率高的字符用的编码短些,出现频率低的字符则可以编码长些
不等长编码会长生二义性,如a=1,e=0,s=11,t=10;那么编码1011就会有多种不同意思,因为我们不知道各个编码长度具体是多少。
前缀码(prefix code):任何字符的编码都不是另一字符编码的前缀,可以无二义地解码
用二叉树进行编码:(1)左右分支:0、1 (2)字符只在叶结点上
只要待编字符在叶结点上,其二叉树编码都不是另一字符编码的前缀
由哈夫曼树构造一棵编码代价最小的树
例:

集合运算:交集、并集、补集、差集,判定一个元素是否属于某一集合
并查集:集合并、查某元素属于什么集合
eg:10台电脑{1,2,3,…,9,10}已知下列电脑之间已经实现了连接:1和2、2和4、3和5、4和7、5和8、6和9、6和10,问2和7之间、5和9之间是否是连通的
思路:(1)将10台电脑看成10个集合(2)已知一种连接“x和y”,就将x和y对应的集合合并(3)查询“x和y是否是连通的”就是判别x和y是否属于同一集合;这种思路就是并查集。
并查集问题中集合存储的实现:
可以用树结构表示集合,树的每一个结点代表一个集合元素;双亲表示法:孩子指向双亲
采用数组存储形式:data表示元素的值,Parent表示其父结点的下标,根结点的Parent用-1表示;
数组中每个元素的类型描述为:
typedef struct {
ElementType Data ;
int Parent ; // 每个结点的父结点的下标
} SetType ;

int Find ( SetType S [ ], ElementType X )
{ // 在数组 S 中查找值为 X 的元素所属的集合
// MaxSize 是全局变量,为数组 S 的最大长度
int i ;
for ( i = 0 ; i < MaxSize && S [ i ]. Data != X ; i ++ ) ;
if ( i >= MaxSize ) return -1 ; // 未找到X,返回-1
for ( ; S [ i ] . Parent >= 0 ; i = S [ i ] . Parent ) ;
return i ; // 找到X所属集合,返回树根结点在数组S中的下标
}

分别找到X1和X2两个元素所在集合树的根结点,如果它们不同根 则将其中一个根结点的父结点指针设置成另一个根结点的数组下标。
void Union ( SetType S[ ], ElementType X1 , ElementType X2 )
{
int Root1 , Root2 ;
Root1 = Find ( S , X1 ) ;
Root2 = Find ( S , X2 ) ;
if ( Root != Root2 ) S[ Root2 ] . Parent = Root1 ;
}

这样并集后,树越来越大越来越高,树高了之后Find函数效率会变差,所以尽量把小的集合并到大的集合中去;用负数代表根结点,负数的绝对值为这棵树的元素个数

相似回答