哈夫曼树
定义: 最优二叉树或哈夫曼树即为 WPL 最小的二叉树。
哈夫曼树的构造: 每次把权值最小的两棵二叉树合并
构造代码:
typedef struct TreeNode *HuffmanTree;
struct TreeNode{
int Weight;
HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H )
{ /* 假设H->Size个权值已经存在H->Elements[]->Weight里 */
int i; HuffmanTree T;
BuildMinHeap(H); /*将H->Elements[]按权值调整为最小堆*/
for (i = 1; i < H->Size; i++) { /*做H->Size-1次合并*/
T = malloc( sizeof( struct TreeNode) ); /*建立新结点*/
T->Left = DeleteMin(H);
/*从最小堆中删除一个结点,作为新T的左子结点*/
T->Right = DeleteMin(H);
/*从最小堆中删除一个结点,作为新T的右子结点*/
T->Weight = T->Left->Weight+T->Right->Weight;
/*计算新权值*/
Insert( H, T ); /*将新T插入最小堆*/
}
T = DeleteMin(H);
return T;
}
特点:
- 没有度为 1 的结点;
- n 个叶子结点的哈夫曼树共有 2n - 1 个结点;
- 哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树;
- 对于同一组权值 {w1,w2,…,wn},可能存在不同构的哈夫曼树,但WPL一定相同,例子如下。
哈夫曼编码
不等长编码: 出现频率高的字符用的编码短些,出现频率低的字符则编码长些。
用哈夫曼进行不等长编码: 可以避免编码的二义性,因为每个字符所在的结点均为叶子结点,不可能出现在别的字符的路径上,就不会出现二义性,即任何字符的编码都不是另一字符编码的前缀。
编码的二义性: 有些字符的编码可能是另一些字符编码的前缀。例如: a 编码为 1,b 编码为 0,c 编码为10。那倘若现在给定编码为10,需要进行解码,就会出现二义性。10可以解码为 ab 和 c。
如下图所示,用二叉树进行编码:左边的二叉树编码为普通的不等长编码,WPL较大。右边为哈夫曼树(也算是一种二叉树)编码,可以保证达到最小的WPL,即编码代价最小。
哈夫曼树编码规则: 每次把权值最小的两个字母所在二叉树进行合并
集合及运算
集合的结构表示:
将一个集合里的元素构造成一棵树,用双亲表示法,即孩子指向双亲(之前学的树都是父亲指向左右儿子)。树再用数组进行表示。
数据描述:
采用数组存储形式。Parent处为 -1 则表示根节点;非负数表示双亲节点的下标。可以将Parent为负数进行改进,比如用 -7 表示这个集合里的元素个数。表示如下:
集合运算
(1)查找某个元素所在集合(用根节点表示)
int Find( SetType S[ ], ElementType X )
{ /* 在数组S中查找值为X的元素所属的集合 */
/* MaxSize是全局变量,为数组S的最大长度 */
int i;
for ( i=0; i < MaxSize && S[i].Data != X; i++) ;//这一步时间复杂度很高T(N) = O(N^2)
if( i >= MaxSize ) return -1; /* 未找到X,返回-1 */
for( ; S[i].Parent >= 0; i = S[i].Parent ) ;
return i; /* 找到X所属集合,返回树根结点在数组S中的下标 */
}
并运算
思路:
1、分别找到X1和X2两个元素所在集合树的根结点
2、如果它们不同根,则将其中一个根结点的父结点指针设置成另一个根结点的数组下标。
void Union( SetType S[ ], ElementType X1, ElementType X2 )
{
int Root1, Root2;
Root1 = Find(S, X1);
Root2 = Find(S, X2);
if( Root1 != Root2 )S[Root2].Parent = Root1; //即当 x1 和 x2 不属于同一子集时,才需要合并。
}
改进: 为了改善合并以后的查找性能,可以进行按秩归并:
1、比规模:将规模小的集合合并到规模相对大的集合中。所以将根节点的 Parent 改成可以表示集合元素个数的负数。
2、比高度:将高度小的集合合并到高度相对大的集合中。所以将根节点的 Parent 改成可以表示集合的高度。
集合的应用
集合的简化表示:
将集合表示成数组形式存储,将集合的元素用下标的标号表示,而对应标号的数组内存储父节点的编号。可以减少存储空间,也可以减少查找操作的时间复杂度。
例如:
集合简化前操作:
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++) ; //这一步时间复杂度很高!!!T(N) = O(N^2)
if( i >= MaxSize ) return -1; /* 未找到X,返回-1 */
for( ; S[i].Parent >= 0; i = S[i].Parent ) ;
return i; /* 找到X所属集合,返回树根结点在数组S中的下标 */
}
集合的简化表示:
typedef int ElementType; /*默认元素可以用非负整数表示*/
typedef int SetName; /*默认用根结点的下标作为集合名称*/
typedef ElementType SetType[MaxSize];
SetName Find( SetType S, ElementType X ){ /* 默认集合元素全部初始化为-1 */
for ( ; S[X]>=0; X=S[X] ) ;//这里是默认 X 一定在某个集合里面,如果严谨一点,则要多判定一下X是否在集合中
return X;//返回的是要查找的元素 X 所在集合的父节点
}
void Union( SetType S, SetName Root1, SetName Root2 ){ /* 这里默认Root1和Root2是不同集合的根结点 */
S[Root2] = Root1; // 这里就是进行最简单的并集操作,没有进行按秩归并和路径压缩。
// 可能最终会退化成单向链表,查找的时间复杂度很大等等后果。
}
集合的 按秩归并 和 路径压缩 操作具体参考PTA的 05-树8 File Transfer
按秩归并:
根据树高进行归并: 将矮树贴到高树上
根据树的规模进行归并: 将规模小的树贴到规模大的树上
路径压缩: 用路径压缩,查找操作的时间复杂度能从 O( Nlog2N) 降为 O( Clog2N) ,其中C为常数。
注意! 路径压缩通常与按规模归并使用更方便。因为如果按树高归并的话,路径压缩会破坏树的高度,导致后续的归并变得比较复杂,还需要重新计算树高,而且计算复杂。但是路径压缩不会破坏树的规模,后续可以正常归并。