【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

时间:2021-10-10 15:10:18

一、熵编码概念:

熵越大越混乱

信息学中的熵:

  • 用于度量消息的平均信息量,和信息的不确定性
  • 越是随机的、前后不相关的信息,其熵越高

信源编码定理:

  • 说明了香农熵越信源符号概率之间的关系
  • 信息的熵为信源无损编码后平均码长的下限
  • 任何的无损编码方法都不可能使编码后的平均码长小于香农熵,只能使其尽量接近

熵与混乱程度:

混乱度越高的信源,越难以被压缩,需要更大量的信息来表示其排列顺序

【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

熵编码基本思想:

是使其前后的码字之间尽量更加随机,尽量减小前后的相关性,更加接近其信源的香农熵。这样在表示同样的信息量时所用的数据长度更短。

常用的熵编码算法:

  • 变长编码:哈夫曼编码 和 香农-费诺编码。运算复杂度低,但同时编码效率也低。
  • 算术编码:运算复杂,但编码效率高

二、哈夫曼编码基本原理:

1. 哈夫曼树简单介绍:

  • 哈夫曼编码是变长编码方法的一种,该方法完全依赖于码字出现的概率来构造整体平均长度最短的编码
  • 关键步骤:建立符合哈夫曼编码规则的二叉树,该树又称作哈夫曼树

哈夫曼树:

  • 一种特殊的二叉树,其终端节点的个数与待编码的码元的个数等同,而且每个终端节点上都带有各自的权值
  • 每个终端节点的路径长度乘以该节点的权值的总和称为整个二叉树的加权路径长度
  • 在满足条件的各种二叉树中,该路径长度最短的二叉树即为哈夫曼树

2. 哈夫曼树构建过程:

  1. 将所有左,右子树都为空的作为根节点。
  2. 在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。
  3. 从森林中删除这两棵树,同时把新树加入到森林中。
  4. 重复2,3步骤,直到森林中只有一棵树为止,此树便是哈夫曼树。

下面是构建哈夫曼树的图解过程:

【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

3. 哈夫曼编码:

利用哈夫曼树求得的用于通信的二进制编码称为哈夫曼编码。树中从根到每个叶子节点都有一条路径,对路径上的各分支约定指向左子树的分支表示”0”码,指向右子树的分支表示“1”码,取每条路径上的“0”或“1”的序列作为各个叶子节点对应的字符编码,即是哈夫曼编码。

以上图例子来说:

A,B,C,D对应的哈夫曼编码分别为:111,10,110,0

用图说明如下:

【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

4. 重要特点:

哈夫曼编码的任意一个码字,都不可能是其他码字的前缀。因此通过哈夫曼编码的信息可以紧密排列连续传输,而不用担心解码时的歧义性。

三、哈夫曼树的构建程序:

新建一个VS工程,起名为Huffman。这个程序用来将一个英文短文中的字母,按照出现次数多少,进行哈夫曼编码。(需自备一个英语短文,保存为txt格式,放到工程子目录下:xxx\Huffman\Huffman)

1. 首先编写打开和读取文件内容部分:

#include "stdafx.h"
#include <iostream>
#include <fstream> using namespace std; static bool open_input_file(ifstream &input, const char *inputFileName)
{
input.open(inputFileName);
if (!input.is_open())
{
return false;
}
return true;
} int _tmain(int argc, _TCHAR* argv[])
{
ifstream inputFile;
if (!open_input_file(inputFile, "input.txt"))
{
cout << "Error: opening input file failed!" << endl;
return -1;
} char buf = inputFile.get();
while (inputFile.good())
{
cout << buf;
buf = inputFile.get();
} inputFile.close();
return 0;
}

2. 统计字符出现频次:

创建一个结构体,保存字符及其频次:

typedef struct
{
unsigned char character;
unsigned int frequency;
} CharNode;

在之前的代码中添加统计部分:

	char buf = inputFile.get();
// 下面直接用字符的ascii码作为索引,一个ascii码占一个字节,共256种可能
CharNode nodeArr[256] = { {0,0} };
while (inputFile.good())
{
cout << buf;
nodeArr[buf].character = buf;
nodeArr[buf].frequency++;
buf = inputFile.get();
} cout << endl << endl;
for (int i = 0; i < 256; i++)
{
if (nodeArr[i].frequency > 0)
{
cout << "Node " << i << ": [" << nodeArr[i].character << ", " << nodeArr[i].frequency << "]" << endl;
}
}

输出如下(Node 10那块换行是因为,ascii中10对应的是“换行”):

【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

3. 根据频次对字符排序:

首先添加几个需要用到的库:

#include <queue>
#include <vector>
#include <string>

排序用到了priority_queue重载运算符 方面的知识,对此不了解的可以看以下几篇讲解:

http://blog.csdn.net/keshacookie/article/details/19612355

http://blog.csdn.net/xiaoquantouer/article/details/52015928

https://www.cnblogs.com/zhaoheng/p/4513185.html

然后定义一个哈夫曼树节点,并重载比较运算符:

