目录
四、哈夫曼树的动画演示(Microsoft Visual Studio2010)
一、什么是哈夫曼树
1. 哈夫曼树的基本概念和特性。
哈夫曼树(Huffman Tree),也被称为最优二叉树,是一种特殊的带权路径长度最短的二叉树。它用于实现哈夫曼编码,这是一种无损压缩算法。哈夫曼树的特点是:
-
它是一个满二叉树,即每个节点要么是叶节点,要么有两个子节点。
-
叶节点表示实际的数据元素(如字符),而内部节点不携带数据信息。
-
树中没有度为1的节点。
2. 哈夫曼树的构建方法。
哈夫曼树通过以下步骤构建:
2.1. 初始化:根据给定的数据集(例如字符及其频率),创建一个包含所有单个节点的最小堆(优先队列)。
2.2 合并操作:重复地从堆中取出两个具有最小权重的节点,创建一个新的内部节点,其权重等于这两个节点权重之和,并将新节点放回堆中。
2.3 结束条件:当堆中只剩下一个节点时,这便是完整的哈夫曼树。
3. 如何使用哈夫曼编码进行数据压缩。
哈夫曼编码利用哈夫曼树来生成针对每个字符的编码规则。编码规则如下:
-
每个左分支代表0,右分支代表1。
-
从根节点到任意叶节点的路径上的所有边构成该叶节点所代表字符的编码。
-
编码后的数据量通常比原始数据要小,因为常用字符会被分配更短的编码。
4. 哈夫曼树的定义和特点。
哈夫曼树是由一系列带权值的节点组成的二叉树,其特点是带权路径长度最小。
是指所有叶子节点的带权路径长度之和,计算公式为:
其中是第
个叶节点的权重,
是从根到该叶节点的路径长度。
5. 权重、路径长度的概念及其重要性
-
权重(Weight):在哈夫曼树中,权重通常是字符出现的频率或概率。
-
路径长度(Path Length):指从树的根节点到特定节点的距离,用边的数量衡量。
这些概念对于确定如何最佳地压缩数据至关重要,因为它们直接影响了最终编码的效率。
6. 构建哈夫曼树的基本原则和算法思想
构建哈夫曼树遵循贪心算法的思想,即每一步都选择当前最优解,而不考虑未来的后果。基本原则是:
-
总是选取两个最小权重的节点进行合并。
-
新创建的节点总是作为父节点,原来的两个节点成为它的左右孩子。
-
确保最终形成的树具有最小的带权路径长度。
7. 哈夫曼编码规则以及其优越性解释
哈夫曼编码的主要优势在于它是前缀码,这意味着任何字符的编码都不是另一个字符编码的前缀。这种特性保证了解码过程中的唯一性和准确性。此外,由于常用字符被分配了较短的编码,所以整体上减少了存储空间或传输所需的比特数,实现了高效的数据压缩。同时,哈夫曼编码是无损压缩方法的一种,可以在解压后完全恢复原始数据。
二、构建哈夫曼树
构建哈夫曼树是一个经典的数据压缩算法,它使用贪心策略来创建一个二叉树,该二叉树用于编码字符。每个叶节点代表一个字符,而路径上的每条边代表0或1。这个过程涉及到最小堆(优先队列)的使用,以有效地找到两个具有最低频率的节点进行合并。
1.创建最小堆
- 假设我们有以下字符及其出现频率:
A: 45, B: 13, C: 12, D: 16, E: 9, F: 5
- 为每个字符创建一个节点,并将它们加入到最小堆中。最小堆内容:
[F:5, E:9, C:12, D:16, B:13, A:45]
- 哈夫曼树状态:
此时,哈夫曼树尚未开始构建。
2. 合并两个最小频率节点
- 从最小堆中取出两个最小频率的节点,创建一个新的内部节点,其频率是这两个节点频率之和,并将其放回最小堆中。
最小堆内容:
[新节点(FE):14, C:12, D:16, B:13, A:45]
- 哈夫曼树状态:
(FE)
/ \
F E
3. 重复2
- 再次执行上述操作,直到最小堆中只剩下一个节点。
最小堆内容:
[新节点(CFE):26, D:16, B:13, A:45]
- 哈夫曼树状态:
(CFE)
/ \
(FE) C
/ \
F E
4. 继续合并
- 最小堆内容:
[新节点(BCFE):39, A:45]
- 哈夫曼树状态:
(BCFE)
/ \
(CFE) B
/ \
(FE) C
/ \
F E
5. 构建最终的哈夫曼树
最后,当最小堆中只剩下最后一个节点时,这便是完整的哈夫曼树。
- 最小堆内容:
[最终节点(ABCDEF):84]
- 哈夫曼树状态:
(ABCDEF)
/ \
(BCFE) A
/ \
(CFE) B
/ \
(FE) C
/ \
F E
三. 生成哈夫曼编码
哈夫曼编码生成是基于构建好的哈夫曼树,通过从根节点到叶子节点的路径来为每个字符分配一个唯一的二进制编码。以下是详细的步骤和方法:
1. 前提条件
- 已经根据字符频率构建好了哈夫曼树。每个叶子节点代表一个字符,并且已经确定了它的位置。
2. 编码规则设定
-
约定:通常情况下,左分支用'0'表示,右分支用'1'表示。这个约定不是固定的,可以根据具体实现进行调整,但是一旦确定下来在整个编码过程中必须保持一致。
3. 生成编码
- 遍历哈夫曼树:从根节点开始,沿着树的路径向下走直到到达叶子节点。
- 记录路径:每经过一条边(分支),就将对应的编码位('0'或'1')添加到当前路径的编码序列中。
- 到达叶子节点:当到达叶子节点时,该路径上的所有编码位构成的就是该叶子节点所代表字符的哈夫曼编码。
4. 创建编码表
- 收集编码对:对于每一个叶子节点,将其字符与相应的哈夫曼编码配对,并存入编码表中。
- 编码表用途:编码表用于在实际编码过程中快速查找字符对应的哈夫曼编码;同样,在解码过程中也用来将编码转换回原始字符。
5. 示例
假设我们有如下简单的哈夫曼树:
(root)
/ \
A(0) B(10)
/
C(11)
其中,字符'A'的编码是'0',字符'B'的编码是'10',字符'C'的编码是'11'。
6. 实际编码过程
- 输入数据:要被编码的数据串。
- 编码操作:对于数据串中的每一个字符,在编码表中查找对应的哈夫曼编码,并将这些编码依次连接起来形成最终的编码结果。
7. 注意事项
如果哈夫曼树中有多个频率相同的字符,则它们之间的相对位置可以影响最终生成的编码。为了保证编码的一致性,应该在构建哈夫曼树时设定好明确的规则来处理这种情况。在某些应用中,可能需要同时保存哈夫曼树或者编码表以便于解码过程。
通过上述步骤,可以有效地生成哈夫曼编码。这种方法能够确保编码的唯一性和最小化加权路径长度,从而达到高效的数据压缩效果。
四、哈夫曼树的演示(Microsoft Visual Studio2010)
动画演示
核心代码
-
最小堆的创建
// 创建一个空的最小堆,长度为maxSize
MinHeap createHeap(int maxSize)
{
MinHeap H = (MinHeap) malloc(sizeof(struct HNode)); //为堆中的数组分配内存
H->data = (HuffmanTree *) malloc((maxSize + 1) * sizeof(HuffmanTree));//堆的数据为哈夫曼
H->size = 0;
H->capacity = maxSize;
HuffmanTree t = (HuffmanTree) malloc(sizeof(struct HTNode));//创建哈夫曼树结点
//t->Weight = MAXDATA;
t->data = '!';
t->Weight = 0;
H->data[0] = t; // 堆中数组[0]用作哨兵,指向哈夫曼树结点
printf("创建空的最小堆,容量%d 当前元素数量 %d ,哈夫曼树结点哨兵的权%d \n", H->capacity,H->size ,H->data[0]->Weight);
return H;
}
// 判断堆是否已满,已满返回true 否则返回false
bool isFull(MinHeap minh)
{
return (minh->size == minh->capacity);//当前数量是否等于容量
}
// 插入元素到最小堆中 若堆已满,返回false,否则,插入后返回true
bool insert(MinHeap minh, HuffmanTree x)
{
printf("\n插入操作 \n");
int i;
if (isFull(minh))
{
return false;
}
i = ++(minh->size); // i 指向插入后堆中的最后一个元素的位置
printf("找到最小堆的最后一个位置%d \n",i);
// 将x上滤至合适的位置 需要保证任意节点的权值不大于其父结点
//对于下标为i的结点,其父结点坐标为i/2,其左子结点为2i,右子结点为2i+1
for (; (minh->data[i / 2]->Weight) > (x->Weight); i /= 2) //只要新元素x的权小于其父
{
printf("该结点不符合最小堆要求: 字符%c 权%d 下标 %d \n" , x->data , x->Weight , i);
minh->data[i] = minh->data[i / 2]; //其父结点就向下移动(或者说新元素下回合与祖父比较)
printf("i上的内容: 下标%d 的位置上是字符%c 权%d \n",i, minh->data[i]->data , minh->data[i]->Weight );
printf("i/2上的内容(其父): 下标%d 的位置上是字符%c 权%d \n",i/2, minh->data[i/2]->data , minh->data[i/2]->Weight );
}
//结束循环后,i就是新元素的正确位置。
minh->data[i] = x; // 将x插入
printf("插入完毕后 下标%d 的位置上是字符%c 权%d \n",i, minh->data[i]->data , minh->data[i]->Weight );
return true;
}
// 删除并返回最小堆中的最小元素,并保证了删除堆的最小元素后,堆仍保留性质
HuffmanTree deleteMin(MinHeap minh)
{
int parent, child;
HuffmanTree MinItem, X;
if (minh->size == 0)
return NULL; // 若堆已空,返回NULL
MinItem = minh->data[1]; // 取出根节点存放的最小值
printf("\n删除结点的操作:\n 取最小权值 %d 字母 为 %c \n" , MinItem->Weight , MinItem->data);
int index = minh->size--;
X = minh->data[index]; // 取出最后一个元素
printf("最后一的元素 [%d] 字母 %c 权值为%d \n\n" ,index , X ->data , X ->Weight);
//从根结点开始,用堆中最后一个元素向上过滤下层结点
for (parent = 1; parent * 2 <= minh->size ; parent = child)
{
child = parent * 2;
//比较x和其子结点的权值
if ((child != minh->size) && (minh->data[child]->Weight > minh->data[child + 1]->Weight))
child++; // child指向左右子节点的较小者
if (X->Weight <= minh->data[child]->Weight) break; // 当x权小于其子结点的较小者,找到了合适位置
else // 下滤X
minh->data[parent] = minh->data[child];
}
minh->data[parent] = X; // 将X插入
return MinItem;
}
//打印最小堆的操作
void printHeap(MinHeap minh)
{
printf("\n遍历最小堆:\n");
for(int i = 0; i <= minh->size; i++)
{
printf("位置 %d ,字符 %c ,权 %d\n" , i , minh->data[i]->data ,minh->data[i]->Weight);
}
printf("\n");
}
// 下滤操作,即将位于minh中位置P的元素下滤到合适位置,用于调整堆
void PercDown(MinHeap minh, int p)
{
int parent, child;
HuffmanTree X;
X = minh->data[p]; // 取出p位置存存放的值
for(parent = p; parent * 2 <= minh->size; parent = child)
{
child = parent * 2;
if((child != minh->size) && (minh->data[child]->Weight > minh->data[child + 1]->Weight))
child++; // child指向左右子节点的较小者
if(X->Weight <= minh->data[child]->Weight) break; // 找到了合适位置
else // 下滤X
minh->data[parent] = minh->data[child];
}
minh->data[parent] = X; // 将X插入
}
//构建一个最小堆
void BuildHeap(MinHeap minh)
{
int i;
// 从最后一个有孩子的节点开始,到根结点1
for(i = minh->size / 2; i > 0; i--)
{
// 对所有有孩子的节点进行调整
PercDown(minh, i);
}
printf("构建了一个最小堆,size为 %d\n", minh->size);
printHeap(minh);
}
-
构建哈夫曼树
//根据minh内最小堆的内容,构建一个哈夫曼树
HuffmanTree Huffman(MinHeap minh)
{
int i;
HuffmanTree T;
BuildHeap(minh);
int n = minh->size;
for(i = 1 ; i < n ; i++)
{
//printf("Huffman 中第%d次循环",i);
T = (HuffmanTree)malloc(sizeof(struct HTNode));//初始化新结点
T->lchild = deleteMin(minh);//从最小堆中删除一个结点,作为新结点的左孩子
//printf("从最小堆中删除%c 权%d 作为左结点\n",T->lchild->data,T->lchild->Weight);
//printHeap(minh);//打印最小堆
T->rchild = deleteMin(minh);//从最小堆中删除一个结点,作为新结点的左孩子
//printf("从最小堆中删除%c 权%d 作为右结点\n",T->rchild->data,T->rchild->Weight);
//printHeap(minh);//打印最小堆
T->Weight = T->lchild->Weight + T->rchild->Weight;
T->data = i + '0';//新结点的data是当前循环的次数(转字符)
//printf("新结点取名为 %c 权%d",T->data , T->Weight);
insert(minh,T);//新结点插入最小堆
//printHeap(minh);//打印最小堆
//动画显示当前的哈夫曼树
draw_huffman_tree(T , EASYX_WIDTH / 2 , 20 ,0 , 100 , 0);
Sleep(1000);
cleardevice();
}
T = deleteMin(minh);//最小堆中剩下的节点就是最终的哈夫曼树
return T;
}
//哈夫曼树的前序遍历
void PreOrderTraverse(HuffmanTree pTree)
{
if(pTree==NULL)
return;
printf("%c",pTree->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(pTree->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(pTree->rchild); /* 最后先序遍历右子树 */
}
int main()
{
//printf("哈夫曼树程序测试\n");
init_display_window();
int freq[] = {3, 6, 2, 4, 5, 7, 8, 9, 10, 11, 12};
char chars[] = {'W', 'A', 'N', 'G', 'M', 'I', 'N', 'G', 'Y', 'U', 'E'};
MinHeap minh = createHeap(MAXSIZE);//建立空的最小堆
//将测试数据插入最小堆
for(int i = 0 ; i < sizeof(chars)/sizeof(chars[0]); i++)
{
//printf("第%d次循环: ",i);
HuffmanTree ht = (HuffmanTree)malloc(sizeof(struct HTNode));//初始化新结点
ht->data = chars[i];
ht->Weight = freq[i];
ht->lchild =NULL;
ht->rchild =NULL;
insert(minh,ht);
}
//使用最小堆生成哈夫曼树
HuffmanTree huffTree = Huffman(minh);
//printf("最终生成的哈夫曼树字符: %c 权%d",huffTree->data , huffTree->Weight);
//PreOrderTraverse(huffTree);
draw_huffman_tree(huffTree , EASYX_WIDTH / 2 , 20 ,0 , 100 , 0);
end_display();
//system("pause");
return 0;
}