哈夫曼编码的概念
我们为什么要使用Huffman编码?
- 当我们使用ASCII码表的时候,其中a,b,c的编码分别是0,00,000,当他们分别出来的时候可以分辨出来,但是当一个字符串需要解码的时候,可能会出现歧义.所以我们需要一种效率高,并且不存在歧义的一种编码,于是便有了Huffman编码
哈夫曼树的构建原理
Huffman树的原理实际上就是构建一颗权值最小的最优二叉树.
- 如今有四个点a,b,c,d,他们的权值分别是2,4,5,7.我们通过这四个点来构建一个Huffman树.
- 我们选取其中权值最小的两个点来作为左子树和右子树来获得二者的双亲.将二者的双亲继续看作一个点,然后重复上述操作直至只留下一个根节点.
- 最后我们会得到上面的这样一棵树,也就是所谓的Huffman树.接下来我们来规定Huffman编码的编码方式与过程.
哈夫曼编码的构建过程
- 我们将从根节点开始遍历,当下一个结点是根节点的左子树结点的时候,那么该节点到根节点的哈夫曼编码就是'' 0 ",如果是右子树结点的化,那么该节点到根节点的哈夫曼编码就是" 1 ",最终各个点的哈夫曼编码就是从根节点到该节点的路径上的"编码之和".
- 最终得到的各点的哈夫曼编码如上所示,接下来我们通过代码的形式来更加直观的了解Huffman编码以及Huffman编码的搭建过程.
哈夫曼树的基本框架------树的创建
- 由于哈夫曼树是建立在二叉树的基础上的,所以我们需要先创建一个合适的二叉树.
public class TreeNode {
private int val; //字母对应的权值
private TreeNode left; //左子节点
private TreeNode right; //右子节点
private Character ch; //存储的字母
private String code=""; //对应的哈姆曼编码
public TreeNode() {}
public TreeNode(int val,char ch){
this.val=val;
this.ch=ch;
}
public TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
public Character getCh() {
return ch;
}
public void setCh(Character ch) {
this.ch = ch;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
", ch=" + ch +
", code='" + code + '\'' +
'}';
}
}
- 关于这一基本结构,我在设计树的基本结构的原有基础上同时加入了编码值与每个节点所对应的字符值,以此来更好更直观的进行我们接下来的操作.
Huffman树的构建与操作
Huffman树的前提条件
rivate TreeNode root;//根节点
private static ArrayList<BinaryTree> list=new ArrayList<>(); //一个森林
private static String str="******************";
private static HashMap<Character,Integer> numsMap = new HashMap<>();
private static HashMap<Character,String> codeMap=new HashMap<>();
private static int num=0;
- 创建一个哈希图来存储字符和其出现的次数,我们将其定义为numsMap,创建第二个哈希图的codeMap来存储字符与该字符的Huffman编码.
- 创建一个ArrayList<BinaryTree>类型的集合list来存储哈夫曼树的各个子树.
- 导入需要编码的字符串.
- 其中需要实现一个排序用的Comparable接口用于后面更好的排序字符出现次数的大小,因为HashMap是无序的.
@Override
public int compareTo(BinaryTree binaryTree) {
int result=(int) (this.root.getVal() - binaryTree.root.getVal());
return result;
}
统计字符串中各个字符的数量
public static void statisticTimes(){
char[] ch=str.toCharArray();
for(char c : ch){
for (int i = 'A'; i <= 'Z'; i++) { //遍历大写字母
if(c==i){
if(numsMap.containsKey(c)){
num++;
Integer val=numsMap.get(c);
numsMap.put(c,++val);
}
else{
num++;
numsMap.put(c,1);
}
}
}
for (int i = 'a'; i <= 'z' ; i++) { //遍历小写字母
if(c==i){
if(numsMap.containsKey(c)){
num++;
Integer val=numsMap.get(c);
numsMap.put(c,++val);
}
else{
num++;
numsMap.put(c,1);
}
}
}
}
}
- 首先我们将字符串转换为char类型的数组方便来计算出现次数和方便存储进哈希图中.
- 我们先遍历该字符数组,之后统计字符出现的次数,最后将其全部加入numsMap中.
构建哈夫曼树
public static void buildHuffmanTree(){
TreeNode leftNode;
TreeNode rightNode;
Set<Character> set=numsMap.keySet(); //遍历Map集合
for(Character key :set){
Integer val=numsMap.get(key);
list.add(new BinaryTree(new TreeNode(val,key))); //以权值为根结点构建n棵二叉树,形成森林
}
while(list.size()!=1){
Collections.sort(list);
leftNode = list.remove(0).getRoot();
leftNode.setCode("0");
rightNode =list.remove(0).getRoot();
rightNode.setCode("1");
TreeNode node=new TreeNode(leftNode.getVal()+rightNode.getVal(),leftNode,rightNode);
list.add(new BinaryTree(node));
}
}
-
定义两个临时变量,分别是leftNode和rightNode.之后我们定义一个散列表,并且将numsMap中的字符元素全部放入该散列表中,之后遍历该散列表,之后我们在通过散列表中的各个字符来判断其在numsMap中的出现次数,
- 之后我们将每个字符作为结点,其所出现的次数作为权值来构建一颗新的树并且将其存入森林list中.
- 此时我们开始创建Huffman树,我们首先先对list进行从小到大排序,关于排序的原理我们在之前的Comparable接口中已经实现.
- 最后我们每次将list中的前两个元素抽取作为左子树和右子树,在这之间,我们将每棵树的根节点到左子树的路径的编码变为"0",将根节点到右子树的路劲的编码变为"1",之后再将二者的权值之和和各自结点来形成新的树放入list中
- 重复上述操作直至只剩下一个根节点
构建哈夫曼编码
public static void buildHuffmanCode(TreeNode root){
if(root.getLeft()!=null){
root.getLeft().setCode(root.getCode()+root.getLeft().getCode());
buildHuffmanCode(root.getLeft());
}
if(root.getRight()!=null){
root.getRight().setCode(root.getCode()+root.getRight().getCode());
buildHuffmanCode(root.getRight());
}
}
-
首先我们在上述的构建Huffman树的过程中获得可Huffman树的根节点root,我们将沿着跟根节点到每个叶子结点进行遍历并且将编码拼接在一起来获得个字符的最终哈夫曼编码.
打印哈夫曼编码
public static void printHuffmanCode(TreeNode root){
if(root==null){
return;
}
if(root.getLeft()==null&&root.getRight()==null){
if(root.getVal()>=0&&root.getVal()<=9){
System.out.println(root.getCh()+"出现的次数是:"+root.getVal());
}
else{
System.out.println(root.getCh()+"出现的次数是:"+root.getVal());
}
System.out.println(" 对应的编码是:"+ root.getCode());
codeMap.put(root.getCh(),root.getCode());
}
printHuffmanCode(root.getLeft());
printHuffmanCode(root.getRight());
}
- 遍历哈夫曼树,并且将每个字符都放入codeMap中
Test进行验证
public static void main(String[] args) {
statisticTimes();
buildHuffmanTree();
buildHuffmanCode(list.get(0).getRoot());
printHuffmanCode(list.get(0).getRoot());
}