ハフマン木(Huffman Tree)とハフマン符号化の構築原理と方法を超詳しく解説し、それをコードで実現します。
1 ハフマン木の基本概念
パス: ツリー内のあるノードから別のノードへの分岐は、これら2 つのノード間のパスを構成します。
ノード パスの長さ: 2 つのノード間のパス上の分岐の数。
ツリーのパス長:ツリーのルートから各ノードまでのパス長の合計。書き込み:TL
重み(重み)は、重みとも呼ばれます。ツリー内のノードに特定の意味を持った値(具体的な意味はツリーが使用される場面に応じて決定されます)を割り当て、この値をノードの重みと呼びます。例えば、上記の判定ツリーにおいて、5%は、総人数に占める該当スコアセグメントの割合を表します
ノードの重み付き経路長:ルートノードからノードまでの経路長と、ノードまでの経路長の積ノードにかかる重量
ツリーの加重パス長:ツリー内のすべてのリーフ ノードの加重パス長の合計。
ツリーのパス長:ツリーのルートから各ノードまでのパス長の合計。
ハフマン ツリー:最適なツリー、最も短い加重パス長 (WPL) を持つツリー
「最短の重み付き経路長」は「同じ次数」の木同士を比較した結果であるため、最適二分木、最適三分木と呼ばれます。
ハフマン木:最適な二分木、最も短い加重経路長 (WPL) を持つ二分木。この木を構築するためのアルゴリズムは 1952 年にハフマン教授によって提案されたため、ハフマン木と呼ばれます。ハフマンアルゴリズム。
2. ハフマン木構築アルゴリズム
ハフマンアルゴリズム(ハフマン木の構築方法)
(1) n 個の指定された重み (W1、W2、...、Wn) に従って n 個のバイナリ ツリー F=(T1、T2、...、Tn) のフォレストを構築します。ここで、Ti の 1 つの重みのみが Wi、ルート ノードです。 。
構造林はすべて根です
(2) F では、ルート ノードの重みが最も小さい 2 つの木を左右のサブツリーとして選択し、新しい二分木を構築し、新しい二分木のルート ノードの重みを、そのルート ノードのルート ノードに設定します。左右のサブツリー ポイントの重みの合計。
小さな木を 2 つ選んで新しい木を作ります
(3) F のこれら 2 つのツリーを削除し、新しく取得したバイナリ ツリーをフォレストに追加します。
2 つの小さな追加を削除します
(4) 森の中にハフマンの木が 1 本だけになるまで (2) と (3) を繰り返します。
2と3を繰り返してルートを1つだけ残します
要約する
1. ハフマン アルゴリズムでは、最初は n 個のバイナリ ツリーがあり、最終的にハフマン ツリーを形成するには、これらを n-1 回マージする必要があります。
2. n-1 回のマージの後、n-1 個の新しいノードが生成され、これらの n-1 個の新しいノードはすべて 2 つの子を持つブランチ ノードになります。
ハフマン木には n+n-1 =2n-1 個のノードがあり、すべての分岐ノードの次数が 1 ではないことがわかります。
3. ハフマンツリーコードの実装を構築する
3.1 ハフマン木のノード構造
ハフマン ツリーを構築するときは、まずツリー内のノードの構成を決定する必要があります。
ハフマン ツリーの構築はリーフ ノードから開始され、ツリーのルートまで新しい親ノードが継続的に構築されるため、ノードには親ノードへのポインタが含まれている必要があります。ただし、ハフマン ツリーを使用する場合、ツリーのルートから開始して要件に応じてツリー内のノードをたどるため、各ノードにはその左右の子へのポインタが必要です。
//哈夫曼树结点结构
typedef struct {
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;
3.2 ハフマン木の探索アルゴリズム
ハフマン木を構築する場合、毎回各ノードの重み値に応じて値が最小の2つのノードをフィルタリングして二分木を構築する必要があります。
最小の重み値を持つ 2 つのノードを見つけるという考え方は、ツリー グループの開始位置から開始して、まず親ノードのない 2 つのノードを見つけます (ツリーの構築に使用されていないことを示します)。その後、親ノードなしでフォローアップします。ノードは順番に比較されます。考慮すべき状況は 2 つあります。
- 2 つのノードのうち小さい方のノードよりも小さい場合は、このノードを保持し、元の大きい方のノードを削除します。
- 2 つのノードの重み値の間にある場合は、元の大きい方のノードを置き換えます。
//HT数组中存放的哈夫曼树,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置
void Select(HuffmanTree HT, int end, int *s1, int *s2)
{
int min1, min2;
//遍历数组初始下标为 1
int i = 1;
//找到还没构建树的结点
while(HT[i].parent != 0 && i <= end){
i++;
}
min1 = HT[i].weight;
*s1 = i;
i++;
while(HT[i].parent != 0 && i <= end){
i++;
}
//对找到的两个结点比较大小,min2为大的,min1为小的
if(HT[i].weight < min1){
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
}else{
min2 = HT[i].weight;
*s2 = i;
}
//两个结点和后续的所有未构建成树的结点做比较
for(int j=i+1; j <= end; j++)
{
//如果有父结点,直接跳过,进行下一个
if(HT[j].parent != 0){
continue;
}
//如果比最小的还小,将min2=min1,min1赋值新的结点的下标
if(HT[j].weight < min1){
min2 = min1;
min1 = HT[j].weight;
*s2 = *s1;
*s1 = j;
}
//如果介于两者之间,min2赋值为新的结点的位置下标
else if(HT[j].weight >= min1 && HT[j].weight < min2){
min2 = HT[j].weight;
*s2 = j;
}
}
}
3.3 構築アルゴリズムの実装
//HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
void CreateHuffmanTree(HuffmanTree *HT, int *w, int n)
{
if(n<=1) return; // 如果只有一个编码就相当于0
int m = 2*n-1; // 哈夫曼树总节点数,n就是叶子结点
*HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0号位置不用
HuffmanTree p = *HT;
// 初始化哈夫曼树中的所有结点
for(int i = 1; i <= n; i++)
{
(p+i)->weight = *(w+i-1);
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
for(int i = n+1; i <= m; i++)
{
(p+i)->weight = 0;
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//构建哈夫曼树
for(int i = n+1; i <= m; i++)
{
int s1, s2;
Select(*HT, i-1, &s1, &s2);
(*HT)[s1].parent = (*HT)[s2].parent = i;
(*HT)[i].left = s1;
(*HT)[i].right = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
4. ハフマン符号化
4.1 ハフマン符号化の基本概念
リモート通信では送信する文字をバイナリ文字列に変換する必要があります
コードが異なる長さのバイナリ コードとして設計されている場合、つまり、送信する文字列内で頻繁に出現する文字をできるだけ短くコード化すると、変換されるバイナリ文字列の数が削減される可能性があります。
重要:異なる長さのコードを設計するには、任意の文字のコードが別の文字のコードの接頭辞であってはなりません。このコードは接頭辞コードと呼ばれます。
質問: メッセージの全長を最短にするプレフィックス コードは何ですか?
ハフマン符号化方式:
1.統計文字セット内の各文字がメッセージに出現する平均確率 (確率が高いほど、必要なコードは短くなります)
2. ハフマン木の特性を利用し、重みの大きい葉ほど根に近く、各文字の確率値を重みとしてハフマン木を構築します。確率が高いノードほどパスが短くなります。
3. ハフマン ツリーの各枝に 0 または 1 をマークします。
ノードの左のブランチは 0 とマークされ、右のブランチは 1 とマークされます。
ルートから各リーフまでのパス上のラベルを、リーフによって表される文字のエンコーディングとして接続します。
2 つの質問:
1. ハフマン エンコーディングがプレフィックス エンコーディングであることが保証されるのはなぜですか?
どのリーフも別のリーフの祖先ではないため、各リーフ ノードのコードが他のリーフ ノード コードのプレフィックスになることは不可能です。(文字はすべてリーフ ノードであり、文字へのルートは別の文字 T を渡しません)
2. なぜハフマン符号化は文字コードの全長の最短化を保証できるのでしょうか?
ハフマン木の重み付き経路長が最も短いため、文字コードの全長も最も短くなる。
プロパティ 1 ハフマン コードはプレフィックス コードです
特性 2 ハフマン符号化が最適なプレフィックス符号である
4.2 ハフマン符号化コードの実装
プログラムを使用してハフマン コードを見つけるには 2 つの方法があります。
- リーフノードからルートノードを見つけ、途中で通過したマークを逆記録します。たとえば、図 3 の文字 c のハフマン エンコードでは、ノード c からルート ノードが検索され、結果は 0 1 1 となるため、文字 c のハフマン エンコードは 1 1 0 (逆順出力) となります。
- ルートノードからリーフノードまで、途中で通過したマークを記録します。たとえば、図 3 の文字 c のハフマン コードを見つけるには、ルート ノードから開始し、シーケンスは 1 1 0 になります。
方法 1 を使用した実装コードは次のとおりです。
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
*HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
char *cd = (char *)malloc(n*sizeof(char)); //存放结点哈夫曼编码的字符串数组
cd[n-1] = '\0';//字符串结束符
for(int i=1; i<=n; i++){
//从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
int start = n-1;
//当前结点在数组中的位置
int c = i;
//当前结点的父结点在数组中的位置
int j = HT[i].parent;
// 一直寻找到根结点
while(j != 0){
// 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
if(HT[j].left == c)
cd[--start] = '0';
else
cd[--start] = '1';
//以父结点为孩子结点,继续朝树根的方向遍历
c = j;
j = HT[j].parent;
}
//跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
(*HC)[i] = (char *)malloc((n-start)*sizeof(char));
strcpy((*HC)[i], &cd[start]);
}
//使用malloc申请的cd动态数组需要手动释放
free(cd);
}
2 番目のアルゴリズムを使用した実装コードは次のとおりです。
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
*HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
int m=2*n-1;
int p=m;
int cdlen=0;
char *cd = (char *)malloc(n*sizeof(char));
//将各个结点的权重用于记录访问结点的次数,首先初始化为0
for (int i=1; i<=m; i++) {
HT[i].weight=0;
}
//一开始 p 初始化为 m,也就是从树根开始。一直到p为0
while (p) {
//如果当前结点一次没有访问,进入这个if语句
if (HT[p].weight==0) {
HT[p].weight=1;//重置访问次数为1
//如果有左孩子,则访问左孩子,并且存储走过的标记为0
if (HT[p].left!=0) {
p=HT[p].left;
cd[cdlen++]='0';
}
//当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
else if(HT[p].right==0){
(*HC)[p]=(char*)malloc((cdlen+1)*sizeof(char));
cd[cdlen]='\0';
strcpy((*HC)[p], cd);
}
}
//如果weight为1,说明访问过一次,即是从其左孩子返回的
else if(HT[p].weight==1){
HT[p].weight=2;//设置访问次数为2
//如果有右孩子,遍历右孩子,记录标记值 1
if (HT[p].right!=0) {
p=HT[p].right;
cd[cdlen++]='1';
}
}
//如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
else{
HT[p].weight=0;
p=HT[p].parent;
--cdlen;
}
}
}