霍夫曼编码 和 译码 — Swift

一、基本概念

霍夫曼树:给定n个权值作为n个叶子结点,构造一颗二叉树,若带权路径达到最小,称这样的树为最优二叉树,也称霍夫曼树

我们回顾一下树的相关概念

路径:若树中存在一个结点序列k1,k2,…,kj,使得ki是ki+1的双亲,则称该结点序列是从k1到kj的一条路径
路径长度:等于路径上的结点数减1
结点的权:在许多应用中,常常将树中的结点赋予一个有意义的数,称为该结点的权结
点的带权路径长度:是指该结点到树根之间的路径长度与该结点上权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作:

052153427884455.png

二、霍夫曼树的构造
构造过程分为 四步:

1、根据给定的n个权值{w1,w2,w3 …wn}构造n棵二叉树的集合,每棵二叉树都只包含一个结点
2、在上面的二叉树中选出两棵结点权值最小的树,同时取另外一个新的结点作为这两棵树的根结点,设新结点的权值为两棵权值最小的树的权值和,将得到的这棵树也加入到数的集合中
3.在2操作后,从集合中删除权值最小的那两棵树
4.重复2和3,直到集合中的树只剩下一颗为止,剩下的这颗树就是我们要求得的赫夫曼树

示例演示如下:

我们 初始化 五个节点:A(7) 、B(6) 、C(7) 、D(9)、E(2)

Pasted Graphic 1.png

在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
五、霍夫曼编码/译码
霍夫曼编码

根据给定的字符和权值, 输出字符和对应的赫夫曼编码

注意要点
  1. 我们编写一个HuffmanCode内部类用于存放字符(data实例变量)和它对应的二进制字符串(bit实例变量)

  2. 要求所有字符对应编码时,如果采用从根结点下行到叶结点的思路处理,逻辑会相对复杂一些, 所以我们用逆向的方式获取: 按照从叶子结点到根结点的路径累加二进制字符串


    1060770-20180117232123834-93460458.png
  3. 因为 2 的原因, 累加二进制字符串的时候也必须反向累加,例如写成bit= "0" + bit; 而不是写成bit= bit+ "0";

  4. 上溯时候要做的工作是: 判断当前经过的是 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容