什么是哈夫曼树
在介绍哈夫曼树前,我们先介绍二叉树的基本概念,以便大家更好地理解哈夫曼树:
- 路径:两个节点之间分支的连线即两个节点之间的路径。
- 路径长:两个节点之间路径所包含分支的和。
- 深度:根节点的深度为0,其子节点的深度为1,往下逐一递推。
- 子节点数:和普通的树不同,二叉树从根节点出发,每个节点最多只能有两个子节点。
- 满二叉树:除了叶子结点,每个节点都有两个子节点。
哈夫曼树是一种最优的二叉树,它的带权路径最短。那么怎么算一个二叉树的权值呢?我们从根节点开始递推,根节点的权值为0,随着深度的递增,权值也递增,同一深度的节点权值是相同的,每个节点都有它的数据值,每个节点的数据值乘权值累加即可得到树的权值,哈夫曼树是带权路径最短的树。如图:
权值1=73+53+32+11=43
权值2=13+33+52+71=29
比较这两棵树的权值我们可以发现:要使带权路径最短,较大的数应该靠上放,较小的靠下放。第二张图才是带权路径最短的二叉树,即最优二叉树(哈夫曼树)。
如何生成一棵哈夫曼树
- 把需要用来构建哈夫曼树的数组进行排序。
- 取最小的两个数作为叶子结点,生成双亲节点,双亲节点的值为二者的值之和。
- 从数组中删除这两个节点,添加生成的双亲节点入数组并重新排序。
- 重复以上操作直至数组只剩下一个新生成的节点,它就是哈夫曼树的根节点。
哈夫曼编码
哈夫曼树是最优的二叉树,带权路径最短,这意味着哈夫曼树上数值较大的节点更加靠近根节点。信号的传输是以字节为单位的,ASCII码表示的255个字符分别可以用一个字节来表示,一个字符的传输需要用到一个字节(8bit),如果在一段报文中我们只用到了一部分字符,并且不同字符出现的频率还是不同的,我们能否用一些特殊的方法来对其加工从而压缩需要传输的总字节数呢?我们试着去缩短那些出现频率高的字符的编码表示,联系我们学的哈夫曼树,我们做这样的假设:从根节点出发,向左记“0”,向右记“1”,在哈夫曼树中,根节点是不带数据的,它的左子节点可以用“0”来编码,左子节点的左子节点用“00”表示,左子节点的右子节点用“01”表示,根节点的右子节点用“1”表示…以此类推,我们可以为树上所有的节点分配一个编码,编码是不重复的,且靠近根节点的那些出现频次较高的节点对应的编码较短。这样我们每个字符对应的编码表示都会小于等于8位,这极大压缩了需要传输的数据大小。
哈夫曼树的实现
1、节点类:
这是一种支持泛型的节点类,我们定义了一些方法来操作节点的数据。
public class Node<T> implements Comparable<Node<T>>{
private T data;
private int weight;
private Node<T> left;
private Node<T> right;
public Node(T data,int weight)
{
this.data=data;
this.weight=weight;
}
/**
* 获取节点数据
*/
public String toString()
{
return "data:"+data+" "+"weight:"+weight;
}
/**
* 节点权值比较方法
* @param o
* @return
*/
public int compareTo(Node<T> o) {
if(this.weight>o.weight)
return 1;
else if(this.weight<o.weight)
return -1;
return 0;
}
public void setData(T data)
{
this.data=data;
}
public void setWeight(int weight)
{
this.weight=weight;
}
public T getData()
{
return data;
}
public int getWeight()
{
return weight;
}
public void setLeft(Node<T> node)
{
this.left=node;
}
public void setRight(Node<T> node)
{
this.right=node;
}
public Node<T> getLeft()
{
return this.left;
}
public Node<T> getRight()
{
return this.right;
}
}
2、建树:
步骤:
1、输入字符串。
2、统计各字符出现的次数,生成ASCII码数组。
3、用数组生成节点并存放在链表中。
4、根据字符出现频率对链表中的节点进行排序。
5、根据链表中的有序节点生成哈夫曼树。
6、根据哈夫曼树获取输入字符对应的哈夫曼编码值,存入HashMap。
public class Create {
public static void main(String[] args)
{
Create ct = new Create();
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String str = sc.nextLine();
int[] a = ct.getArrays(str);
System.out.println(Arrays.toString(a));
LinkedList<Node<String>> list = ct.createNodeList(a);//把数组的元素转为节点并存入链表
for(int i=0;i<list.size();i++)
{
System.out.println(list.get(i).toString());
}
Node<String> root = ct.CreateHFMTree(list); //建树
System.out.println("根节点权重:"+root.getWeight());
System.out.println("打印整棵树、、、、");
ct.inOrder(root); //打印整棵树
System.out.println("获取叶子结点哈夫曼编码");
HashMap<String,String> map = ct.getAllCode(root);
}
/**
* 通过字符串获取数组的方法
* @param str
*/
public int[] getArrays(String str)
{
int[] arrays = new int[256];
for(int i=0;i<str.length();i++)
{
int Ascii = str.charAt(i);
arrays[Ascii]++;
}
return arrays;
}
/**
* 把获得的数组转化为节点并存在链表中
* @param arrays
* @return
*/
public LinkedList<Node<String>> createNodeList(int[] arrays)
{
LinkedList<Node<String>> list = new LinkedList<>();
for(int i=0;i<arrays.length;i++)
{
if(arrays[i]!=0)
{
String ch = (char)i+"";
Node<String> node = new Node<String>(ch,arrays[i]); //构建节点并传入字符和权值
list.add(node); //添加节点
}
}
return list;
}
/**
* 对链表中的元素排序
* @param list
* @return
*/
public void sortList(LinkedList<Node<String>> list)
{
for(int i=list.size();i>1;i--)
{
for(int j=0; j<i-1;j++)
{
Node<String> node1 = list.get(j);
Node<String> node2 = list.get(j+1);
if(node1.getWeight()>node2.getWeight())
{
int temp ;
temp = node2.getWeight();
node2.setWeight(node1.getWeight());
node1.setWeight(temp);
String tempChar;
tempChar = node2.getData();
node2.setData(node1.getData());
node1.setData(tempChar);
Node<String> tempNode = new Node<String>(null, 0);
tempNode.setLeft(node2.getLeft());
tempNode.setRight(node2.getRight());
node2.setLeft(node1.getLeft());
node2.setRight(node1.getRight());
node1.setLeft(tempNode.getLeft());
node1.setRight(tempNode.getRight());
}
}
}
}
/**
* 建树的方法
* @param list
*/
public Node<String> CreateHFMTree(LinkedList<Node<String>> list)
{
while(list.size()>1)
{
sortList(list); //排序节点链表
Node<String> nodeLeft = list.removeFirst();
Node<String> nodeRight = list.removeFirst();
Node<String> nodeParent = new Node<String>( null ,nodeLeft.getWeight()+nodeRight.getWeight());
nodeParent.setLeft(nodeLeft);
nodeParent.setRight(nodeRight);
list.addFirst(nodeParent);
}
return list.get(0);//返回根节点
}
public HashMap<String, String> getAllCode(Node<String> root)
{
HashMap<String, String> map = new HashMap<>();
inOrderGetCode("", map, root);
return map;
}
/**
* 查询指定字符的哈弗曼编码(中序遍历)
* @param code
* @param st
* @param root
* @return
*/
public void inOrderGetCode(String code ,HashMap<String, String> map,Node<String> root)
{
if(root!=null)
{
inOrderGetCode(code+"0",map,root.getLeft());
if(root.getLeft()==null&&root.getRight()==null)//存储叶子结点的哈夫曼编码
{
System.out.println(root.getData());
System.out.println(code);
map.put(root.getData(), code);
}
inOrderGetCode(code+"1",map,root.getRight());
}
}
/**
* 中序遍历输出整棵树
* @param root
* @return
*/
public void inOrder(Node<String> root)
{
if(root!=null)
{
inOrder(root.getLeft());
if(root.getData()!=null)
System.out.println(root.getData());
inOrder(root.getRight());
}
}
}
3、测试:
输入一字符串,选取几个key值观察其对应的编码值是否和理论值相同:
1、输入字符串,获得了一些节点数据。
2、排序整理。
3、建树并返回根节点,打印权值。
4、打印整棵树,观察中序遍历是否正确。
5、在往HashMap存值前打印字符和编码的对应关系。