数据压缩之贪心算法-赫夫曼编码

一、从压缩说起

       提起压缩这个概念,脑海中不禁会跳出这样一个情形:在不改变物体容量的前提下,减少物体的体积,使空间得以更有效的利用。在你外出旅行时,会将许多衣服放入行李     箱,你可以压一压衣服,让它们小到能被行李箱容纳,你压缩了衣服。之后,你打开行李箱,穿上衣服时,这就是所谓的解压。颇感欣慰的是,信息也能以同样的方式压缩,计算机文件和传输在互联网的信息都可以被压缩,以方便存储和传输,然后解压并以原始方式利用。我们日常听到的MP3格式文件常见的视频文件甚至是打电话时都用到了压缩,现下流行的zip压缩文件格式运用了精巧的压缩算法,计算机使用两种不同的压缩:无损压缩和有损压缩。接下来简要谈谈zip文件是怎么压缩和解压缩的。

二、无损压缩

   无损压缩顾名思义就是压缩不改变原始文件的体积,在解压后的文件和原始文件一模一样,计算机使用某种高效的算法吧文件压缩的更小,而不改变文件的本来面貌,究竟它是怎么办到的?我们先一起看一串文本数据:

                             AAAAAAAAABBBBBBBBBOKOKOKOKOKOKOKYESYESYESYES

如果让你来口述这些文本,你怎么描述呢?如果乍一看还不明显,思考一下你会如何通过电话向某人口述这份数据。和说"A A A A....."YESYES YES"不同的是,我肯定你会说9个A 9个B 7个OK 4个YES,在这个例子中,你将这个包含44个字符的串变成了说出9A9B7OK4YES 这11个字符压缩了25% 看起来压缩率还可以但不是绝对的称赞,但44个字符毕竟在实际应用中相比是非常小的甚至忽略不计,在一般的实际应用中压缩率可以高达50%以上! 非常不错,是不是计算机上的压缩文件都是用这种算法的呢?此言差矣! 也许读者会觉得如此轻松简单掌握这样的压缩算法未免太没有挑战性了,是的,简单的东西暴露出很多的缺陷。话说回来,这种办法在计算机科学中称之为前程长度编码(run-length encoding),因为它将重复的“行程”和行程“长度”编码在了一起。所不幸的是,它的价值只在压缩非常特殊的数据上管用,读者已经发现了,上面这个例子就是这么一个特殊的:数据中的重复片段必须相邻,譬如:ABACABAD就不能那这个办法使用了,还有一点,就是这个算法大部分和其他压缩算法结合起来使用。如何另一种也就是本博客的主题赫夫曼编码(Huffman Coding)结合起来使用,于是计算机科学家发明了一系列更成熟的算法:同前压缩(same-as-earlier trick)和更短符号压缩(shorter-symbol trick)。只需要这两个算法就能生成ZIP文件,由于篇幅所限,这里就介绍同前压缩了。

  •       同前压缩

   加入一下有一串你要处理的可怕任务,通过电话向某人口述如下数据:

                       VJGDNQMYLH-KW-VJGDNQMYLH-ADXSGF-OVJGDNQMYLH-ADXSGF-VJGDNQMYLH-EW-ADXSGF

总共有63个字母需要口述,“-”表示的是分隔符使更容易区分,假如你要逐个字符的往下念,你能保证不会说错或者说漏么?如果换了一个更长的字符串呢?

前10个字母我们没办法只能照着念:A、J、G、D、……H,然后K、W,接下来,发现到然后10个字母和一开始的10个字母一样,你可能会说:接下来10个字母和开始的10个字母重复,然后A、D、X、S、G、F、O 仔细观察O后面的16个字符又和开始的10个字母一样,于是我们想办法得到一个更简短的描述:往回数17个字母,抄到第16个字母,我们再换一种更加精炼的表达:back17 copy 16(b17c16)然后发现接下俩的10个字母也是重复的部分,因此 b16c10,再接下来两个字母没有重复,需要逐个口述为E、W最后的6个字母是之前的重复,可以b18c6

让我们总结一下这个压缩算法,我们用b代替back c代替copy原本的字符串被说成这样VJGDNQMYLH-KW-b12c10-ADXSGF-O-b17c16-b16c10-EW-b18c6

这个字符串只包含44个字母,节省了将近1/3。

还有许多压缩数据的算法,可参考《 Introduction to Data Compression》Khalid Sayood 数据压缩导论.

三、赫夫曼编码

              好了,该进入正题了,赫夫曼编码(Huffman coding) 从应用来说是一种数据压缩算法,从算法理论角度来说是一种贪心算法。所谓贪心算法就是分阶段的工作,在每一个阶段,可以认为所选择的决定是最优的,这种从当下得到最优的就决定而不考虑将来的后果的策略就是这种算法的来源。当算法结束时我们希望局部最优就是全局最优,如果是这样的话,那么算法就是正确的,否则就是一个次最优解。

                一、编码

                          

假如有一个文件,文件中包括了以上图所示的字符和出现的频率,当然每个字符需要编码姑且已上图为准吧,可以看到每个字符的编码为3位的定长编码,大小为678个比特位,在这里只表现了一般情况下的文件存储方式将每个数据编码为二进制的表示,根据每个数据出现的频率计算出总的二进制比特位长,这种方式有一个特点,即每个字符的编码都是随机给的比如a:000 ,b:001 从上到下依次有规律的递增,但这体现不出压缩的概念。

                1、前缀编码

                                 在电报传输中,电文的传送是被压缩成二进制串的,且尽可能的短,比如 a:0 ,b:01, c:11, d:001, e:010.假入要传送eaacdb一串数据,对应的二进制串为:010001100101 ,于是对方在接受时完全可能将开头的01译成b,接下来三个零译成a,然后两个一译成c然后为db于是整个译码就变成了baaacdb,与传送放发送的本意完全不对,信息接受错误。在这个例子中,最突出的问题是二进制码的前缀可能是别的字符二进制码的前缀,这样在译码的过程中完全可能出现混淆。为了避免这种混淆,出现了一种叫做前缀编码(prefix code)的技术,既没有任何二进制码是其他码字的前缀,前缀码的作用是简化解码的过程,由于没有码字是其他任何码字的前缀,编码文件的开始码字是无歧义的。我们可以简单的识别出开始码字,将其转换回原字符,然后对剩下的重复这种解码过程。但怎么构造这种前缀码?

一种二叉树可以解决问题。

在上面的电文传输例子中,我们让freq表示每个字符出现的频率,假定a.frea=20,b.freq=15,c.freq=40,d.freq=90,e.freq=55,于是我们可构造如下图所示的二叉树:

                                                       

我们把字符放在叶子节点上,一个字符的码字可以从根节点到字符节点开始的简单路径表示,其中0表示向左,1表示向右。

那么a:000,b:001,c:010,d:011,e:100可以证明每个字符的码字不是其他任何码字的前缀,实际上这也是开头对应的哪一张表用相同的方法组成的前缀码。所以来说,可构造这种将所有的字符节点放在叶子节点上,左分支为0,右分支为1的二叉树就是唯一的可表示成字符的前缀编码树。

解决了前缀码的问题但是这种编码还不是最优的,因为发现所有的字符编码为定长编码,达不到压缩的概念。为此我们定义树的代价:

                                                                                                      

其中,T表示一颗前缀码的树T,对于字母表C中每个字符c,令属性c.freq表示c在文件出现的频率,dT表示c在叶节点中的深度,则B(T)为编码文件需要的二进制位,

也称为B(T)为数的代价。

                                      

             2、构造赫夫曼编码

                              赫夫曼设计了一个贪新算法来构造最优前缀编码,称为赫夫曼编码(Huffman code),赫夫曼算法的过程可描述如下:

                                (1)、根据给定的n个频率值集合{f1,f2,f3,4,……,fn} 构成二叉树集合T={T1,T2,T3,T4,……,Tn};每颗二叉树都为根节点且左右子树为空

                               (2)、在T中选择两颗根节点频率值最小的树作为左右子树构造一颗新的二叉树,且新置的二叉树的根节点的频率值为左右孩子频率值之和

                               (3)、在T中删除这两颗树,同时将新得到的二叉树插入到T中。

                               (4)、重复步骤(2)和(3),直到T中只含一颗树为止。这棵树便是赫夫曼树。

                              以开头的文件的编码制作成的表作为例子,频率集合{a:20,b:93,c:45,d:27,e:8,f:33}按上面的步骤构造赫夫曼树的一个解如下图所示:


                                                                                                 

构造的赫夫曼树不是唯一的,每个字符的编码形式也不是唯一的,但每个字符的长度都是相同的,且是最优前缀编码。

              

            3、赫夫曼变编码的实现

                      要实现赫夫曼编码需要合适的数据结构,一种叫做静态三叉链表的二叉树结构可以方便的实现这种算法,赫夫曼节点的结构包括频率值双亲节点值,左右孩子值,双亲节点为0的是根节点,左右孩子值为0的是叶子节点,当叶子节点数确定时,赫夫曼树的节点数也就确定了。具体代码和注释如下:

<pre class="cpp" name="huyufei">
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#pragma warning(disable:4996)
typedef struct HTNode{
	unsigned int freq;
	unsigned int parent, left, right;
}*HuffmanTree;//动态顺序生成的赫夫曼树
typedef char** HuffmanCode;//动态顺序生成的赫夫曼编码
//在前k个二叉树根节点中选择一个频率值最小的,并返回这个根节点的序号。
int extract_min(HuffmanTree T, int k){
	int index;
	unsigned int m = UINT_MAX;//保存频率的最小值,初值为不小于所有的可能值
	for (int i = 1; i <= k; i++){
		if (T[i].freq < m&&T[i].parent == 0){
			m = T[i].freq;//保存频率小的值
			index = i;//保存频率小的序号
		}
	}
	T[index].parent = 1;//给选中的根节点的双亲赋非零值,以免重复查找这个节点。
	return index;//返回这个根节点频率最小的序号
}
/*
	根据频率节点freq生成赫夫曼树T,每个频率节点都有对应的编码HC;
	*/
void HuffmanCoding(HuffmanTree&T, HuffmanCode&HC, int *freq, int n){
	int i;
	HuffmanTree t;
	int total = 2 * n - 1;//n个叶子节点共需要2*n-1个节点构成赫夫曼树
	if ((T = (HuffmanTree)malloc(sizeof(HTNode)*(total + 1))) == NULL){//动态生成未赋值的赫夫曼树,0号空间未用
		printf("分配节点失败\n");
		exit(EXIT_FAILURE);
	}//如果节点分配失败,怎退出程序
	for (i = 1, t = T + 1; i <= n; t++, freq++, i++){//从1号单元开始给赫夫曼树的叶子节点初始化
		(*t).freq = *freq;
		(*t).left = 0;
		(*t).right = 0;
		(*t).parent = 0;
	}
	for (; i <= total; i++, t++){//剩下的内部节点将双亲域初始化为0
		(*t).parent = 0;
	}

	for (i = n + 1; i <= total; i++){//构造赫夫曼树
		int l = extract_min(T, i - 1);//从i-1个单元取最小的频率值
		int r = extract_min(T, i - 1);//从i-1个单元取最小的频率值
		T[i].left = l;
		T[i].right = r;
		T[l].parent = T[r].parent = i;
		T[i].freq = T[l].freq + T[r].freq;
	}
	HC = (HuffmanCode)malloc(sizeof(char*)*(n + 1));//分配n个叶子节点的编码空间,0号单元未用
	char*temp = (char*)malloc(sizeof(char)*n);//分配求一个字符编码的工作空间
	temp[n - 1] = '\0';//编码结束符
	int f = 0;//父节点序号
	for (i = 1; i <= n; i++){
		int start = n - 1;//编码结束符的位置
		for (int c = i, f = T[i].parent; f != 0; c = f, f = T[f].parent){
			if (T[f].left == c){
				temp[--start] = '0';//如果是左孩子则赋0
			}
			else{
				temp[--start] = '1';//如果是右孩子则赋1
			}
		}
		HC[i] = (char*)malloc(sizeof(char)*(n - start));//分配第i个字符空间
		strcpy(HC[i], &temp[start]);//将求得的编码赋给第i个字符空间
	}
	free(temp);//释放资源
}
int main(){
	int n;
	printf("请输入要构造的频率节点:\n");
	scanf_s("%d", &n);
	int *freq = (int *)malloc(sizeof(int)*n);
	printf("请依次给%d个节点赋值:\n", n);
	for (int i = 0; i < n; i++){
		scanf_s("%d", freq + i);
	}

	HuffmanTree T;
	HuffmanCode HC;
	HuffmanCoding(T, HC, freq, n);


	printf("以下是赫夫曼树节点之间的关系:\n");
 	printf("freq\tparent\tleft\tright\n");
 	for (int i = 1; i <= 2 * n - 1; i++){
  		printf("%d\t%d\t%d\t%d", T[i].freq, T[i].parent, T[i].left, T[i].right);
  		printf("\n");
 	}
	printf("--------------------------------------------\n");
 	for (int i = 1; i <= n; i++){
  		printf("频率为%d的编码为%s\n", T[i].freq, HC[i]);
	}
}

