数据结构 - Huffman Tree
Huffman Tree练手

介绍

霍夫曼树一般用于两种场景

  • 加解密文,通过霍夫曼树可以生成一个字典,然后通过字典对内容进行加密与解密
  • 压缩算法,根据词频生成一个霍夫曼树,其特点是出现次数越多的词,他的路径是最短的,从而起到压缩作用

其他霍夫曼树的介绍及原理可以参考https://oi-wiki.org/ds/huffman-tree/

代码

使用优先队列创建霍夫曼树,使用栈模拟后根遍历生成编码。

package main

import (
	"container/heap"
	"fmt"
	"strings"
)

type Node struct {
	Left *Node
	Right *Node
	Weight int
	Word byte
}

// 用于构造小顶堆
type Nodes []*Node

func (ns Nodes) Len() int {
	return len(ns)
}

func (ns Nodes) Swap(i, j int) {
	ns[i], ns[j] = ns[j], ns[i]
}

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

func (ns *Nodes) Push(v interface{}) {
	*ns = append(*ns, v.(*Node))
}

func (ns *Nodes) Pop() (v interface{}) {
	old := *ns
	n := len(old)
	v, *ns = old[n-1], old[:n-1]
	return
}

type HuffTree struct {
	Root *Node
	WordDict map[byte]string
}

func (ht *HuffTree) Initialize(freqDict map[byte]int) {
	ns := &Nodes{}
	// 压入堆
	for word, freq := range freqDict {
		heap.Push(ns, &Node{
			Weight: freq,
			Word: word,
		})
	}
	// 取最小的两个节点,拼接组合成新节点,并重新入堆
	for ns.Len() > 1 {
		top1 := heap.Pop(ns).(*Node)
		top2 := heap.Pop(ns).(*Node)
		heap.Push(ns, &Node{
			Left: top1,
			Right: top2,
			Weight: top1.Weight + top2.Weight,
		})
	}

	// 最后取得的即Root节点
	ht.Root = heap.Pop(ns).(*Node)
	ht.GenDict()
	for k, v := range ht.WordDict {
		fmt.Printf("%s:%s ", string(k), v)
	}
	fmt.Println()
}

// 根据哈夫曼树生成字典
func (ht *HuffTree) GenDict() {
	if ht.Root == nil {
		return
	}

	// 通过栈后序遍历的方式生成字典
	stack := make([]*Node, 0)
	n := ht.Root
	var prev *Node

	// 当前的哈夫曼编码
	code := make([]byte, 0)
	for len(stack) != 0 || n != nil {
		// 左子树一直入队
		for n != nil {
			stack = append(stack, n)
			n = n.Left

			// 此路径为左侧路径,编码追加0
			// 当n不等于nil,才追加,因为两个节点间只有一条边,以此类推,三个节点只有两条边
			if n != nil {
				code = append(code, '0')
			}
		}

		// 此时栈top节点没有左子树
		top := stack[len(stack)-1]
		// 如果不存在右子树,或者右子树已经被遍历过(prev标记),弹出栈顶节点
		if top.Right == nil || prev == top.Right {
			// 弹出,此时可以做后序遍历的操作
			stack = stack[:len(stack)-1]
			// 如果弹出的节点为叶子节点,生成字符对应的编码
			if top.Left == nil && top.Right == nil {
				ht.WordDict[top.Word] = string(code)
			}
			// 根节点就不弹了
			if len(code) != 0 {
				// 将编码的最后一位也一并弹出,回到父节点的编码状态
				code = code[:len(code)-1]
			}
			// 标记当前节点的左右子树均已经遍历
			prev = top
		} else {
			// 走右侧追加1
			code = append(code, '1')
			n = top.Right
		}
	}
}

func (ht *HuffTree) EncodeString(str string) string {
	s := strings.Builder{}
	for i := range str {
		if _, ok := ht.WordDict[str[i]]; !ok {
			fmt.Println("None exist word[", str[i], "]")
			return ""
		}
		s.WriteString(ht.WordDict[str[i]])
	}
	return s.String()
}

