Golang赫夫曼樹(shù)及其編碼

赫夫曼樹(shù)

在樹(shù)這種數(shù)據(jù)結(jié)構(gòu)中载弄,從樹(shù)中的一個(gè)結(jié)點(diǎn)到另一個(gè)結(jié)點(diǎn)之間的分支構(gòu)成這兩個(gè)結(jié)點(diǎn)之間的路徑,路徑上分支的數(shù)目叫做路徑長(zhǎng)度撵颊,從根結(jié)點(diǎn)到每個(gè)結(jié)點(diǎn)的路徑長(zhǎng)度之和叫做樹(shù)的路徑長(zhǎng)度宇攻。如果一個(gè)結(jié)點(diǎn)帶有權(quán)重,則路徑長(zhǎng)度和該結(jié)點(diǎn)權(quán)重的乘積叫做該結(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)度(WPL)倡勇,一棵樹(shù)的帶權(quán)路徑長(zhǎng)度為樹(shù)中所有葉子結(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)度之和逞刷。

赫夫曼樹(shù)(霍夫曼樹(shù)),又稱最優(yōu)樹(shù)或者最優(yōu)二叉樹(shù)。假設(shè)有n個(gè)權(quán)值{w_1, w_2,...,w_n}夸浅,通過(guò)這n個(gè)權(quán)值構(gòu)造n個(gè)結(jié)點(diǎn)仑最,每個(gè)結(jié)點(diǎn)的權(quán)值為w_i(1<=i<=n),然后以這n個(gè)結(jié)點(diǎn)為葉子結(jié)點(diǎn)構(gòu)造一棵二叉樹(shù)帆喇,在所有構(gòu)成的二叉樹(shù)中帶權(quán)路徑長(zhǎng)度最小的二叉樹(shù)被稱作最優(yōu)二叉樹(shù)或者赫夫曼樹(shù)警医。如下:

給定4個(gè)權(quán)重分別為7, 5, 2, 4的四個(gè)葉子結(jié)點(diǎn)a, b, c, d, 下圖為用該4個(gè)結(jié)點(diǎn)構(gòu)造的3棵二叉樹(shù):

上面圖中三個(gè)二叉樹(shù)的帶權(quán)路徑長(zhǎng)度分別為:

(a) WPL = 72 + 52 + 22 + 42 = 36

(b) WPL = 73 + 53 + 21 + 42 = 46

(c) WPL = 71 + 52 + 23 + 43 = 35

其中(c)的帶權(quán)路徑最小,從圖中可以看出:權(quán)值從高到低的節(jié)點(diǎn)對(duì)應(yīng)路徑長(zhǎng)度依次遞增坯钦,可以得出該樹(shù)的帶權(quán)路徑為最小预皇,該樹(shù)即為赫夫曼樹(shù)。

赫夫曼樹(shù)的構(gòu)造

構(gòu)造赫夫曼樹(shù)的步驟如下:

  1. 根據(jù)給定的n個(gè)權(quán)值{w_1, w_2,...,w_n}構(gòu)成n棵二叉樹(shù)集合F={T_1,T_2,...,T_n}葫笼,其中每棵二叉樹(shù)T_i中只有一個(gè)帶權(quán)為w_i的根結(jié)點(diǎn)深啤,其左右子樹(shù)均為空(即初始集合中每棵樹(shù)只有一個(gè)根結(jié)點(diǎn))。

  2. 在F中選取兩棵根結(jié)點(diǎn)權(quán)值最小的樹(shù)作為左右子樹(shù)構(gòu)造一棵新的二叉樹(shù)路星,并且新二叉樹(shù)根結(jié)點(diǎn)的權(quán)值為兩棵子樹(shù)根結(jié)點(diǎn)的權(quán)值之和溯街。

  3. 在F中刪除步驟2中選中的兩棵樹(shù),同時(shí)步驟2新構(gòu)造出的二叉樹(shù)加入到F中洋丐。

  4. 重復(fù)步驟2和3呈昔,直到F中只剩下一棵樹(shù)為止,此時(shí)剩下的這棵樹(shù)便是赫夫曼樹(shù)友绝。

具體過(guò)程如下圖所示堤尾,為前面給定的4個(gè)結(jié)點(diǎn)構(gòu)造赫夫曼樹(shù)的過(guò)程(注意:圖中的結(jié)點(diǎn)按照權(quán)值大小已經(jīng)排好序,在實(shí)際代碼中需要排序或者選出最小權(quán)值結(jié)點(diǎn)的過(guò)程):

