哈夫曼编码和哈夫曼树

本篇文章的读者是初学者,旨在帮助他们了解数据结构中的哈夫曼编码和哈夫曼树,同时也检验自己的水平,从而共同提高代码水平和效率,共勉之。
本文为读者详细介绍了哈夫曼编码的应用和哈夫曼树的实现。

哈夫曼编码

哈夫曼编码(Huffman Code)是一种变长编码,用于数据压缩。它是由David A. Huffman于1952年提出的,通过对不同符号赋予不同的变长编码来实现对数据的高效压缩。

基本思想是根据符号出现的频率,为频率较高的符号分配较短的编码,而为频率较低的符号分配较长的编码。这样做的目的是使得出现频率高的符号用较短的编码表示,从而减少整体编码长度,实现数据压

例子:哈夫曼编码与二进制编码在文本数据中的对比

考虑以下文本数据:“ABRACADABRA”

1. 二进制编码:

使用ASCII编码,每个字符都用8位二进制表示。

  • A: 01000001
  • B: 01000010
  • R: 01010010
  • C: 01000011
  • D: 01000100

整个字符串的二进制表示为:

0100000101000010010100100100000101000010010100100101001001000100
2. 哈夫曼编码:

首先,统计字符频率:

  • A: 5
  • B: 2
  • R: 2
  • C: 1
  • D: 1

构建哈夫曼树:(下面有过程)

         (11)
         /   \
      (7)   (4)
      / \    / \
     A   (2) R   B
        / \
       C   D

生成哈夫曼编码:(从根节点出发向左为’0’,向右为’1’)

  • A: 00
  • B: 11
  • R: 10
  • C: 010
  • D: 011

整个字符串的哈夫曼编码表示为:

001110000100011001110000100011
对比与分析:
  • 二进制编码需要固定的8位表示每个字符,而哈夫曼编码根据字符频率动态分配不同长度的编码,因此哈夫曼编码在这个例子中显著减少了总体的比特数。
  • 哈夫曼编码中字符’A’,由于频率最高,获得最短的编码,这在二进制编码中是无法实现的。
  • 对于频率较低的字符,哈夫曼编码引入了一定的额外开销,例如字符’C’和’D’。然而,总体上由于高频字符的短编码,哈夫曼编码在这个例子中实现了更好的压缩效果。

哈夫曼树(Huffman Tree)

哈夫曼树是一种带权路径长度最短的二叉树,常用于数据压缩。它的构建基于一组权值,每个权值表示一个字符在数据中出现的频率。构建哈夫曼树的目标是通过赋予权值较小的字符以较短的编码,而给予权值较大的字符以较长的编码,从而达到压缩数据的目的。

哈夫曼树的构建过程包括以下步骤:

  1. 初始化:给定一组权值,每个权值对应一个字符。初始时,每个字符都被视为一个只包含一个结点的树。
  2. 选择:在剩余的树中选择两棵树,它们的权值最小。这两棵树将成为哈夫曼树的左右子树。
  3. 合并:将选中的两棵树合并为一棵新树,新树的根结点的权值为两棵树根结点的权值之和。
  4. 重复:重复以上步骤,直到只剩下一棵树为止。这棵树就是哈夫曼树。

在哈夫曼树中,左子树的编码为0,右子树的编码为1。通过从根结点到每个叶子结点的路径,即可得到每个字符的哈夫曼编码。

哈夫曼编码的特点是没有编码是另一个编码的前缀,这被称为“前缀码”。这使得在解码时不会出现歧义。

在构建完成哈夫曼树后,可以通过遍历树的路径,从根结点到每个叶子结点,得到每个字符的哈夫曼编码。这些编码可以用于数据的压缩和解压。

2. 构建哈夫曼树

构建哈夫曼树的过程包括选择两个权值最小的结点,创建一个新的父节点,将这两个结点作为新节点的左右子节点,并将新节点的权值设置为左右子节点权值之和。这个过程一直重复,直到只剩下一个节点,这个节点就是哈夫曼树的根节点。

在构建过程中,可以使用优先队列(最小堆)来保持权值最小的两个结点始终在队列的前面。这确保了在每一步中我们都选择了最小的两个权值。

示例

考虑字符串 “ABRACADABRA”,我们首先计算字符的频率:

A: 5
B: 2
R: 2
C: 1
D: 1

