哈夫曼树(最优二叉树)
判断树:用于描述分类过程的二叉树
基本概念
路径: 从树中的一个结点到另一个结点之间的分支构成这两个结点的路径
结点的路径长度: 两个结点间路径上的分支数
树的路径长度: 从树根到每个结点的路径长度之和
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树,路径长度最短的二叉树不一定是完全二叉树
权: 将树中结点赋给一个有某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度: 从根结点到该节点之间的路径长度与该节点的权的乘积
树的带权路径长度(wpl): 树中所有叶子结点的带权路径长度之和
哈夫曼树是带权路径长度最短的二叉树
哈夫曼树中权值越大的叶子离根越近
具有相同带权结点的哈夫曼树不唯一
构造算法
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
哈夫曼算法:
- 根据n个给定的权值构成n棵二叉树的森林F
- 选取两棵根结点的权值最小的数作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和
- 在森林中删除这两棵树,同时将新得到的二叉树加入森林
- 重复2、3,直到森林中只有一棵树为止,这棵树即为哈夫曼树
哈夫曼树的结点是度数为0或2,没有度数为1的结点
包含n个叶子结点的哈夫曼树中共有2n-1个结点,要经过n-1此合并才能形成哈夫曼树,共产生n-1个新结点
算法实现
采用顺序存储结构——一维结构数组
只有一个parent值为0时结束
结点定义:
typedef struct
{
int weight;
int parent,lch,rch;
}HTNODE,*HTree;
初始化:
void CreatHTree(HTree HT,int n)
{
if(n<=1)return;
m=2*n-1;//数组一共有2n-1个元素
HT=(HTree)calloc(m+1,sizeof(HTNODE));//0号单元未用,HT[m]表示根结点
int i;
for(i=1;i<=m;i++){
HT[i].parent=0;
HT[i].lch=0;
HT[i].rch=0;
}
for(i=1;i<=n;i++){
scanf("%d",&HT[i].weight);
}
因为在数组中叶子结点的左右孩子是0,根结点的父结点是0,若是用数组中下标为0元素存储结点信息,那么将不能区分左右孩子为0的结点是叶子结点还是说该结点的左右孩子是下标为0的结点,同时也不知道哈夫曼树的根结点到底是谁
进行以下n-1次合并,依次产生n-1个结点HT[i],i=n+1……:
- 在HT[1,2,……,i-1]中选取两个未被选过(从parent==0的结点中选)的weight最小的两个结点HT[s1],HT[s2]
- 修改HT[s1]HT[s2]的parent值
- 修改产生新的HT[i]:
HT[i].weight=HT[s1].weight+HT[s2].eight;
HT[i].lch=s1;HT[i].rch=s2;
//接上文
for(i=n+1;i<=m;i++){
Select(HT,i-1,s1,s2)//在HT[k](1<=k&&k<=k-i)中选取两个其双亲域为0,且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;//表示从F中删除s1,s2
HT[s2].parent=i;
HT[i].lch=s1;//s1,s2分别表示左右孩子
HT[i].rch=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;//i的权值为左右孩子权值之和
}
}
哈夫曼编码
题型:将字符转换成由二进制组成的字符串
哈夫曼编码:
-
统计字符集中每个字符在电文中出现是概率(概率越大,要求编码越短)
-
利用哈夫曼树特点:权越大的叶子离根越近,将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短
-
在哈夫曼树的每个分支上标1或0
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
哈夫曼编码能保证是前缀编码:没有一片叶子是另一片叶子的祖先,所以每个叶结点的编码就不可能是其他叶结点编码的前缀
能保证字符编码总长最短:哈夫曼树带权路径长度最短,故字符编码的总长最短
哈夫曼编码是最优前缀码