赫夫曼樹(shù)的應(yīng)用-赫夫曼編碼

在現(xiàn)實(shí)生活中迁客,我們通常經(jīng)常會(huì)遇到對(duì)數(shù)據(jù)進(jìn)行編碼的場(chǎng)景郭宝,目的是為了對(duì)數(shù)據(jù)進(jìn)行一定程度的壓縮的同時(shí)也能對(duì)原始數(shù)據(jù)進(jìn)行加密工作,從而避免原始數(shù)據(jù)暴露在網(wǎng)絡(luò)中掷漱。最簡(jiǎn)單的方法是對(duì)每個(gè)原始數(shù)據(jù)的每個(gè)源符號(hào)生成一個(gè)固定長(zhǎng)度的編碼粘室,然后用編碼替換每個(gè)源符號(hào),以達(dá)到對(duì)整個(gè)數(shù)據(jù)編碼的目的卜范。但是這種方法有兩個(gè)弊端:

  1. 每個(gè)編碼長(zhǎng)度固定衔统,容易被破譯。

  2. 每個(gè)編碼長(zhǎng)度固定海雪,導(dǎo)致編碼后的數(shù)據(jù)長(zhǎng)度不盡如人意锦爵。

而赫夫曼編碼則很好的規(guī)避了這兩個(gè)問(wèn)題,赫夫曼編碼使用變長(zhǎng)編碼表對(duì)源符號(hào)進(jìn)行編碼奥裸,其中每個(gè)編碼的長(zhǎng)度是根據(jù)評(píng)估源符號(hào)出現(xiàn)幾率的方法得到的险掀,即出現(xiàn)頻率高的源符號(hào)使用較短的編碼,反之使用較長(zhǎng)的編碼湾宙,這樣編碼后的數(shù)據(jù)整體平均長(zhǎng)度降低迷郑,從而實(shí)現(xiàn)更高效的壓縮枝恋。例如在英文中,e的出現(xiàn)幾率最高嗡害,而z的出現(xiàn)幾率最低,當(dāng)利用赫夫曼編碼對(duì)一篇英文進(jìn)行壓縮時(shí)畦攘,假設(shè)e用一個(gè)bit來(lái)表示霸妹,而z則可能用25個(gè)bit表示。如果使用定長(zhǎng)的編碼表示知押,每個(gè)英文字母使用8個(gè)bit來(lái)表示叹螟。二者相比,普通方法中台盯,頻率最高的e就會(huì)浪費(fèi)7個(gè)bit罢绽,而赫夫曼中z則是3倍多,但是如果能夠?qū)τ⑽闹懈鱾€(gè)字母出現(xiàn)的概率有較準(zhǔn)確的估算静盅,則可以大幅度的提高無(wú)損壓縮比例良价。

赫夫曼編碼是對(duì)赫夫曼樹(shù)的一種應(yīng)用,常用于處理符號(hào)編寫工作蒿叠,根據(jù)原始數(shù)據(jù)中符號(hào)出現(xiàn)的頻率高低明垢,決定如何給符號(hào)編碼,頻率越高的符號(hào)編碼越短市咽,相反編碼越長(zhǎng)痊银。

赫夫曼編碼是由通過(guò)構(gòu)造赫夫曼樹(shù)生成的一種編碼。編碼生成過(guò)程如下:

假設(shè)給定的某些英文符號(hào)的出現(xiàn)頻率如下表所示:

符號(hào) A B C D E F
頻率 2 3 4 4 5 7

我們以頻率為權(quán)值構(gòu)造6棵只有一個(gè)結(jié)點(diǎn)的二叉樹(shù)施绎,然后按照赫夫曼樹(shù)的構(gòu)造規(guī)則構(gòu)造赫夫曼樹(shù):

構(gòu)造好赫夫曼樹(shù)后溯革,下一步便是進(jìn)行編碼,赫夫曼編碼步驟為:

  1. 給赫夫曼樹(shù)中存在孩子結(jié)點(diǎn)的左路徑編碼"0"谷醉,右路徑編碼"1"
  2. 從赫夫曼樹(shù)的根結(jié)點(diǎn)開(kāi)始到每一個(gè)葉子結(jié)點(diǎn)所經(jīng)過(guò)的路徑的編碼組成的一個(gè)編碼串便是該葉子結(jié)點(diǎn)代表的符號(hào)的赫夫曼編碼致稀。

如下圖:

根據(jù)上圖得到每個(gè)字符的編碼為:

符號(hào) A B C D E F
頻率 2 3 4 4 5 7
編碼 010 011 110 111 00 10

注意: 赫夫曼編碼并不唯一,如上圖中的C和D的位置可以互換

對(duì)數(shù)據(jù)編碼的過(guò)程為構(gòu)造赫夫曼樹(shù)孤紧,然后根據(jù)生成的編碼來(lái)進(jìn)行壓縮豺裆,而數(shù)據(jù)解碼則與編碼相反,解碼根據(jù)給定的編碼數(shù)據(jù)号显,遍歷赫夫曼樹(shù)得到原始數(shù)據(jù)臭猜。下面為用Go語(yǔ)言實(shí)現(xiàn)的赫夫曼樹(shù)。

代碼實(shí)現(xiàn)

type HuffmanNodeList []*HuffmanNode

func (list HuffmanNodeList) Len() int {
    return len(list)
}

func (list HuffmanNodeList) Swap(i, j int) {
    list[i], list[j] = list[j], list[i]
}

func (list HuffmanNodeList) Less(i, j int) bool {
    return list[i].Weight < list[j].Weight
}

//赫夫曼節(jié)點(diǎn)押蚤,用于構(gòu)成赫夫曼樹(shù)
type HuffmanNode struct {
    Weight uint         //權(quán)重
    Data   interface{}  //數(shù)據(jù)
    Parent *HuffmanNode //父節(jié)點(diǎn)
    Left   *HuffmanNode //左孩子
    Right  *HuffmanNode //右孩子
}

//赫夫曼樹(shù)結(jié)構(gòu)蔑歌,這里使用的interface作為源數(shù)據(jù)類型
type HuffmanTree struct {
    root    *HuffmanNode           //根節(jié)點(diǎn)
    leaf    HuffmanNodeList        //所有葉子節(jié)點(diǎn)(即數(shù)據(jù)對(duì)應(yīng)的節(jié)點(diǎn))
    src     map[interface{}]uint   //源數(shù)據(jù),key為數(shù)據(jù)揽碘,value為權(quán)重
    codeSet map[interface{}]string //編碼集次屠,key為數(shù)據(jù)园匹,value為通過(guò)構(gòu)造赫夫曼樹(shù)得到的數(shù)據(jù)的編碼
}

//給定一組字符及其權(quán)重的集合,初始化出一棵赫夫曼樹(shù)
func NewHuffmanTree(src map[interface{}]uint) *HuffmanTree {
    var tree = &HuffmanTree{
        src: src,
    }
    tree.init()
    tree.build()
    tree.parse()
    return tree
}

//根據(jù)數(shù)據(jù)進(jìn)行赫夫曼編碼
func (h *HuffmanTree) Coding(target interface{}) (result string) {
    if target == nil {
            return
        }
        var s string
        switch t := target.(type) {
        case string:
            s = t
        case []byte:
            s = string(t)
        default:
            return
        }
        for _, t := range s {
            v := string(t)
            if c, ok := h.codeSet[v]; !ok {
                panic("invalid code: " + v)
            } else {
                result += c
            }
        }
        return result
}

//根據(jù)赫夫曼編碼獲取數(shù)據(jù)
func (h *HuffmanTree) UnCoding(target string) (result string) {
    node := h.root
    for i := 0; i < len(target); i++ {
        switch target[i] {
        case '0':
            node = node.Left
        case '1':
            node = node.Right
        }
        if node.Left == nil && node.Right == nil {
            result = result + node.Data.(string)
            node = h.root
        }
    }
    return
}

//初始化所有葉子節(jié)點(diǎn)
func (h *HuffmanTree) init() {
    if len(h.src) <= 1 {
        panic("invalid src length.")
    }
    h.codeSet = make(map[interface{}]string)
    h.leaf = make(HuffmanNodeList, len(h.src))
    var i int
    for data, weight := range h.src {
        var node = &HuffmanNode{
            Weight: weight,
            Data:   data,
        }
        h.leaf[i] = node
        i++
    }
    //對(duì)leaf根據(jù)權(quán)值排序
    sort.Sort(h.leaf)
}