然后按照哈夫曼树的构建规则,构建哈夫曼树:

  1. 选择最小的两个权值,创建一个新节点,更新权值:

       S2         A5        B2        R2
     /     \
    C1      D1
    
    S2	A5 B2 R2
    
  2. 再次选择最小的两个权值,创建一个新节点,更新权值:

            P4   A5  S2
         /     \
        R2      B2
    
  3. 继续这个过程,直到只剩下一个节点:

               E7     P4
            /     \ 
          A5      S2        
                /     \
              C1    D1
    
					11
				 /     \ 
               E7       P4
           /     \    /     \
          A5     S2   R2      B2    
              /     \
             C1    D1

可能存在差异性
这就是构建的哈夫曼树。通过遍历从根到每个叶子节点的路径,我们可以为每个字符构建哈夫曼编码:

这样,原始数据就可以通过这些短编码进行高效的压缩。

哈夫曼树代码讲解:

  1. 结构体定义:

    typedef struct {
        int weight;      // 结点的权值
        int parent, lchild, rchild;  // 结点的双亲、左孩子、右孩子的下标
    } HTNode, *HuffmanTree;
    

    这定义了一个结构体 HTNode 用于表示哈夫曼树中的节点。它包含了权值、双亲、左孩子和右孩子的字段。typedef 用于为指向 HTNode 的指针创建一个别名 HuffmanTree,使得使用动态数组更加方便。

  2. Select 函数:

    void Select(HuffmanTree HT, int k, int& s1, int& s2) {
        unsigned int minWeight = 10000;
        unsigned int secondMinWeight = 10000;
    
        for (int i = 1; i <= k; i++) {
            if (!HT[i].parent) {
                if (HT[i].weight < minWeight) {
                    secondMinWeight = minWeight;
                    minWeight = HT[i].weight;
                    s2 = s1;  // 将当前最小值更新为次最小值
                    s1 = i;
                } else if (HT[i].weight < secondMinWeight) {
                    secondMinWeight = HT[i].weight;
                    s2 = i;
                }
            }
        }
    }
    

    该函数在前 k 个结点中选择两个双亲域为0且权值最小的结点,并通过引用参数 s1s2 返回它们的下标。它通过遍历哈夫曼树的结点,根据权值找到最小和次最小的两个结点。

  3. CreateHuffmanTree 函数:

    void CreateHuffmanTree(HuffmanTree& HT, int n) {
        // 构造哈夫曼树
        if (n <= 1)
            return;
        int m = 2 * n - 1;
        HT = new HTNode[m + 1];
    
        // 初始化结点数组
        for (int i = 1; i <= m; i++) {
            HT[i].parent = 0;
            HT[i].lchild = 0;
            HT[i].rchild = 0;
            HT[i].weight = 0;
        }
    
        // 输入叶子结点的权值
        for (int i = 1; i <= n; i++) {
            cin >> HT[i].weight;
        }
    
        // 创建哈夫曼树
        for (int i = n + 1; i <= m; i++) {
            int s1 = 0, s2 = 0;
            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;
        }
    }
    

    该函数用于构建哈夫曼树。首先,动态分配一个包含 m+1 个结点的数组,然后初始化叶子结点的权值。接着,通过选择、删除、合并的过程构建哈夫曼树。

  4. PrintHuffmanTree 函数:

    void PrintHuffmanTree(HuffmanTree HT, int m) {
        cout << std::left << setw(10) << "i" << setw(10) << "weight" << setw(10) << "parent" << setw(10) << "lchild" << setw(10) << "rchild" << endl;
        for (int i = 1; i <= m; i++) {
            cout << setw(12) << i << setw(11) << HT[i].weight << setw(10) << HT[i].parent << setw(10) << HT[i].lchild << setw(10) << HT[i].rchild << endl;
        }
    }
    

    该函数用于打印哈夫曼树的节点信息,包括节点序号、权值、双亲、左孩子和右孩子的下标。

  5. PrintHuffmanCodes 函数:

    void PrintHuffmanCodes(HuffmanTree HT, int n) {
        string code;
        for (int i = 1; i <= n; i++) {
            int current = i;
            code = "";
            
            // 从叶子节点向上遍历树以找到编码
            while (HT[current].parent != 0) {
                if (HT[HT[current].parent].lchild == current) {
                    code = '0' + code;  // '0' 代表左孩子
                } else {
                    code = '1' + code;  // '1' 代表右孩子
                }
                current = HT[current].parent;
            }
    
            cout << "结点 " << i << " 的编码: " << code << endl;
        }
    }
    

    该函数用于打印哈夫曼树中每个叶子节点的编码。它从叶子节点向上遍历树以找到对应的编码,并输出结果。

  6. 主函数 main:

    int main() {
        HuffmanTree HT = new HTNode;
        int n = 0;
        cin >> n;
        int m = 2 * n - 1;
        CreateHuffmanTree(HT, n);
        PrintHuffmanTree(HT, m);
        cout << "哈夫曼编码:" << endl;
        PrintHuffmanCodes(HT, n);
    }
    

    主函数首先创建了一个 HuffmanTree 类型的变量 HT,然后输入结点个数 n。接着,调用 CreateHuffmanTree 构建哈夫曼树,然后使用 PrintHuffmanTree 打印哈夫曼树的结点信息,最后使用 PrintHuffmanCodes 打印每个叶子节点的哈夫曼编码。

    完整代码:

