-
正则二叉树 / 严格二叉树
没有度为1的结点
的树。 -
赫夫曼树
就是一种正则二叉树 / 严格二叉树
。
报文编码问题
-
电报 : 将
要传送的文字
转换成二进制数串
。
如电文中包含四种字符 { A , B , C , D },那么只需要两位二进制数即可完成编码。 -
在传送电报时,总是希望总长尽可能短。
如果每个字符出现频率相同,那么对每个字符采用一样的编码位数对文本总长无影响。
如果每个字符出现频率不同,那么可以对出现频率高的字符采用较短位数的编码,对出现频率低的字符的编码位数可以多一些。 -
前缀编码
有这么一种编码方式,任意一个字符的编码都不是另一个字符的编码的前缀,叫做前缀编码。 -
可以利用二叉树来设计二进制的前缀编码。
因为到二叉树的叶子结点的路径
必不可能是到另一个叶子结点的路径
。所以不存在前缀关系。 -
利用二叉树来获取报文字符 { A , B , C , D } 的报文最短编码 (赫夫曼码)
-
如上图,设叶子结点的权值就是对应报文字符在报文中出现的次数。
那么电文总长
就是上述二叉树的带权路径长度
∑ Wi * Li,也就是报文编码的总长度
。 -
赫夫曼编码 / 最优前缀编码
设计一棵赫夫曼树,由此得到的二进制前缀编码的方式。
具体做法
- 赫夫曼树中没有度为1的结点,是一种
正则二叉树
,因此一棵有n
个叶子结点的赫夫曼树共有2n-1
个结点。
编码需要从叶子结点
出发,找到一条从叶子到根的路径
。
译码需要从根结点
出发,找到一条从根到叶子的路径
。
因此,对于每个结点而言,既需要知道双亲
的信息,又需要知道孩子结点
的信息。
由此引出结点的存储结构
,如下
typedef struct
{
// 结点数据域 存储 结点权重
unsigned int weight;
// 父母结点下标,左子树下标,右子树下标(在顺序表中的下标)
unsigned int parent, lchild, rchild;
} HTNode, *HuffmanCode;
typedef char* *HuffmanCode;
// 求报文赫夫曼编码的算法
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n)
{
// w 存放n个报文字符的权重值. HT 为已知的赫夫曼树
// HC 为字符数组,存放赫夫曼树的叶子结点的编码
if(n <= 1)
{
return;
}
m = 2 * n - 1;
// 0号单元弃用
HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));
// HT 的 1到n个位置 用来存放(叶子)结点,初始化它们的权重
for(p - HT, i = 1;
i <= n;
i++, p++, w++)
{
*p = { *w, 0, 0, 0 };
}
// HT 剩下来的结点用来存放遍历过程中产生的分支结点
for(; i <= m; i++, p++)
{
*p = { 0, 0, 0, 0 };
}
// 建立赫夫曼树
for(i=n+1; i<=m; i++)
{
// 在 [1, n-1] 选择 parent 为 0 且 weight 最小的两个结点
// 结点序号存入 s1, s2
Select(HT, i-1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i;
// 从 HT[n+1] 开始,依次存放新建分支结点的信息
// HT 最后一个单元表示根结点
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
// HC 是一个字符串数组,用于存放n个叶子结点编码串的n个起始地址
HC = (HuffmanCode)malloc((n+1) * sizeof(char *));
// 分配求编码的工作空间
cd = (char *)malloc(n * sizeof(char));
// 表示'\0' ,编码结束符
cd[n-1] = 0;
// 逐个字符求赫夫曼编码,
// 从叶子结点开始逆向求赫夫曼编码
for(i=1; i<=n; i++)
{
start = n-1;
// 以下实现一个叶子结点到根结点的路径
for(c=i, f=HT[i].parent;
f!=0; //只有根结点的parent域为0
c=f, f=HT[f].parent )
{
if(HT[f].lchild == c)
{
cd[--start] = '0';
}
else
{
cd[--start] = '1';
}
// 根据编码位数分配空间存储赫夫曼编码
HC[i] = (char *)malloc((n-start) * sizeof(char));
// 从工作区间 复制到 编码数组里面
strcpy(HC[i], &cd[start]);
}
}
}
- 无栈非递归遍历赫夫曼树。
typedef struct
{
unsigned int weight;
unsigned int parent, lchild, rchild;
} HTNode, *HuffmanCode;
typedef char* *HuffmanCode;
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n)
{
typedef struct
{
unsigned int weight;
unsigned int parent, lchild, rchild;
} HTNode, *HuffmanCode;
typedef char * *HuffmanCode;
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n)
{
if(n <= 1)
{
return;
}
// n个叶子结点 和 n-1个度为2的结点
m = 2 * n - 1;
// 0号单元弃用,下面从1号单元开始存放
HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));
for(p = HT+1, i = 1;
i <= n;
i++, p++, w++)
{
// 1到n号位置存放n个叶子结点
*p = { *w, 0, 0, 0 };
}
// 剩下的n-1个空位存放遍历过程中产生的分支结点,到最终的根结点
for(; i <= m; i++, p++)
{
*p = { 0, 0, 0, 0 };
}
// 从n+1号位置开始,依次放入遍历过程产生的分支结点
// m号位置也就是第m+1个单元存放根结点
for(i=n+1; i<=m; i++)
{
// 从 1 到 i-1 号单元中选出权值最小的两个结点,
// 下标赋值给 s1,s2
Select(HT, i-1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
// HC 存放 n个叶子结点编码串的n个起始地址
// 第 n+1 个存放'\0',表示编码结束符
HC = (HuffmanCode)malloc((n+1) * sizeof(char *));
p = m;
cdlen = 0;
// 清空上面产生的m个结点的weight域
// 结点的weight域将作为遍历时结点的状态标志
for(i=1; i<=m; i++)
{
HT[i].weight = 0;
}
// 从根结点到叶子结点
// 开始遍历,产生编码
while(p)
{
if(HT[p].weight == 0) // 0表示接下来访问结点左子树
{
// 访问完左子树,weight为1,
// 表示 下一次遇到该结点访问右子树
HT[p] = weight = 1;
if(HT[p].lchild != 0)
{
// 左子树存在,访问左子树,
// 该步编码值为 0
p = HT[p].lchild;
cd[cdlen++] = "0";
}
else if(HT[p].rchild == 0)
{
// 左子树不存在,且右子树不存在
// 成功到达一个叶子结点,完成一个结点的编码
HC[p] = (char *)malloc((cdlen + 1) * sizeof(char));
cd[len] = "\0";
strcpy(HC[p], cd);
}
}
// 为1 表示已访问完左子树,此时该访问右子树
else if(HT[p].weight == 1)
{
HT[p].weight = 2;
// 右子树不为0,则访问右子树根结点
// 此步码值为 1
if(HT[p].rchild != 0)
{
p = HT[p].rchild;
cd[cdlen++] = "1";
}
}
else
{
// 为2的情况,表示左右子树都已经访问完
// 开始回退一步
HT[p].weight = 0;
p = HT[p].parent;
--cdlen;
}
}
}