一、哈夫曼树的定义:
哈夫曼树:又称最优二叉树,是一种带权路径最短的树。
树的路径长度:从树根到树中每一个节点的路径长度之和。
结点之间的路径长度:从一个结点到另一个结点之间的分支数目。
结点的带权路径长度:从该结点到树根之间的路径长度与节点上权的乘积。
树的带权路径长度:Weighted Path Length of Tree,简称为WPL,树中所有叶子结点的带权路径长度之和,记作:
WPL最小的二叉树就称作最有二叉树或哈夫曼树。
二、构造哈夫曼树:
1、给出n个带有权值的结点,组成一个结点集合M;
2、从集合M中选出权值最小的两个结点A,B,权值分别为,构造一棵二叉树T,T的根结点C权值为 ;
3、删除结点A、B,将结点C加入到集合M中;
4、重复步骤2和3,直至M中只剩下一个结点为止。
三、哈弗曼编码
从哈夫曼树根结点开始,对左子树分配“0”,右子树分配“1”,直至叶子结点,然后,将树根沿每条路径到达叶子结点的代码排列起来,便可以得到哈夫曼编码。
四、练习:
将一个字符串转换成一棵HFM树,并打印出每个字符的HFM编码,将HFM树图形化。
1、先写一个节点类Node,里面存放字符串(一个字符组成的字符串)、字符串出现的次数、节点所在位置的横坐标、节点所在位置的纵坐标、左子结点、右子结点、父节点。
/** * HFM树的节点类 * @author zr * */ public class Node { private String s;//字符串 private int count;//字符串s出现的次数 private int x;//节点所在位置的横坐标 private int y;//节点所在位置的纵坐标 private Node left;//左子节点 private Node right;//右子结点 private Node parent;//父节点 public Node getParent() { return parent; } public void setParent(Node parent) { this.parent = parent; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } public String getS() { return s; } public void setS(String s) { this.s = s; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public Node getLeft() { return left; } public void setLeft(Node left) { this.left = left; } public Node getRight() { return right; } public void setRight(Node right) { this.right = right; } }
2、写一个HFM类:
(1)先把字符串转换成节点数组,其中需要求出某个字符在字符串中出现的次数,然后,我们需要获得一个无重复字符的字符串noRepeat,最后,将每个字符和它出现的次数放到一个Node对象中,最后,转换成一个节点数组。
/** * 字符串c在字符串s中出现的次数 * @param c 需要统计的对象字符串c * @param s 字符串c所在的字符串 * @return 返回字符串c在字符串s中出现的次数 */ public int getCount(String c,String s){ int count=0; for(int i=0;i<s.length();i++){ String ch=""+s.charAt(i); if(c.equals(ch)){ count++; } } return count; } /** * 把字符串转换成节点数组 * @return 返回节点数组 */ public Node [] toNodeArray(){ String noRepeat=""; for(int i=0;i<s.length();i++){ String c=""+s.charAt(i); if(noRepeat.indexOf(c)==-1){ noRepeat+=c; } } Node [] nodes=new Node[noRepeat.length()]; for(int i=0;i<noRepeat.length();i++){ Node node=new Node(); String ch=""+noRepeat.charAt(i); node.setS(ch); int count=this.getCount(ch, s); node.setCount(count); nodes[i]=node; } return nodes; }
(2)将节点按字符出现次数的大小排序,这里采用冒泡排序;
注意,当nodes[i].getCount()>nodes[j].getCount())时,要交换的是节点对象,而不是count.
/** * 把节点数组排序 * @param nodes 节点数组 */ public void sort(Node [] nodes){ for(int i=0;i<nodes.length;i++){ for(int j=i+1;j<nodes.length;j++){ if(nodes[i].getCount()>nodes[j].getCount()){ Node temp=nodes[i]; nodes[i]=nodes[j]; nodes[j]=temp; } } } }
(3)创建HFM树,完成(1)(2)步之后,这里我们采用循环,循环条件:节点数组长度>1;
a、从排好序的Node节点中选出两个count值最小的节点n1,n2,
b、再实例化一个Node对象,其count值为n1,n2的count值之和,
c、新建一个节点数组nodes2,长度为原节点数组nodes长度-1,把n3放在nodes[0]位置,原节点数组nodes中除n1,n2外的剩余节点的放到nodes2中
d、把nodes指向nodes2
/** * 创建HFM树 * @return 返回根节点 */ public Node createHFM(){ Node [] nodes=this.toNodeArray(); while(nodes.length>1){ this.sort(nodes); Node n1=nodes[0]; Node n2=nodes[1]; Node n3=new Node(); n3.setCount(n1.getCount()+n2.getCount()); n3.setLeft(n1); n3.setRight(n2); n1.setParent(n3); n2.setParent(n3); Node [] nodes2=new Node[nodes.length-1]; nodes2[0]=n3; for(int i=0;i<nodes.length-2;i++){ nodes2[i+1]=nodes[i+2]; } nodes=nodes2; } return nodes[0]; }
(4)打印HFM编码:当遇到叶子节点是开始打印;当遇到的不是叶子节点,则采用递归,则可得到HFM编码。
/** * * 打印HFM编码 * @param node 根节点 * @param code 上一个节点的编码 * @return 返回根节点 */ private Node print(Node node,String code){ if(node.getLeft()==null&&node.getRight()==null){ System.out.println("字符"+node.getS()+"出现的次数为:"+node.getCount()+",哈夫曼编码为:"+code); if(node.getX()!=0&&node.getY()!=0){ g.setColor(Color.white); g.fillRect(node.getX(), node.getY()+65, 50, 30); g.setColor(Color.black); g.drawString(code, node.getX()+25, node.getY()+80); } }else{ if(node.getLeft()!=null){ print(node.getLeft(),code+"0"); } if(node.getRight()!=null){ print(node.getRight(),code+"1"); } } return node; } /** * 打印HFM编码 */ public void print(){ Node node=this.createHFM(); this.print(node, ""); }
(5)画出HFM树:当遇到的节点没有父节点,即该节点节为根节点时,画出根节点,给出权值,接着,可以画出该父节点的左右子节点及其权值;否则,遇到左子点不为空时,可以通过递归,画出所有左子结点,遇到右子结点不为空时,也通过递归,画出所有右子结点。最后,遇到叶子结点时,打印出字符及其HFM编码。
/** * 画出HFM树 */ public void draw(){ Node node=this.createHFM(); this.drawHFM(node); } /** * 画出HFM树并打印出每个叶子结点的HFM编码 * @param node 根节点 */ private void drawHFM(Node node){ if(node.getParent()==null){ node.setX(300); node.setY(100); x1=node.getX(); y1=node.getY(); String sc1=Integer.toString(node.getCount()); g.setColor(Color.yellow); g.fillOval(x1, y1, 50, 50); g.setColor(Color.black); g.drawString(sc1, x1+25, y1+25); if(node.getLeft()!=null){ Node left=node.getLeft(); left.setX(x1-100); left.setY(y1+100); int x2=left.getX(); int y2=left.getY(); String sc2=Integer.toString(left.getCount()); g.setColor(Color.yellow); g.fillOval(x2, y2, 50, 50); g.setColor(Color.black); g.drawString(sc2, x2+25, y2+25); g.drawLine(x2+25, y2, x1+25, y1+50); g.drawString("0", (x1+x2+50)/2-10, (y1+y2+50)/2); x4=x1; y4=y1; drawHFM(left); } if(node.getRight()!=null){ Node right=node.getRight(); right.setX(x4+100); right.setY(y4+100); int x3=right.getX(); int y3=right.getY(); String sc3=Integer.toString(right.getCount()); g.setColor(Color.yellow); g.fillOval(x3, y3, 50, 50); g.setColor(Color.black); g.drawString(sc3, x3+25, y3+25); g.drawLine(x3+25, y3, x4+25, y4+50); g.drawString("1", (x4+x3+50)/2+10, (y4+y3+50)/2); drawHFM(right); } }else{ if(node.getLeft()!=null){ x1=node.getX(); y1=node.getY(); Node left=node.getLeft(); left.setX(x1-30); left.setY(y1+100); int x2=left.getX(); int y2=left.getY(); String sc2=Integer.toString(left.getCount()); g.setColor(Color.yellow); g.fillOval(x2, y2, 50, 50); g.setColor(Color.black); g.drawString(sc2, x2+25, y2+25); g.drawLine(x2+25, y2, x1+25, y1+50); g.drawString("0", (x1+x2+50)/2-10, (y1+y2+50)/2); drawHFM(left); } if(node.getRight()!=null){ x1=node.getX(); y1=node.getY(); Node right=node.getRight(); right.setX(x1+30); right.setY(y1+100); int x3=right.getX(); int y3=right.getY(); String sc3=Integer.toString(right.getCount()); g.setColor(Color.yellow); g.fillOval(x3, y3, 50, 50); g.setColor(Color.black); g.drawString(sc3, x3+25, y3+25); g.drawLine(x3+25, y3, x1+25, y1+50); g.drawString("1", (x1+x3+50)/2+10, (y1+y3+50)/2); drawHFM(right); } if(node.getLeft()==null&&node.getRight()==null){ String str=node.getS(); g.drawString(str, node.getX()+25, node.getY()+60); } } //打印HFM编码 this.print(node, ""); }
任意给出一个字符串,画出的HFM编码如下: