如何用贪心算法实现霍夫曼编码?
假设我有一个包含1000个字符的文件,每个字符占1个byte(1byte=8bits),存储这1000个字符就一共需要8000bits,那么也没有更节约存储空间的存储方式呢?
假设我们通过统计分析发现,这1000个字符中包含6种不同的字符,假设他们分别是a,b,c,d,e,f。而3个二进制位(bit)就可以表示8个不同的字符,所以,为了节约存储空间,每个字符我们用3个二进制位来表示。那么存储这1000个字符只需要3000bits就可以了,比原来节约很多的空间,但是有没有更节约的存储方式呢?
此时,霍夫曼编码就要出现了,霍夫曼编码是一种十分有效的编码方式,广泛应用于数据压缩中,其压缩率通常在20%~90%之间。霍夫曼编码不仅会考察文本中有多少个不同的字符,还会考察每个字符出现的频率,根据频率的不同,选择不同长度的编码,霍夫曼编码试图用这种不等长的编码方式来进一步增加压缩的效率。根据贪心的思想,可以把出现频率较高的字符用稍微短一些的编码,出现频率较低的字符,用稍微长一些的编码。
因为霍夫曼编码是不等长的,每次应该读取1位还是2位等等来解压缩呢?为了避免在解压缩的过程中的歧义,霍夫曼编码要求各个字符的编码之间,不会出现某个编码是另一个编码前缀的情况。
在霍夫曼编码中如何根据字符出现的频率的不同给不同的字符进行不同长度的编码呢?
我们把每个字符看作一个节点,并且附带着把频率放到优先级队列中。我们从队列中取出频率最小的两个节点A、B,然后新建也该节点C,把频率设置位两个节点的频率之和,并把这个新节点C作为节点A、B的父节点,最后再把C节点放到优先级队列中,重复这个过程,直到队列中没有数据。
现在,给每一条边加上一个权值,指向左子节点的边统统标记为0,指向右子节点的边标记为1,那么从根节点到叶子节点的路径就是叶子节点对应字符的霍夫曼编码
实际上,贪心算法适用的场景比较有限,这种算法思想更多的是指导设计基础算法,比如最小生成树算法、单源最短路径算法。最难的是如何将要解决的问题抽象成贪心算法模型。
思考:
1.在一个非负整数a中,我们希望从中移除k个数字,让剩下的数字值最小,如何选择移除哪k个数字呢?
2.假设有n个人等待被服务,但是服务窗口只有一个,每个人需要被服务的时间长度是不一样的,如何安排被服务的先后顺序,才能让这n个人总的等待时间最短?
由等待时间最短的先开始服务。
回溯算法的应用:深度优先算法、数独、八皇后、0-1背包、图的着色、旅行商问题、全排列等等。
回溯的处理思想:有点类似于枚举搜索,枚举所有的解,找到满足期望的解。为了有规律的枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段,每个阶段都会面对一个岔路口,我们随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一条走法继续走
八皇后问题:我们有一个8*8的棋盘,希望往里放8个棋子(皇后),每个棋子所在的行、列、对角线都不能有另一个棋子。我们把这个问题划分为八个阶段,依次i将8个棋子放到第一行、第二行、第三行…第八行,在放置的过程中,我们不停的检查当前放法,是否满足要求。如果满足,则跳到下一行继续放置棋子;如果不满足,那就再换一种放法,继续尝试。(回溯算法非常适合用递归来实现)
原问题与分解成的小问题具有相同的模式;
原问题分解成的子问题可以独立求解,子问题之间没有相关性,这也是分治算法和动态规划算法的明显区别;
具有分解终止条件,即当问题足够小时,可以直接求解;
可以将子问题合并为原问题,而这个合并操作的复杂度不能太高,不然就起不到减小算法总体复杂的效果了。
4.经典题型:
二维平面上有n个点,如何快速的计算出两个距离最接近的点对?
有两个nn的矩阵A、B,如何快速求解两个矩阵的乘积C=AB?
分治思想不仅仅应用于指导编程和算法设计,还经常用在海量数据的处理。我们之前讲的数据机构和算法,大部分都是基于内存存储和单机处理,但是当要处理的数据量非常大,没法一次性放到内存中,此时需要分治思想。
比如,给10GB的订单文件按照金额排序这个需求,因为机器的内存有限,无法一次加载到内存,也就无法单纯的通过归并排序、快排等基础算法来解决。
利用分治的思想将海量的数据通过某种方法,划分为几个小的数据集合,每个小的数据集合单独加载到内存来解决,然后再将小数据集合合并为大数据集合。先扫描一遍订单,根据订单的金额,将10GB的文件划分为几个金额区间,比如将订单金额为1到100的放到一个小文件,101到200的放一个小文件,以此类推,每个小文件都能单独加载到内存排序,最后将这些有序的小文件合并就是最终有序的10GB订单数据。
对于谷歌搜索引擎来说,网页爬取、清洗、分析、分词、计算权重、倒排索引等各个环节都会面对如此海量的数据,所以,利用集群并行处理是大势所趋。实际上,Map Reduce框架只是一个任务调度器,底层依赖于GFS来存储数据,依赖Borg中的机器执行,并且时刻监视机器执行的速度,一旦出现机器宕机、进度卡壳等就会重新从Borg中调度一台机器执行。
MapReduce提供了高可靠、高性能、高容错的并行计算框架,并行的处理这几十亿、上百亿的网页。
如果对这四种算法思想进行分类的话,贪心、回溯、动态规划为一类,分治算法为另一类。因为前三个算法解决问题的模型,都可以抽象成多阶段决策最优解模型,而分治算法解决的打本份问题也是最优解问题,但是大部分不能抽象为多决策模型。 基本上能用动态规划、贪心解决的问题都可以用回溯解决。回溯相当于穷举搜索,穷举所有的情况,然后对比得到最优解。但是,回溯的时间复杂度很高,是指数级别的,只能解决小规模数据的问题。 尽管动态规划比回溯算法高效,但是不是所有的问题都可以用动态规划来解决。能用动态规划解决的问题,需满足最优子结构、无后效性、重复子问题三个条件。在重复子问题上,动态规划和分治算法的区别十分明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题。 贪心算法实际上是动态规划的一种特殊情况。他更高效,但是能解决的问题有限,它能解决的问题需要满足最优子结构、无后效性、贪心选择性。贪心选择性是指通过局部的最优的选择,能产生全局的最优选择。每个阶段,我们都选择当前看起来最优的决策,所有阶段的决策完成之后,最终由这些局部最优解构成全局最优解。