一、基本概念
霍夫曼树:给定n个权值作为n个叶子结点,构造一颗二叉树,若带权路径达到最小,称这样的树为最优二叉树,也称霍夫曼树
我们回顾一下树的相关概念
路径:若树中存在一个结点序列k1,k2,…,kj,使得ki是ki+1的双亲,则称该结点序列是从k1到kj的一条路径
路径长度:等于路径上的结点数减1
结点的权:在许多应用中,常常将树中的结点赋予一个有意义的数,称为该结点的权结
点的带权路径长度:是指该结点到树根之间的路径长度与该结点上权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作:
二、霍夫曼树的构造
构造过程分为 四步:
1、根据给定的n个权值{w1,w2,w3 …wn}构造n棵二叉树的集合,每棵二叉树都只包含一个结点
2、在上面的二叉树中选出两棵结点权值最小的树,同时取另外一个新的结点作为这两棵树的根结点,设新结点的权值为两棵权值最小的树的权值和,将得到的这棵树也加入到数的集合中
3.在2操作后,从集合中删除权值最小的那两棵树
4.重复2和3,直到集合中的树只剩下一颗为止,剩下的这颗树就是我们要求得的赫夫曼树
示例演示如下:
我们 初始化 五个节点:A(7) 、B(6) 、C(7) 、D(9)、E(2)
在WPL相同的情况下,构造的霍夫曼树不止一种,在我们介绍的算法中,人为的要求某个内节点的左儿子的权值要比右儿子的大,这样一来构造出来的霍夫曼树就只有一种了
我们把构造完成的霍夫曼树,叶子节点称为外部结点,非叶子结点称为内部结点
三、构造霍夫曼树的实现
首先我们要编写一个 Node 结点类,类包含5个实例变量:data 表示外结点存储的字符(编码和解码的时候会用到) , weight 表示权值 。而同样是霍夫曼编码的需求 ,这里使用三叉链实现二叉树,在 left 和 right 属性的基础上,为结点增加了 parent 属性 ,目的是能够从叶子结点上溯到根结点,从而实现哈弗曼编码
class Node: NSObject {
var data:Character? //存放字符
var weight:Int? //权值
var left:Int? //左链(指向左孩子结点)
var right:Int? //右链(指向右孩子结点)
var parent:Int? //父链(父母结点)
init(data:Character , weight:Int) {
super.init()
self.data = data
self.weight = weight
}
init(weight:Int ,parent:Int) {
super.init()
self.weight = weight
self.parent = parent
self.left = 0
self.right = 0
}
}
BuildTree 类的设计
输入参数和返回值
输入参数:一个由外结点对象组成的Node数组,假设其nodes
返回值:一个由内、外结点共同组成,且建立了链接关系的Node数组,假设其为HT(Huffman Tree)(外部结点+内部结点 共同 组成的新数组HT)
需要注意:我们为Node设置的链接变量 left/right/parent是整型的,它指向的是某个结点对象在HT中的下标,而不是结点本身
class BuildTree: NSObject {
var selectStart = 0 //已处理的节点数,每次都把两个节点合成一个(新增一个节点),排序之后,又需要两个节点合成下一个节点,所以步长是 2
///构建霍夫曼树
///
/// - Parameter nodes:叶子节点(我们把构造完成的霍夫曼树,叶子节点称为外部节点,非叶子节点称为内部节点)
/// - Returns:处理过后的霍夫曼树
func buildTree(nodes:Array<Node>)->Array<Node> {
var left:Int? //左节点索引
var right:Int? //右节点索引
var parent:Int? //父母节点索引
let n = nodes.count //外部节点的数量
let m = 2*n - 1 //内部节点 + 外部节点的总数量
var HT:Array<Node> = []; //存储节点的对象
//给所有的节点一个默认的权值为0
for _ in 0..<m {
let node = Node.init(weight: 0,parent:0)
HT.append(node)
}
//把叶子节点的值赋值给 HT[0]~HT[n-1](内部节点)
for index in 0..<n {
HT[index].data = nodes[index].data
HT[index].weight = nodes[index].weight
}
//计算内部节点的值
for index in n..<m {
parent = index
left = selectSubscript(HT: HT, rang: index, rank: 0) //取得HT数组中权值最小的节点对象的下标
right = selectSubscript(HT: HT, rang: index, rank: 1) //取得HT数组中权值次小的节点对象的下标
//index 节点为当前数组HT 中合成的节点(父节点) ,left 为左节点索引,right 为右节点索引 ,以下建立关联关系
HT[index].left = left
HT[index].right = right
HT[index].weight = (HT[left!].weight)! + (HT[right!].weight)! //当前节点的权值
HT[left!].parent = parent
HT[right!].parent = parent
selectStart += 2
}
return HT
}
/// 返回权值排名为 rank 的节点对象在HT中的下标(权值从小到大排序)
///
/// - Parameters:
/// - HT: 所有节点的对象
/// - rang: 处理的节点的范围
/// - rank: left / right
func selectSubscript(HT:Array<Node> ,rang:Int , rank:Int)->Int{
//将HT[0]~HT[range]拷贝到copyNodes中
var copyNodes:Array<Node> = []
for index in 0..<rang {
copyNodes.append(HT[index])
}
//对数组 copyNodes 进行从小到大的排序
copyNodes = QuickSort().sort(nodes: copyNodes)
//从未参与合成的节点 中取得最小的值/次最小值
let targetNode = copyNodes[rank + selectStart]
for index in 0..<HT.count {
if targetNode == HT[index] { //返回该结点对象在数组HT中的下标
return index
}
}
return -1
}
}
构建哈夫曼树用例
func test(){
var nodes:Array<Node> = []
let node1 = Node.init(data:"A", weight: 4)
let node2 = Node.init(data:"B", weight: 6)
let node3 = Node.init(data:"C", weight: 7)
let node4 = Node.init(data:"D", weight: 9)
let node5 = Node.init(data:"E", weight: 2)
nodes.append(node1)
nodes.append(node2)
nodes.append(node3)
nodes.append(node4)
nodes.append(node5)
//构建哈夫曼树
print("构建霍夫曼树")
let buildTree = BuildTree()
let HT = buildTree.buildTree(nodes:nodes)
for index in 0..<HT.count {
let node = HT[index]
print("Node -",index,": data = ","\(String(describing: node.data))"," weight = ","\((node.weight)!)")
}
}
四、哈夫曼算法应用
霍夫曼树,最著名的应用就是霍夫曼编码了,广泛的用于数据文件压缩的编码方式。其压缩率通常在 20%~90%之间。其通常用二进制编码来表示字母或者其他字符,并用这样的编码来表示字符序列。
霍夫曼编码(Huffman Coding)是一种编码方式,是一种用于无损数据压缩的熵编码(权编码)算法
霍夫曼编码的特点:
1.编码长度短(压缩性):实际的应用中,各字符的出现的频率是不同的,如果用统一定长的编码表示字符,肯定会浪费很多空间,用短编码表示大频率的字符,长编码表示小频率的字符,使得编码序列的总长度最小,就会节省很多空间
2.编码的唯一性:要求任一字符的编码都不能是另一字符编码的前缀,这种编码称为前缀编码(其实是非前缀码)
赫夫曼编码是如何实现前缀编码的
假设有一棵如下图所示的二叉树, 其4个叶结点分别表示A,B,C,D这4个字符,且约定左分支表示字符'0', 右分支代表字符'1', 则可以从根结点到叶子结点的路径上的各分支字符组成的字符串作为该叶子结点字符的编码。 从而得到A,B,C,D的二进制编码分别为0, 10, 110, 111
具体如下图所示:
1060770-20180117232101506-1692052052.png五、霍夫曼编码/译码
霍夫曼编码
根据给定的字符和权值, 输出字符和对应的赫夫曼编码
注意要点
-
我们编写一个HuffmanCode内部类用于存放字符(data实例变量)和它对应的二进制字符串(bit实例变量)
-
要求所有字符对应编码时,如果采用从根结点下行到叶结点的思路处理,逻辑会相对复杂一些, 所以我们用逆向的方式获取: 按照从叶子结点到根结点的路径累加二进制字符串
1060770-20180117232123834-93460458.png -
因为 2 的原因, 累加二进制字符串的时候也必须反向累加,例如写成bit= "0" + bit; 而不是写成bit= bit+ "0";
-
上溯时候要做的工作是: 判断当前经过的是 0 还是 1, 判断的方法如下图所示:
假设P是X的父节点:
• 如果P.left==X在HT中的下标,则说明X是P的左分支,说明经过的是 0
• 如果P.right==X在HT中的下标,则说明X是P的右分支,说明经过的是 1
1060770-20180117232210021-584760922.png
///编码
///
/// - Parameter nodes: 叶子节点
/// - Returns: 每个字符的编码 数组
func encode(nodes:Array<Node>)->Array<HuffmanCode>{
var HT = BuildTree().buildTree(nodes: nodes) //构造霍夫曼树
let n = nodes.count
var HC:Array<HuffmanCode> = []
var bit:String = ""
for leavesIndex in 0..<n { // 遍历各个叶子结点
bit = ""
// 从叶子结点上溯到根结点
var nodeIndex = leavesIndex
while (HT[nodeIndex].parent != 0) { //遍历结束的条件是 node 的父节点的值为 0 (根节点)
let parentIndex = (HT[nodeIndex].parent)!
if HT[parentIndex].left == nodeIndex { //左
bit = "0\(bit)"
} else { //右
bit = "1\(bit)"
}
// 开始下一个节点的循环
nodeIndex = parentIndex
}
//将字符和对应的编码存储起来
let code = HuffmanCode.init(data:HT[leavesIndex].data , bit: bit)
HC.append(code)
}
return HC
}
霍夫曼译码
根据给定的字符串和权值,将输入的霍夫曼编码翻译回原来的字符串
/// 译码
///
/// - Parameters:
/// - nodes: 叶子节点
/// - code: 编码
/// - Returns: 译码后的字符
func decode(nodes:Array<Node> , code:String)->String {
var str = ""
var HT = BuildTree().buildTree(nodes: nodes) //构造霍夫曼树
var n = HT.count - 1
//遍历字符串
for c in code {
if "\(c)" == "1" {
n = (HT[n].right)!
} else {
n = (HT[n].left)!
}
if (HT[n].left)! == 0 { //叶子节点为0 打印当前字符
str += "\((HT[n].data)!)"
n = HT.count - 1
}
}
return str
}
霍夫曼编码/译码用例
func test(){
var nodes:Array<Node> = []
let node1 = Node.init(data:"A", weight: 4)
let node2 = Node.init(data:"B", weight: 6)
let node3 = Node.init(data:"C", weight: 7)
let node4 = Node.init(data:"D", weight: 9)
let node5 = Node.init(data:"E", weight: 2)
nodes.append(node1)
nodes.append(node2)
nodes.append(node3)
nodes.append(node4)
nodes.append(node5)
//霍夫曼编码
print("霍夫曼编码")
let huffmanCode = HuffmanEncode()
for index in 0..<huffmanCode.encode(nodes:nodes).count {
let code = huffmanCode.encode(nodes:nodes)[index]
print("encode -",index,": data = ","\((code.data)!)"," bit = ","\((code.bit)!)")
}
//解码
print("译码")
let decode = huffmanCode.decode(nodes:nodes , code:"00101111000011" )
print("decode - 00101111000011 译码后的字符为: ",decode)
}
五、小结
最近把算法的知识回顾了一下,发现 霍夫曼编码和译码的实现大多是使用 C 、C++ 、C#、Java 等语言,可能是swift 相对比较新的原因也可能是很少在APP客户端用到这这个算法的原因,很少看到 swift的实现版本,为了方便理解和学习,我用swift实现了一下,供后来学习的小伙伴参考
demo https://github.com/hylccmh/HuffmanAlgorithm
参考文章:http://www.cnblogs.com/penghuwan/p/8308324.html#_label1
网友评论