10. 哈夫曼树、Trie、补充

1 哈夫曼树
  1. 哈夫曼编码,又称为霍夫曼编码,它是现代压缩算法的基础
  2. 假设要把字符串【ABBBCCCCCCCCDDDDDDEE】转成二进制编码进行传输,可以转成ASCII编码,但是有点冗长
  3. 可以先约定5个字母对应的二进制,从而使编码更短
    在这里插入图片描述
  4. 注意新的编码,谁都不是谁的前缀,否则无法将二进制码解码成原字符串
  5. 对应的二进制编码:000001001001010010010010010010010010011011011011011011100100,一共20个字母,转成了60个二进制位
  6. 使用哈夫曼编码,可以压缩至41个二进制位,约为原来长度的68.3%
  7. 构建哈夫曼树
    1. 先计算出每个字母的出现频率(权值,这里直接用出现次数)
      在这里插入图片描述
    2. 以权值作为根节点构建 n 棵二叉树,组成森林
    3. 在森林中选出 2 个根节点最小的树合并,作为一棵新树的左右子树,且新树的根节点为其左右子树根节点之和
    4. 从森林中删除刚才选取的 2 棵树,并将新树加入森林
    5. 重复 2、3 步骤,直到森林只剩一棵树为止,该树即为哈夫曼树
      10. 哈夫曼树、Trie、补充_第1张图片
    6. left为0,right为1,可以得出5个字母对应的哈夫曼编码
      在这里插入图片描述
    7. 因此ABBBCCCCCCCCDDDDDDEE的哈夫曼编码是1110110110110000000001010101010101111
      10. 哈夫曼树、Trie、补充_第2张图片
  8. 总结
    1. n 个权值构建出来的哈夫曼树拥有 n 个叶子节点
    2. 5个字母对应的哈夫曼编码实际上就是叶子节点的路径二进制码表示,而每个叶子节点的路径都不可能是其它叶子节点的前缀,所以不会重复
    3. 哈夫曼树可以保证出现频率最高字母的编码最短,因此可以保证哈夫曼树是带权路径长度最短的树
    4. 带权路径长度:树中所有的叶子节点的权值乘上其到根节点的路径长度。与最终的哈夫曼编码总长度成正比关系
2 Trie
  1. Trie 也叫做字典树、前缀树(Prefix Tree)、单词查找树
  2. Trie 搜索字符串的效率主要跟待查找的字符串的长度有关
  3. 假设使用 Trie 存储 cat、dog、doggy、does、cast、add 六个单词
    10. 哈夫曼树、Trie、补充_第3张图片
  4. Trie 的缺点:需要耗费大量的内存,因此还有待改进
  5. Trie
package com.mj;

import java.util.HashMap;

public class Trie<V> {
	private int size;
	private Node<V> root;
	
	public int size() {
		return size;
	}

	public boolean isEmpty() {
		return size == 0;
	}

	public void clear() {
		size = 0;
		root = null;
	}

	public V get(String key) {
		Node<V> node = node(key);
		return node != null && node.word ? node.value : null;
	}

	public boolean contains(String key) {
		Node<V> node = node(key);
		return node != null && node.word;
	}

	public V add(String key, V value) {
		keyCheck(key);
		
		// 创建根节点
		if (root == null) {
			root = new Node<>(null);
		}

		Node<V> node = root;
		int len = key.length();
		for (int i = 0; i < len; i++) {
			char c = key.charAt(i); 
			//因为想通过node.children.get(c)获取值,但node.children可能为null,导致空指针,因此此处需要特殊处理
			boolean emptyChildren = node.children == null;
			Node<V> childNode = null;
			//如果node.children都不存在,就应该创建一个空的,如果存在,应该重新查一下node.children.get(c)是否存在
			if(emptyChildren) {
				node.children = new HashMap<>();
			}else {
				childNode = node.children.get(c);
			}
			//如果该节点为空,比如dog,意味着找到了d,但不存在o,应该创建o,并将o放入到d的children中,然后将o作为新的node,继续查找其是否有d
			if (childNode == null) {
				childNode = new Node<>(node);
				childNode.character = c;
				node.children.put(c, childNode);
			}
			node = childNode;
		}
		
		if (node.word) { // 已经存在这个单词
			V oldValue = node.value;
			node.value = value;
			return oldValue;
		}
		
		// 新增一个单词
		node.word = true;
		node.value = value;
		size++;
		return null;
	}

	public V remove(String key) {
		// 找到最后一个节点
		Node<V> node = node(key);
		// 如果不是单词结尾,不用作任何处理
		if (node == null || !node.word) return null;
		size--;
		V oldValue = node.value;
		
		// 如果还有子节点
		if (node.children != null && !node.children.isEmpty()) {
			node.word = false;
			node.value = null;
			return oldValue;
		}
		
		// 如果没有子节点
		//没有子节点情况,应该从后往前删,直到某个节点还有其他子节点,或父节点本身就是红色,就不能继续删了
		Node<V> parent = null;
		while ((parent = node.parent) != null) {
			parent.children.remove(node.character);
			//表示父节点删除了其下面的一个节点后,还有其他节点,就不需要继续删除了
			if (parent.word || !parent.children.isEmpty()) break;
			node = parent;
		}
		
		return oldValue;
	}

	public boolean startsWith(String prefix) {
		return node(prefix) != null;
	}
	
	private Node<V> node(String key) {
		keyCheck(key);
		
		Node<V> node = root;
		int len = key.length();
		for (int i = 0; i < len; i++) {
			if (node == null || node.children == null || node.children.isEmpty()) return null;
			char c = key.charAt(i); 
			node = node.children.get(c);
		}
		
		return node;
	}
	
	private void keyCheck(String key) {
		if (key == null || key.length() == 0) {
			throw new IllegalArgumentException("key must not be empty");
		}
	}
	
	private static class Node<V> {
		//因为删除时,如果没有子节点,需要向上找第一个拥有子节点的父亲节点,所以需要一个parent指针,获取父节点
		Node<V> parent;
		HashMap<Character, Node<V>> children;
		//也是为了删除创建,因为删除时,需要将父节点的children中的指定character删除
		Character character;
		V value;
		// 是否为单词的结尾(是否为一个完整的单词),就是图中红色的节点
		boolean word; // 是否为单词的结尾(是否为一个完整的单词)
		public Node(Node<V> parent) {
			this.parent = parent;
		}
	}
}

3 补充
3.1 四则运算
  1. 四则运算的表达式可以分为3种
    1. 前缀表达式(prefix expression),又称为波兰表达式
      1. 从最右面的运算符开始算起,每次都是将运算符的后两个数做运算
    2. 中缀表达式(infix expression)
    3. 后缀表达式(postfix expression),又称为逆波兰表达式
      10. 哈夫曼树、Trie、补充_第4张图片
  2. 表达式树
    1. 如果将表达式的操作数作为叶子节点,运算符作为父节点(假设只是四则运算)
    2. 这些节点刚好可以组成一棵二叉树
    3. 比如表达式:A / B + C * D – E
      10. 哈夫曼树、Trie、补充_第5张图片
    4. 前序遍历
      1. – + / A B * C D E
      2. 刚好就是前缀表达式(波兰表达式)
    5. 中序遍历
      1. A / B + C * D – E
      2. 刚好就是中缀表达式
    6. 后序遍历
      1. A B / C D * + E –
      2. 刚好就是后缀表达式(逆波兰表达式)

你可能感兴趣的:(数据结构与算法)