//構(gòu)造赫夫曼樹(shù)
//src: key為data劫灶,value為權(quán)值
func (h *HuffmanTree) build() {
    nodeList := h.leaf
    //根據(jù)huffman樹(shù)的規(guī)則構(gòu)造赫夫曼樹(shù)
    for nodeList.Len() > 1 {
        //1. 選取權(quán)值最小的兩個(gè)node構(gòu)造出第一個(gè)節(jié)點(diǎn)
        var temp = &HuffmanNode{
            Weight: nodeList[0].Weight + nodeList[1].Weight,
            Left:   nodeList[0],
            Right:  nodeList[1],
        }
        nodeList[0].Parent = temp
        nodeList[1].Parent = temp

        //2.將生成的新節(jié)點(diǎn)插入節(jié)點(diǎn)序列中
        nodeList = regroup(nodeList[2:], temp)
    }
    h.root = nodeList[0]
}

//獲取每個(gè)byte的編碼裸违,目的是為了下次需要編碼的時(shí)候不用再次遍歷樹(shù)以獲取每個(gè)byte的編碼了
//在赫夫曼樹(shù)中的所有節(jié)點(diǎn)要么沒(méi)有孩子節(jié)點(diǎn),要么有兩個(gè)孩子節(jié)點(diǎn)本昏,不存在只有一個(gè)孩子節(jié)點(diǎn)的節(jié)點(diǎn)
//此處的編碼為由底至頂獲取供汛,也可以由頂至底的獲取
func (h *HuffmanTree) parse() {
    if h.root == nil {
        return
    }
    var temp *HuffmanNode
    var code string
    for _, n := range h.leaf {
        temp = n
        for temp.Parent != nil {
            if temp == temp.Parent.Left {
                code = "0" + code
            } else {
                code = "1" + code
            }
            temp = temp.Parent
        }
        h.codeSet[n.Data] = code
        code = ""
    }
}

//重組,將生成的節(jié)點(diǎn)放入既有的list涌穆,排序后返回怔昨,權(quán)值最小的始終在最前面
func regroup(src HuffmanNodeList, temp *HuffmanNode) HuffmanNodeList {
    //將temp添加進(jìn)src,然后取出weight最小的一個(gè)
    length := len(src)
    result := make(HuffmanNodeList, len(src)+1)
    if length == 0 {
        result[0] = temp
        return result
    }
    if src[length-1].Weight <= temp.Weight {
        copy(result, src)
        result[length] = temp
        return result
    }
    for i := range src {
        if src[i].Weight <= temp.Weight {
            result[i] = src[i]
        } else {
            result[i] = temp
            copy(result[i+1:], src[i:])
            break
        }
    }
    return result
}

參考文獻(xiàn)

《數(shù)據(jù)結(jié)構(gòu)(C語(yǔ)言版)》
《Wiki》

最后

原文鏈接

謝謝閱讀宿稀,此處為源碼趁舀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市祝沸,隨后出現(xiàn)的幾起案子矮烹,更是在濱河造成了極大的恐慌,老刑警劉巖奋隶,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擂送,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唯欣,警方通過(guò)查閱死者的電腦和手機(jī)嘹吨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)境氢,“玉大人蟀拷,你說(shuō)我怎么就攤上這事∑剂模” “怎么了问芬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)寿桨。 經(jīng)常有香客問(wèn)我此衅,道長(zhǎng),這世上最難降的妖魔是什么亭螟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任挡鞍,我火速辦了婚禮,結(jié)果婚禮上预烙,老公的妹妹穿的比我還像新娘墨微。我一直安慰自己,他們只是感情好扁掸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布翘县。 她就那樣靜靜地躺著最域,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锈麸。 梳的紋絲不亂的頭發(fā)上镀脂,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音掐隐,去河邊找鬼狗热。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虑省,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僧凰,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼探颈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了训措?” 一聲冷哼從身側(cè)響起伪节,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绩鸣,沒(méi)想到半個(gè)月后怀大,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呀闻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年化借,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捡多。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蓖康,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垒手,到底是詐尸還是另有隱情蒜焊,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布科贬,位于F島的核電站泳梆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏榜掌。R本人自食惡果不足惜优妙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唐责。 院中可真熱鬧鳞溉,春花似錦、人聲如沸鼠哥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至抄罕,卻和暖如春允蚣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呆贿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工嚷兔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人做入。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓冒晰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親竟块。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壶运,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355