012 go语言 赫夫曼(哈夫曼)树及其应用

1 赫夫曼树定义及其原理

我们先把这两棵二叉树简化成 叶子结点带权的二叉树, 如下图所示

A表示不及格, B表示及格, C表示中等, D表示良好, E表示优秀。

image.png

从树中一个结点到另一个结点之间的分支构成两个结点之间的路径, 路径上的分支数目称为路径长度,
二叉树a中, 从根节点到D的路径长度为4, 二叉树b中, 从根节点到D的路径长度为2, 数的路径长度就是从树根 到每一个结点的路径长度之和, 二叉树 a的树路径长度为 1 + 1 +2+2+3+3+4+4 = 20
二叉树 b的树路径长度就是 1+2+3+3+2+1+2+2 = 16

如果考虑到带权的结点, 结点的带权的路径长度为从该节点到树根之间的路径长度与结点上权的乘机, 数的带权路径长度为树中所有叶子结点的带权路径长度之和。 假设有n个权值,(w1, w2, ... wn), 构造一棵有n个结点的二叉树, 每个叶子结点带权 wk, 每个叶子结点的路径长度为 lk, 我们通常记做, 其中带权路径长度 WPL最小的二叉树为赫夫曼树

image.png

image.png

2 如何构建赫夫曼树

  1. 先把有权值的叶子结点按照从小到大的顺序排成一个有序序列, 即: A5 E10 B15 D30 C40
  2. 取头两个最小权值的结点作为一个新结点N1的两个子节点, 注意取相对较小的是左孩子, 这里就是A为N1的左孩子, E为N1的右孩子, 新结点的权值为两个叶子权值的和 5 + 10 = 15
  3. 将N1替换A与E, 插入有序序列中, 保持从小到大排列, 即 N115, B15, D30, C40
image.png
  1. 重复步骤2, 将N1和B作为一个新结点N2 的两个子节点, 如图所示:
    N2的权值为 15 +15 = 30
    image.png

5.将N2 替换 N1 和B, 插入有序序列中, 保持从小到大排序, 即: N230, D30, C40

  1. 重复步骤2, 将N2 与D作为一个新结点, N3的两个子结点, N3的权值是 60

7.将N3 替换N2 和D, 插入有序序列中, 保持从小到大排列, 即 C40, N360

8 重复步骤2, 将C于N3作为一个新结点T的两个子节点, 用于T即是跟结点, 完成赫夫曼树的构造。


image.png

此时构造出来的树的权值 WPL = 401 +302 + 153 +104 + 5*4 = 205

通过刚才的步骤分析, 可以得出构造赫夫曼树的算法描述:

  1. 根据给定的N个权值{w1, w2, w3, ... wn} 构成n棵二叉树的集合 F={T1,T2,...Tn}, 其中每棵二叉树 Ti 中, 只有一个带权wi 根节点, 其左右子树均为空
  2. 在F中, 选取两个根节点的权值最小的树作为左右子树构成一个新的二叉树, 且置新的二叉树的根节点的权值为其左右子树的权值的和
  3. 在F中删除这两棵树, 同时将新得到的二叉树加入F中
    4 重复2 和3步骤, 直到F中只含一棵树为止, 这棵树就是 赫夫曼树。

假设 6个字母的频率为 A27, B8, C15, D15, E30 , F5

image.png

如果我们中序遍历这个赫夫曼树的叶子结点, 则其结果应该是
DAFBCE

image.png

注意事项: 在生成哈夫曼树的时候, 每次都要取权重最小的结点, 所以每次从集合 F中取结点时, 都要先对结点进行排序, 然后再取最小的两个值。
第一次做的时候, 先对最原始的结点按照权重进行排序, 然后生成哈夫曼树, 生成错了, 耽误了好久.

代码如下:

代码 参考地址:
https://gitee.com/babyb/data_srtuct/tree/master/012Huffman

package main 

import (
    "fmt"

)

type Node struct{
    value  rune 
    weight int 
    lchild *Node
    rchild *Node
}

//Nodes 用于Node, 按照weight 排序
type Nodes []Node

//Tree 就是整棵Huffman树的根节点
type Tree struct{
    Root *Node
}