func (ht *HuffTree) DecodeString(str string) string {
	s := strings.Builder{}
	for i := 0; i < len(str); {
		n := ht.Root
		for i < len(str) && n.Left != nil && n.Right != nil {
			if str[i] == '0' {
				// 往左走
				n = n.Left
			} else if str[i] == '1' {
				// 往右走
				n = n.Right
			} else {
				fmt.Println("Error.")
				return ""
			}
			i++
		}
		s.WriteString(string(n.Word))
	}
	return s.String()
}

func main() {
	words := "Hello worlddddddddddddddd."
	dict := make(map[byte]int)
	// 同级词频
	for _, v := range words {
		dict[byte(v)]++
	}

	ht := &HuffTree{
		WordDict: make(map[byte]string),
	}
	// 初始化霍夫曼树
	ht.Initialize(dict)
	// 编码
	encoded := ht.EncodeString(words)
	fmt.Println("Encoded:", encoded)
	// 解码
	decoded := ht.DecodeString(encoded)
	fmt.Println("Decoded:", decoded)
}

程序运行后的输出如下,可正常编解码

.:01111 :01100 H:0010 e:0011 l:010 r:01101 w:01110 d:1 o:000 Encoded: 0010001101001000001100011100000110101011111111111111101111 Decoded: Hello worlddddddddddddddd.

衍生题目

https://leetcode-cn.com/problems/concatenated-words/

LeetCode第472题

472. 连接词

难度困难235收藏分享切换为英文接收动态反馈

给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词

连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串。

示例 1:

输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成; 
     "dogcatsdog" 由 "dog", "cats" 和 "dog" 组成; 
     "ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。

示例 2:

输入:words = ["cat","dog","catdog"]
输出:["catdog"]

提示:

  • 1 <= words.length <= 104
  • 0 <= words[i].length <= 1000
  • words[i] 仅由小写字母组成
  • 0 <= sum(words[i].length) <= 105

可使用字典树解题。

// 节点对象
type Node struct {
    Nexts [26]*Node
    IsEnd bool
}

// 字典树对象
type Dict struct {
    Root *Node
}

// 构造路径
func (d *Dict) Push(s string) {
    n := d.Root
    for i := range s {
        c := s[i] - 'a'
        if n.Nexts[c] == nil {
            n.Nexts[c] = &Node{}
        }
        n = n.Nexts[c]
    }
    n.IsEnd = true
}

// 递归搜索传入word是否可由已有的字符串组成
func (d *Dict) Dfs(visited []bool, word string, start int) bool {
    // 如果已经到达尾部,返回True
    if start == len(word) {
        return true
    }
    // 如果已经遍历过了,则直接返回false,因为前面此路径已经被尝试过了,且未成功
    if visited[start] {
        return false
    }
    visited[start] = true
    n := d.Root
    for i := start; i < len(word); i++ {
        n = n.Nexts[word[i]-'a']
        if n == nil {
            return false
        }
        // 找到一个单词了,再次递归看后续部分能否由已有的字符串组成
        if n.IsEnd && d.Dfs(visited, word, i+1) {
            return true
        }
    }
    return false
}

func findAllConcatenatedWordsInADict(words []string) (ans []string) {
    // 排序words数组,贪心,长度小的单词应该有些判断
    sort.Slice(words, func(i, j int) bool {
        return len(words[i]) < len(words[j])
    })

    dt := &Dict{
        Root: &Node{},
    }
    // 遍历排序后的数组,如果发现可以由已有部分组成一个完整的单词,则加入答案,反正加入字典树
    for _, word := range words {
        // 过滤空串
        if len(word) == 0 {
            continue
        }
        if dt.Dfs(make([]bool, len(word)), word, 0) {
            ans = append(ans, word)
        } else {
            dt.Push(word)
        }
    }
    return ans
}

最后修改于 2022-01-25