// 哈夫曼树节点
struct MinHeapNode
{
char data; //字符
unsigned freq; //频次(权值)
MinHeapNode *left, *right; //左右子树
MinHeapNode(char data, unsigned freq) //构造函数
{
left = right = NULL;
this->data = data;
this->freq = freq;
}
};
typedef MinHeapNode MinHeapNode; struct compare
{
// 重载()运算符,定义youxai
bool operator()(MinHeapNode* l, MinHeapNode* r)
{
// 由小到大排列采用">"号,如果要由大到小排列,则采用"<"号
return (l->freq > r->freq);
}
};

将节点放入到优先级队列中(会自动排序):

	// 创建优先级队列,由小到大排列
priority_queue<MinHeapNode*, vector<MinHeapNode*>, compare> minHeap;
for (int i = 0; i < 256; i++)
{
if (nodeArr[i].frequency > 0)
{
minHeap.push(new MinHeapNode(nodeArr[i].character, nodeArr[i].frequency));
}
}

4. 构建哈夫曼树,并进行哈夫曼编码:

用排好的队列构建哈夫曼树:

	//用排好的队列实现哈夫曼树
MinHeapNode *leftNode = NULL, *rightNode = NULL, *topNode = NULL;
while (!minHeap.size != 1)
{
leftNode = minHeap.top();
minHeap.pop(); rightNode = minHeap.top();
minHeap.pop(); // 将合并节点的data设置为-1
topNode = new MinHeapNode(-1, leftNode->freq + rightNode->freq);
topNode->left = leftNode;
topNode->right = rightNode;
minHeap.push(topNode);
}

新建函数,对构建好的哈夫曼树进行哈夫曼编码:

static void get_huffman_code(MinHeapNode *root, string code)
{
if (!root)
{
return;
}
// 由于之前设置了合并节点的data为-1,因此检测到不是-1时,即为叶子结点,进行输出
if (root->data != -1)
{
cout << root->data << ": " << code << endl;
} // 递归调用
get_huffman_code(root->left, code + "0");
get_huffman_code(root->right, code + "1");
}

最后在主函数中调用此函数即可:

get_huffman_code(topNode, "");

编译运行程序,输出如下:

【视频编解码·学习笔记】7. 熵编码算法:基础知识 & 哈夫曼编码

由于文本不同,每个字符出现的频率不同,因此对于不同文本的编码也不同。这就要求在解码的时候,也需要有编码表才行。

完整程序如下:

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <queue>
#include <vector>
#include <string> using namespace std; // 每一个符号(字母和标点等)定义为一个结构体,包括字符和出现频次
typedef struct
{
unsigned char character;
unsigned int frequency;
} CharNode; // 哈夫曼树节点
struct MinHeapNode
{
char data; //字符
unsigned freq; //频次(权值)
MinHeapNode *left, *right; //左右子树
MinHeapNode(char data, unsigned freq) //构造函数
{
left = right = NULL;
this->data = data;
this->freq = freq;
}
};
typedef MinHeapNode MinHeapNode; struct compare
{
// 重载()运算符来定义优先级
bool operator()(MinHeapNode* l, MinHeapNode* r)
{
// 由小到大排列采用">"号,如果要由大到小排列,则采用"<"号
return (l->freq > r->freq);
}
}; static bool open_input_file(ifstream &input, const char *inputFileName)
{
input.open(inputFileName);
if (!input.is_open())
{
return false;
}
return true;
} static void get_huffman_code(MinHeapNode *root, string code)
{
if (!root)
{
return;
}
// 由于之前设置了合并节点的data为-1,因此检测到不是-1时,即为叶子结点
if (root->data != -1)
{
cout << root->data << ": " << code << endl;
} // 递归调用
get_huffman_code(root->left, code + "0");
get_huffman_code(root->right, code + "1");
} int _tmain(int argc, _TCHAR* argv[])
{
ifstream inputFile;
if (!open_input_file(inputFile, "input.txt"))
{
cout << "Error: opening input file failed!" << endl;
return -1;
} char buf = inputFile.get();
// 下面直接用字符的ascii码作为索引,一个ascii码占一个字节,共256种可能
CharNode nodeArr[256] = { {0,0} };
while (inputFile.good())
{
cout << buf;
nodeArr[buf].character = buf;
nodeArr[buf].frequency++;
buf = inputFile.get();
}
cout << endl << endl; // 创建优先级队列,由小到大排列
priority_queue<MinHeapNode*, vector<MinHeapNode*>, compare> minHeap;
for (int i = 0; i < 256; i++)
{
if (nodeArr[i].frequency > 0)
{
cout << "Node " << i << ": [" << nodeArr[i].character << ", " << nodeArr[i].frequency << "]" << endl;
minHeap.push(new MinHeapNode(nodeArr[i].character, nodeArr[i].frequency));
}
} //用排好的队列实现哈夫曼树
MinHeapNode *leftNode = NULL, *rightNode = NULL, *topNode = NULL;
while (minHeap.size() != 1)
{
leftNode = minHeap.top();
minHeap.pop(); rightNode = minHeap.top();
minHeap.pop(); // 将合并节点的data设置为-1
topNode = new MinHeapNode(-1, leftNode->freq + rightNode->freq);
topNode->left = leftNode;
topNode->right = rightNode;
minHeap.push(topNode);
} get_huffman_code(topNode, ""); inputFile.close(); return 0;
}