// 哈夫曼树
func main()  {
    a := Node{
        value: 'A',
        weight: 27,
    }

    b := Node{
        value: 'B',
        weight: 8,
    }
    c := Node{
        value: 'C',
        weight: 15,
    }
    d := Node{
        value: 'D',
        weight: 15,
    }
    e := Node{
        value: 'E',
        weight: 30,
    }

    f := Node{
        value: 'F',
        weight: 5,
    }

    nodes :=[]Node{a,b,c,d,e,f}
    // //按照权重排序
    // sorted := sortNodes(nodes);
    // //展示排序后的切片
    // showNodes(sorted);

    //生成哈夫曼树
    tree := makeHuffManTree(nodes);

    //中序遍历二叉树, 展示叶子结点
    root := tree.Root;
    showLeafNode(root)
}

//返回排好序的 Nodes
func  sortNodes(nodes Nodes)  (sorted []Node){
    for _, v := range nodes{
        
        
        if len(sorted) == 0{
            sorted = append(sorted, v);
        }else{
            i:=0;
            for   i<len(sorted) {
                if v.weight < sorted[i].weight {
                    break;
                }
                i++
            }
            // 遍历完之后, 根据i的位置, 把v 插入到 temp当中
            if i==0{
                //头部插入元素
                // fmt.Println("元素插入到头部");
                temp := []Node{v}
                sorted =append(temp, sorted...);
            }else if i== len(sorted){
                //尾部插入元素
                // fmt.Println("元素插入到尾部");
                sorted = append(sorted, v);
            }else{
                //
                // fmt.Println("元素插入到中间位置");
                // fmt.Println("前半截", sorted[:i]);
                // fmt.Println("后半截", sorted[i:]);
                var pre  []Node;
                pre = append(pre, sorted[:i]...);

                var after  []Node
                after = append(after,  sorted[i:]...);

                //插入元素
                temp :=append(pre, v);

                sorted = append(temp, after...)
            }
 
        }
        // fmt.Println("排序之后的切片:")
        // showNodes(sorted)
    }

    return sorted;
}

func  showNodes(nodes Nodes){
    for _, v := range nodes{
        fmt.Printf("value: %c,  weight: %d \n" ,v.value, v.weight)
    }
}

//返回哈夫曼树
func makeHuffManTree(nodes Nodes) *Tree{
    /*
        1 拿出权重最小的两个结点, a, b, 组成新结点; N1,N1的权重 =  a.weight + b.weight , 把N1 放到nodes 切片中

        2  在 nodes中再拿出两个权重最小的结点, 构成新树, 新树放回node中, 
        
        3  重复步骤2 ,  直到 nodes 中只剩一个结点,  则这个结点就是树的根节点  
    */

 
    

    var root Node;
    for  {
        if len(nodes) == 1{
            // 当 nodes中只有一个元素时,  这个元素必然是根元素
            break;
        }else{
            // 按照从权重小到大的顺序对 nodes进行排序, 这样, 取出的前两个就是权重最小的两个结点了
            nodes =  sortNodes(nodes);

            pre := nodes[:2];
            after := nodes[2:];
            // fmt.Println("pre", pre[0].weight, pre[1].weight)
            newNode := Node{}
            newNode.weight = pre[0].weight + pre[1].weight;
 
            newNode.lchild = &pre[0]
            newNode.rchild = &pre[1]

            //新结点放回到nodes中, 进入下一波循环 

            var temp  []Node;
            temp = append(temp, newNode)
            //生成的结点放入到 nodes 中
            nodes = append(temp, after...)

            // var left Node
            // var right Node
            
            // //权重小的放左边
            // if pre[0].weight < pre[1].weight{
            //  left = pre[0]
            //  right = pre[1]
            // }else{
            //  left = pre[1]
            //  right = pre[0]
            // }
    
            // fmt.Printf("左: %c 左边重 %d  右: %c  右边重: %d  新结点权重: %d \n", left.value, left.weight,  right.value, right.weight , newNode.weight)
    



        }
    }

    // for 循环结束后,  node中只有一个根元素 
    root = nodes[0];

    //打印根元素的权重
    // fmt.Println("root weight", root.weight);
    
    tree := &Tree{ Root: &root }
    return tree;
}

/**
遍历叶子结点
**/
func showLeafNode(node *Node)  {
    
    //左节点
    if node.lchild != nil{
        showLeafNode(node.lchild)
    }

    //右子节点
    if node.rchild != nil{
        showLeafNode(node.rchild)
    }

    //
    if node.lchild == nil  && node.rchild == nil {
        fmt.Printf("%c", node.value)
    }
}


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

推荐阅读更多精彩内容