以上图构造的赫夫曼树为例,我们利用这个程序构造赫夫曼编码,运行结果如下所示:

       

到此为止,我们根据赫夫曼算法构造的编码为变长编码,定长编码不同的是,每个字符的编码不仅是前缀码而且是最短的编码,我们可以计算这课树的代价为520与原先678个比特位的代价相比确实压缩了,实际上,这是数据较少的情况,在大量数据前,赫夫曼编码表现出更加可观的压缩效率。

关于赫夫曼编码算法的正确性,这里我不打算赘述,有兴趣的读者可以参考《算法导论》一书。

                 四、压缩算法的起源

                 要追溯压缩算法的起源,我们要把科学史向前推进30年。我们已经了解了香农,那位以其1948年论文创建信息理论领域的贝尔实验室科学家。香农在就错码故事中的两位主要的英雄之一,他与1948年发明的总要论文除了许多卓越贡献之外,好包含对压缩技术之一的的描述。麻省理工学院教授罗伯特法诺大约在同时发明了这一技术,事实上,香农-法诺编码是一种实施更短符号编码的特殊方法,我们在前面描述了更短符号编码。我们很快会知道,香农-法诺编码很快就被另一种算法所取代,但这一方法非常有效,并存活到了今天,称为ZIP格式的可选压缩方法之一。

                             香农和法诺都意识到,尽管他们的方法都既实用又高效,但却不是最好的算法:香农通过算术证明了肯定有更好的压缩技术存在,但还未找到。同时,法诺在麻省理工学院教授一门信息理论的研究生课程,他将实现优化压缩的问题作为该课程学期论文的可选项之一。出人意料的是,法诺的以为学生解决了这个问题,得到了针对每个符号取得最佳可能压缩的方法。这名学生就是大卫-赫夫曼,他的技术———现在以赫夫曼编码来命名,——则是更短符号编码的另一个例子。赫夫曼编码仍是一种基础压缩算法,被广泛用于通信和数据存储系统。


 




            




  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: DS二叉树,也称赫夫曼树,是一种用于数据压缩的树形结构。它的构建过程是将一组权值作为叶子节点,通过不断合并权值最小的两个节点,最终形成一棵树,使得每个叶子节点到根节点的路径上的权值之和最小。 赫夫曼树的编码过程是将每个叶子节点的权值编码为一串二进制数,使得编码后的长度最短,从而实现数据压缩的目的。 以下是赫夫曼树的构建与编码的代码框架: ```python class Node: def __init__(self, value, weight): self.value = value self.weight = weight self.left = None self.right = None def build_huffman_tree(data): # 构建赫夫曼树 nodes = [Node(value, weight) for value, weight in data] while len(nodes) > 1: nodes.sort(key=lambda x: x.weight) left = nodes.pop(0) right = nodes.pop(0) parent = Node(None, left.weight + right.weight) parent.left = left parent.right = right nodes.append(parent) return nodes[0] def encode_huffman_tree(root): # 编码赫夫曼树 codes = {} def dfs(node, code): if node is None: return if node.value is not None: codes[node.value] = code return dfs(node.left, code + '0') dfs(node.right, code + '1') dfs(root, '') return codes # 示例 data = [('a', 5), ('b', 9), ('c', 12), ('d', 13), ('e', 16), ('f', 45)] root = build_huffman_tree(data) codes = encode_huffman_tree(root) print(codes) # 输出:{'a': '1100', 'b': '1101', 'c': '100', 'd': '101', 'e': '111', 'f': '0'} ``` 以上代码中,`Node` 类表示赫夫曼树的节点,包含值、权值、左子节点和右子节点四个属性。`build_huffman_tree` 函数接受一个列表 `data`,其中每个元素是一个二元组,表示一个叶子节点的值和权值。该函数返回构建好的赫夫曼树的根节点。 `encode_huffman_tree` 函数接受赫夫曼树的根节点,返回一个字典,表示每个叶子节点的编码。该函数使用深度优先搜索遍历赫夫曼树,对于每个叶子节点,记录其值和编码。 在示例中,我们使用了一个包含 6 个叶子节点的数据集,构建了一棵赫夫曼树,并对每个叶子节点进行了编码。最终输出了每个叶子节点的编码结果。 ### 回答2: 1. DS二叉树简介 DS二叉树是一种常见的数据结构,也是二叉树数据结构的一种变种。其特点是每个节点只有两个分支,即左子节点和右子节点。DS二叉树在计算机科学中有着重要的应用,例如在文件压缩、加密等领域中,常用DS二叉树来构建赫夫曼树,实现数据的压缩和加密。 2. 赫夫曼树简介 赫夫曼树(Huffman Tree)是一种用于数据压缩和加密的方法。它是一棵带权路径最短的树,即树中所有叶子节点到根节点的路径长度乘以该叶子节点的权值之和最小。赫夫曼树的构建可以通过DS二叉树来实现,是DS二叉树的一种典型应用。 3. 赫夫曼树的构建与编码 赫夫曼树的构建通过以下步骤实现: 1)将所有的数据项按照权值大小从小到大排序; 2)选取权值最小的两个节点作为新的父节点,将这两个节点从序列中删除,再将新的父节点添加到序列中; 3)重复执行第2步,直到序列中只剩下一个节点,即构建出了一棵赫夫曼树。 在构建赫夫曼树的过程中,可以通过DS二叉树来表示和存储树节点。具体来说,可以定义一个叫做HuffmanNode的结构体,用来存储树节点的权值、左右子节点和父节点等信息。同时,可以定义一个HuffmanTree类,用来实现赫夫曼树的构建和编码。 HuffmanNode结构体定义如下: ``` struct HuffmanNode { int weight; // 权值 HuffmanNode* parent; // 父节点 HuffmanNode* left; // 左子节点 HuffmanNode* right; // 右子节点 }; ``` HuffmanTree类的成员函数包括: 1)createTree:用来构建赫夫曼树; 2)encode:用来对数据进行编码。 createTree函数的实现如下: ``` void HuffmanTree::createTree() { // 将所有数据项节点插入到序列中 for (int i = 0; i < data.size(); i++) { HuffmanNode* node = new HuffmanNode; node->weight = data[i].weight; node->parent = nullptr; node->left = nullptr; node->right = nullptr; nodes.push(node); } // 不断从序列中选取权值最小的两个节点,构建一颗新的赫夫曼树 while (nodes.size() > 1) { HuffmanNode* node1 = nodes.top(); nodes.pop(); HuffmanNode* node2 = nodes.top(); nodes.pop(); HuffmanNode *parent = new HuffmanNode; parent->weight = node1->weight + node2->weight; parent->parent = nullptr; parent->left = node1; parent->right = node2; node1->parent = parent; node2->parent = parent; nodes.push(parent); } // 保存赫夫曼树的根节点 root = nodes.top(); } ``` encode函数的实现如下: ``` string HuffmanTree::encode(const string& s) { // 构建字符到编码的映射表 buildEncodingTable(root, ""); // 对输入的数据进行编码 string result; for (char c : s) { result += encodingTable[c]; } return result; } void HuffmanTree::buildEncodingTable(HuffmanNode* node, string code) { if (node == nullptr) { return; } if (node->left == nullptr && node->right == nullptr) { // 将字符和对应的编码加入到映射表中 encodingTable[node->ch] = code; } buildEncodingTable(node->left, code + "0"); buildEncodingTable(node->right, code + "1"); } ``` 在encode函数中,首先调用buildEncodingTable函数构建字符到编码的映射表,然后对输入的数据进行编码并返回。而buildEncodingTable函数则是通过递归的方式构建映射表的。在编码时,只需要将每个字符对应的编码拼接起来即可。 以上就是DS二叉树--赫夫曼树的构建与编码的基本框架,可以根据需要进行调整和优化。 ### 回答3: ds二叉树,也叫赫夫曼树,是一种特殊的二叉树,用于编码和解码数据。在大数据时代,赫夫曼树被广泛应用于数据压缩,因为它可以用最小的比特位编码代表最频繁的字符,从而大大降低文件大小,加速数据的传输和存储。下面我们来看一下赫夫曼树的构建和编码过程。 赫夫曼树的构建 赫夫曼树的构建需要按照以下步骤: 1.统计每个字符的出现频率,把每个字符看作一个结点,并按照频率从小到大排序。 2.取出两个频率最小的结点作为左右子结点,生成一棵新的树,其权值为左右子结点权值之和。 3.把新的树的权值插入有序队列中。 4.重复步骤2和3,直到队列中只剩下一个树,即为赫夫曼树。 下面是赫夫曼树构建的代码框架: ``` struct Node{ char val; //字符值 int freq; //字符频率 Node *left; //左子结点 Node *right; //右子结点 }; struct cmp { bool operator()(Node *a, Node *b) { return a->freq > b->freq; } }; Node* buildHuffmanTree(string s){ unordered_map<char,int> mp; for(int i=0;i<s.size();i++){ mp[s[i]]++; } priority_queue<Node*, vector<Node*>, cmp> pq; for(const auto &ele:mp){ Node *tmp = new Node; tmp->val = ele.first; tmp->freq = ele.second; tmp->left = nullptr; tmp->right = nullptr; pq.push(tmp); } while(pq.size()>1){ Node *a = pq.top(); pq.pop(); Node *b = pq.top(); pq.pop(); Node *c = new Node; c->freq = a->freq + b->freq; c->left = a; c->right = b; pq.push(c); } return pq.top(); } ``` 赫夫曼编码 在赫夫曼树中,从根节点一直到每个叶子结点的路径构成了该叶子结点字符的编码,左分支为0,右分支为1。赫夫曼的编码方式称为前缀编码,即任何一个字符的编码序列都不是另一个字符代码的前缀,这种编码方式保证了解码的唯一性。因为赫夫曼编码是以树为基础构建的,所以我们可以使用深度优先遍历来得到每个字符的编码。 下面是赫夫曼编码的代码框架: ``` void dfs(Node* root, string path, unordered_map<char,string> &mp) { if(root->left==nullptr && root->right==nullptr){ mp[root->val] = path; return; } if(root->left){ dfs(root->left,path+"0",mp); } if(root->right){ dfs(root->right,path+"1",mp); } } unordered_map<char,string> buildHuffmanCode(Node* root) { unordered_map<char,string> mp; if(root==nullptr){ return mp; } dfs(root,"",mp); return mp; } ``` 总结 赫夫曼树是一种高效的数据压缩方式,在实际应用中广泛应用于图像、音频、视频等大型文件的传输和存储。它的构建和编码过程相对简单,只需要按照一定的规则统计字符频率,然后生成二叉树并得到字符的编码,就可以将文件压缩成更小的大小。希望大家能够掌握赫夫曼树的原理和实现,提高数据处理的效率和精度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值