#include<iostream>
#include<iomanip>
using namespace std;
typedef struct {
	int weight;//结点的权值
	int parent, lchild, rchild;//结点的双亲、左孩子、右孩子的下标
}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树
/*----------选择两个权值最小的结点-----------*/
void Select(HuffmanTree HT, int k, int& s1, int& s2) {
unsigned int minWeight = 10000;
unsigned int secondMinWeight = 10000;

for (int i = 1; i <= k; i++) {
    if (!HT[i].parent) {
        if (HT[i].weight < minWeight) {
            secondMinWeight = minWeight;
            minWeight = HT[i].weight;
            s2 = s1;  // 将当前最小值更新为次最小值
            s1 = i;
        } else if (HT[i].weight < secondMinWeight) {
            secondMinWeight = HT[i].weight;
            s2 = i;
        }
    }
}

}
/*-------构造哈夫曼树-------*/
void CreateHuffmanTree(HuffmanTree& HT, int n) {
	//构造哈夫曼树
	if (n <= 1)
		return;
	int m = 2 * n - 1;//一棵有n个叶子节点的哈夫曼树共有2*n-1个结点
	HT = new HTNode[m + 1];//0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
	for (int i = 1; i <= m; i++)
	{//将1-m号单元中的双亲、左孩子、右孩子的下标都初始化为0
		HT[i].parent = 0;
		HT[i].lchild = 0;
		HT[i].rchild = 0;
		HT[i].weight = 0;
	}
	for (int i = 1; i <= n; i++) 
	{//输入前n个单元中叶子结点的权值
		cin >> HT[i].weight;
	}
	/*--------初始化工作结束,下面开始创建哈夫曼树------*/
	for (int i = n+1; i <= m; i++)
	{//通过n-1次的选择、删除、合并来创建哈夫曼树
		int s1=0, s2=0;
		Select(HT, i - 1, s1, s2);//在HT[k](1<=k<=i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
		HT[s1].parent = i; HT[s2].parent = i;
		//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为i
		HT[i].lchild = s1; HT[i].rchild = s2;//s1,s2分别作为i的左右孩子
		HT[i].weight = HT[s1].weight + HT[s2].weight;//i的权值为左右孩子权值之和
	}
}
/*----------打印哈夫曼树---------*/
void PrintHuffmanTree(HuffmanTree HT,int m) {
	cout <<std::left << setw(10) << "i" << setw(10) << "weight" << setw(10) << "parent" << setw(10) << "lchild" << setw(10) << "rchild" << endl;
	for (int i = 1; i <= m; i++)
	{
		cout << setw(12) << i << setw(11) << HT[i].weight << setw(10) << HT[i].parent << setw(10) << HT[i].lchild << setw(10) << HT[i].rchild << endl;
	}
}

void PrintHuffmanCodes(HuffmanTree HT, int n) {
    string code;
    for (int i = 1; i <= n; i++) {
        int current = i;
        code = "";
        
        // 从叶子节点向上遍历树以找到编码
        while (HT[current].parent != 0) {
            if (HT[HT[current].parent].lchild == current) {
                code = '0' + code;  // '0' 代表左孩子
            } else {
                code = '1' + code;  // '1' 代表右孩子
            }
            current = HT[current].parent;
        }

        cout << "结点 " << i << " 的编码: " << code << endl;
    }
}


/*--------主函数--------*/
int main() {
	HuffmanTree HT = new HTNode;//定义一个哈夫曼树变量
	int n=0;
	cin >> n;//输入结点个数
	int m = 2 * n - 1;//m为哈夫曼树的所有结点的个数
	CreateHuffmanTree(HT, n);//构建一个哈夫曼树
	PrintHuffmanTree(HT, m);//输出哈夫曼树
	cout << "哈夫曼编码:" << endl;
    PrintHuffmanCodes(HT, n);
}

  • 20
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值