Huffman编码算法之Java实现

目录(?)[+]

Huffman编码介绍

Huffman编码处理的是字符以及字符对应的二进制的编码配对问题,分为编码和解码,目的是压缩字符对应的二进制数据长度。我们知道字符存贮和传输的时候都是二进制的(计算机只认识0/1),那么就有字符与二进制之间的mapping关系。字符属于字符集(Charset), 字符需要通过编码(encode)为二进制进行存贮和传输,显示的时候需要解码(decode)回字符,字符集与编码方法是一对多关系(Unicode可以用UTF-8,UTF-16等编码)。理解了字符集,编码以及解码,满天飞的乱码问题也就游刃而解了。以英文字母小写a为例, ASCII编码中,十进制为97,二进制为01100001。ASCII的每一个字符都用8个Bit(1Byte)编码,假如有1000个字符要传输,那么就要传输8000个Bit。问题来了,英文中字母e的使用频率为12.702%,而z为0.074%,前者是后者的100多倍,但是确使用相同位数的二进制。可以做得更好,方法就是可变长度编码,指导原则就是频率高的用较短的位数编码,频率低的用较长位数编码。Huffman编码算法就是处理这样的问题。

Huffman编码Java实现

Huffman编码算法主要用到的数据结构是完全二叉树(full binary tree)和优先级队列。后者用的是java.util.PriorityQueue,前者自己实现(都为内部类),代码如下:

  1. staticclassTree{
  2. privateNoderoot;
  3. publicNodegetRoot(){
  4. returnroot;
  5. }
  6. publicvoidsetRoot(Noderoot){
  7. this.root=root;
  8. }
  9. }
  10. staticclassNodeimplementsComparable<Node>{
  11. privateStringchars="";
  12. privateintfrequence=0;
  13. privateNodeparent;
  14. privateNodeleftNode;
  15. privateNoderightNode;
  16. @Override
  17. publicintcompareTo(Noden){
  18. returnfrequence-n.frequence;
  19. }
  20. publicbooleanisLeaf(){
  21. returnchars.length()==1;
  22. }
  23. publicbooleanisRoot(){
  24. returnparent==null;
  25. }
  26. publicbooleanisLeftChild(){
  27. returnparent!=null&&this==parent.leftNode;
  28. }
  29. publicintgetFrequence(){
  30. returnfrequence;
  31. }
  32. publicvoidsetFrequence(intfrequence){
  33. this.frequence=frequence;
  34. }
  35. publicStringgetChars(){
  36. returnchars;
  37. }
  38. publicvoidsetChars(Stringchars){
  39. this.chars=chars;
  40. }
  41. publicNodegetParent(){
  42. returnparent;
  43. }
  44. publicvoidsetParent(Nodeparent){
  45. this.parent=parent;
  46. }
  47. publicNodegetLeftNode(){
  48. returnleftNode;
  49. }
  50. publicvoidsetLeftNode(NodeleftNode){
  51. this.leftNode=leftNode;
  52. }
  53. publicNodegetRightNode(){
  54. returnrightNode;
  55. }
  56. publicvoidsetRightNode(NoderightNode){
  57. this.rightNode=rightNode;
  58. }
  59. }

统计数据

既然要按频率来安排编码表,那么首先当然得获得频率的统计信息。我实现了一个方法处理这样的问题。如果已经有统计信息,那么转为Map<Character,Integer>即可。如果你得到的信息是百分比,乘以100或1000,或10000。总是可以转为整数。比如12.702%乘以1000为12702,Huffman编码只关心大小问题。统计方法实现如下:

  1. publicstaticMap<Character,Integer>statistics(char[]charArray){
  2. Map<Character,Integer>map=newHashMap<Character,Integer>();
  3. for(charc:charArray){
  4. Charactercharacter=newCharacter(c);
  5. if(map.containsKey(character)){
  6. map.put(character,map.get(character)+1);
  7. }else{
  8. map.put(character,1);
  9. }
  10. }
  11. returnmap;
  12. }

构建树

构建树是Huffman编码算法的核心步骤。思想是把所有的字符挂到一颗完全二叉树的叶子节点,任何一个非页子节点的左节点出现频率不大于右节点。算法为把统计信息转为Node存放到一个优先级队列里面,每一次从队列里面弹出两个最小频率的节点,构建一个新的父Node(非叶子节点), 字符内容刚弹出来的两个节点字符内容之和,频率也是它们的和,最开始的弹出来的作为左子节点,后面一个作为右子节点,并且把刚构建的父节点放到队列里面。重复以上的动作N-1次,N为不同字符的个数(每一次队列里面个数减1)。结束以上步骤,队列里面剩一个节点,弹出作为树的根节点。代码如下:

  1. privatestaticTreebuildTree(Map<Character,Integer>statistics,
  2. List<Node>leafs){
  3. Character[]keys=statistics.keySet().toArray(newCharacter[0]);
  4. PriorityQueue<Node>priorityQueue=newPriorityQueue<Node>();
  5. for(Charactercharacter:keys){
  6. Nodenode=newNode();
  7. node.chars=character.toString();
  8. node.frequence=statistics.get(character);
  9. priorityQueue.add(node);
  10. leafs.add(node);
  11. }
  12. intsize=priorityQueue.size();
  13. for(inti=1;i<=size-1;i++){
  14. Nodenode1=priorityQueue.poll();
  15. Nodenode2=priorityQueue.poll();
  16. NodesumNode=newNode();
  17. sumNode.chars=node1.chars+node2.chars;
  18. sumNode.frequence=node1.frequence+node2.frequence;
  19. sumNode.leftNode=node1;
  20. sumNode.rightNode=node2;
  21. node1.parent=sumNode;
  22. node2.parent=sumNode;
  23. priorityQueue.add(sumNode);
  24. }
  25. Treetree=newTree();
  26. tree.root=priorityQueue.poll();
  27. returntree;
  28. }

编码

某个字符对应的编码为,从该字符所在的叶子节点向上搜索,如果该字符节点是父节点的左节点,编码字符之前加0,反之如果是右节点,加1,直到根节点。只要获取了字符和二进制码之间的mapping关系,编码就非常简单。代码如下:

  1. publicstaticStringencode(StringoriginalStr,
  2. Map<Character,Integer>statistics){
  3. if(originalStr==null||originalStr.equals("")){
  4. return"";
  5. }
  6. char[]charArray=originalStr.toCharArray();
  7. List<Node>leafNodes=newArrayList<Node>();
  8. buildTree(statistics,leafNodes);
  9. Map<Character,String>encodInfo=buildEncodingInfo(leafNodes);
  10. StringBufferbuffer=newStringBuffer();
  11. for(charc:charArray){
  12. Charactercharacter=newCharacter(c);
  13. buffer.append(encodInfo.get(character));
  14. }
  15. returnbuffer.toString();
  16. }
  1. privatestaticMap<Character,String>buildEncodingInfo(List<Node>leafNodes){
  2. Map<Character,String>codewords=newHashMap<Character,String>();
  3. for(NodeleafNode:leafNodes){
  4. Charactercharacter=newCharacter(leafNode.getChars().charAt(0));
  5. Stringcodeword="";
  6. NodecurrentNode=leafNode;
  7. do{
  8. if(currentNode.isLeftChild()){
  9. codeword="0"+codeword;
  10. }else{
  11. codeword="1"+codeword;
  12. }
  13. currentNode=currentNode.parent;
  14. }while(currentNode.parent!=null);
  15. codewords.put(character,codeword);
  16. }
  17. returncodewords;
  18. }

解码

因为Huffman编码算法能够保证任何的二进制码都不会是另外一个码的前缀,解码非常简单,依次取出二进制的每一位,从树根向下搜索,1向右,0向左,到了叶子节点(命中),退回根节点继续重复以上动作。代码如下:

  1. publicstaticStringdecode(StringbinaryStr,
  2. Map<Character,Integer>statistics){
  3. if(binaryStr==null||binaryStr.equals("")){
  4. return"";
  5. }
  6. char[]binaryCharArray=binaryStr.toCharArray();
  7. LinkedList<Character>binaryList=newLinkedList<Character>();
  8. intsize=binaryCharArray.length;
  9. for(inti=0;i<size;i++){
  10. binaryList.addLast(newCharacter(binaryCharArray[i]));
  11. }
  12. List<Node>leafNodes=newArrayList<Node>();
  13. Treetree=buildTree(statistics,leafNodes);
  14. StringBufferbuffer=newStringBuffer();
  15. while(binaryList.size()>0){
  16. Nodenode=tree.root;
  17. do{
  18. Characterc=binaryList.removeFirst();
  19. if(c.charValue()=='0'){
  20. node=node.leftNode;
  21. }else{
  22. node=node.rightNode;
  23. }
  24. }while(!node.isLeaf());
  25. buffer.append(node.chars);
  26. }
  27. returnbuffer.toString();
  28. }

测试以及比较

以下测试Huffman编码的正确性(先编码,后解码,包括中文),以及Huffman编码与常见的字符编码的二进制字符串比较。代码如下:

  1. publicstaticvoidmain(String[]args){
  2. StringoriStr="Huffmancodescompressdataveryeffectively:savingsof20%to90%aretypical,"
  3. +"dependingonthecharacteristicsofthedatabeingcompressed.中华崛起";
  4. Map<Character,Integer>statistics=statistics(oriStr.toCharArray());
  5. StringencodedBinariStr=encode(oriStr,statistics);
  6. StringdecodedStr=decode(encodedBinariStr,statistics);
  7. System.out.println("Originalsstring:"+oriStr);
  8. System.out.println("Huffmanencoedbinarystring:"+encodedBinariStr);
  9. System.out.println("decodedstringfrombinariystring:"+decodedStr);
  10. System.out.println("binarystringofUTF-8:"
  11. +getStringOfByte(oriStr,Charset.forName("UTF-8")));
  12. System.out.println("binarystringofUTF-16:"
  13. +getStringOfByte(oriStr,Charset.forName("UTF-16")));
  14. System.out.println("binarystringofUS-ASCII:"
  15. +getStringOfByte(oriStr,Charset.forName("US-ASCII")));
  16. System.out.println("binarystringofGB2312:"
  17. +getStringOfByte(oriStr,Charset.forName("GB2312")));
  18. }
  19. publicstaticStringgetStringOfByte(Stringstr,Charsetcharset){
  20. if(str==null||str.equals("")){
  21. return"";
  22. }
  23. byte[]byteArray=str.getBytes(charset);
  24. intsize=byteArray.length;
  25. StringBufferbuffer=newStringBuffer();
  26. for(inti=0;i<size;i++){
  27. bytetemp=byteArray[i];
  28. buffer.append(getStringOfByte(temp));
  29. }
  30. returnbuffer.toString();
  31. }
  32. publicstaticStringgetStringOfByte(byteb){
  33. StringBufferbuffer=newStringBuffer();
  34. for(inti=7;i>=0;i--){
  35. bytetemp=(byte)((b>>i)&0x1);
  36. buffer.append(String.valueOf(temp));
  37. }
  38. returnbuffer.toString();
  39. }

Huffman编码介绍

Huffman编码处理的是字符以及字符对应的二进制的编码配对问题,分为编码和解码,目的是压缩字符对应的二进制数据长度。我们知道字符存贮和传输的时候都是二进制的(计算机只认识0/1),那么就有字符与二进制之间的mapping关系。字符属于字符集(Charset), 字符需要通过编码(encode)为二进制进行存贮和传输,显示的时候需要解码(decode)回字符,字符集与编码方法是一对多关系(Unicode可以用UTF-8,UTF-16等编码)。理解了字符集,编码以及解码,满天飞的乱码问题也就游刃而解了。以英文字母小写a为例, ASCII编码中,十进制为97,二进制为01100001。ASCII的每一个字符都用8个Bit(1Byte)编码,假如有1000个字符要传输,那么就要传输8000个Bit。问题来了,英文中字母e的使用频率为12.702%,而z为0.074%,前者是后者的100多倍,但是确使用相同位数的二进制。可以做得更好,方法就是可变长度编码,指导原则就是频率高的用较短的位数编码,频率低的用较长位数编码。Huffman编码算法就是处理这样的问题。

Huffman编码Java实现

Huffman编码算法主要用到的数据结构是完全二叉树(full binary tree)和优先级队列。后者用的是java.util.PriorityQueue,前者自己实现(都为内部类),代码如下:

  1. staticclassTree{
  2. privateNoderoot;
  3. publicNodegetRoot(){
  4. returnroot;
  5. }
  6. publicvoidsetRoot(Noderoot){
  7. this.root=root;
  8. }
  9. }
  10. staticclassNodeimplementsComparable<Node>{
  11. privateStringchars="";
  12. privateintfrequence=0;
  13. privateNodeparent;
  14. privateNodeleftNode;
  15. privateNoderightNode;
  16. @Override
  17. publicintcompareTo(Noden){
  18. returnfrequence-n.frequence;
  19. }
  20. publicbooleanisLeaf(){
  21. returnchars.length()==1;
  22. }
  23. publicbooleanisRoot(){
  24. returnparent==null;
  25. }
  26. publicbooleanisLeftChild(){
  27. returnparent!=null&&this==parent.leftNode;
  28. }
  29. publicintgetFrequence(){
  30. returnfrequence;
  31. }
  32. publicvoidsetFrequence(intfrequence){
  33. this.frequence=frequence;
  34. }
  35. publicStringgetChars(){
  36. returnchars;
  37. }
  38. publicvoidsetChars(Stringchars){
  39. this.chars=chars;
  40. }
  41. publicNodegetParent(){
  42. returnparent;
  43. }
  44. publicvoidsetParent(Nodeparent){
  45. this.parent=parent;
  46. }
  47. publicNodegetLeftNode(){
  48. returnleftNode;
  49. }
  50. publicvoidsetLeftNode(NodeleftNode){
  51. this.leftNode=leftNode;
  52. }
  53. publicNodegetRightNode(){
  54. returnrightNode;
  55. }
  56. publicvoidsetRightNode(NoderightNode){
  57. this.rightNode=rightNode;
  58. }
  59. }

统计数据

既然要按频率来安排编码表,那么首先当然得获得频率的统计信息。我实现了一个方法处理这样的问题。如果已经有统计信息,那么转为Map<Character,Integer>即可。如果你得到的信息是百分比,乘以100或1000,或10000。总是可以转为整数。比如12.702%乘以1000为12702,Huffman编码只关心大小问题。统计方法实现如下:

  1. publicstaticMap<Character,Integer>statistics(char[]charArray){
  2. Map<Character,Integer>map=newHashMap<Character,Integer>();
  3. for(charc:charArray){
  4. Charactercharacter=newCharacter(c);
  5. if(map.containsKey(character)){
  6. map.put(character,map.get(character)+1);
  7. }else{
  8. map.put(character,1);
  9. }
  10. }
  11. returnmap;
  12. }

构建树

构建树是Huffman编码算法的核心步骤。思想是把所有的字符挂到一颗完全二叉树的叶子节点,任何一个非页子节点的左节点出现频率不大于右节点。算法为把统计信息转为Node存放到一个优先级队列里面,每一次从队列里面弹出两个最小频率的节点,构建一个新的父Node(非叶子节点), 字符内容刚弹出来的两个节点字符内容之和,频率也是它们的和,最开始的弹出来的作为左子节点,后面一个作为右子节点,并且把刚构建的父节点放到队列里面。重复以上的动作N-1次,N为不同字符的个数(每一次队列里面个数减1)。结束以上步骤,队列里面剩一个节点,弹出作为树的根节点。代码如下:

  1. privatestaticTreebuildTree(Map<Character,Integer>statistics,
  2. List<Node>leafs){
  3. Character[]keys=statistics.keySet().toArray(newCharacter[0]);
  4. PriorityQueue<Node>priorityQueue=newPriorityQueue<Node>();
  5. for(Charactercharacter:keys){
  6. Nodenode=newNode();
  7. node.chars=character.toString();
  8. node.frequence=statistics.get(character);
  9. priorityQueue.add(node);
  10. leafs.add(node);
  11. }
  12. intsize=priorityQueue.size();
  13. for(inti=1;i<=size-1;i++){
  14. Nodenode1=priorityQueue.poll();
  15. Nodenode2=priorityQueue.poll();
  16. NodesumNode=newNode();
  17. sumNode.chars=node1.chars+node2.chars;
  18. sumNode.frequence=node1.frequence+node2.frequence;
  19. sumNode.leftNode=node1;
  20. sumNode.rightNode=node2;
  21. node1.parent=sumNode;
  22. node2.parent=sumNode;
  23. priorityQueue.add(sumNode);
  24. }
  25. Treetree=newTree();
  26. tree.root=priorityQueue.poll();
  27. returntree;
  28. }

编码

某个字符对应的编码为,从该字符所在的叶子节点向上搜索,如果该字符节点是父节点的左节点,编码字符之前加0,反之如果是右节点,加1,直到根节点。只要获取了字符和二进制码之间的mapping关系,编码就非常简单。代码如下:

  1. publicstaticStringencode(StringoriginalStr,
  2. Map<Character,Integer>statistics){
  3. if(originalStr==null||originalStr.equals("")){
  4. return"";
  5. }
  6. char[]charArray=originalStr.toCharArray();
  7. List<Node>leafNodes=newArrayList<Node>();
  8. buildTree(statistics,leafNodes);
  9. Map<Character,String>encodInfo=buildEncodingInfo(leafNodes);
  10. StringBufferbuffer=newStringBuffer();
  11. for(charc:charArray){
  12. Charactercharacter=newCharacter(c);
  13. buffer.append(encodInfo.get(character));
  14. }
  15. returnbuffer.toString();
  16. }
  1. privatestaticMap<Character,String>buildEncodingInfo(List<Node>leafNodes){
  2. Map<Character,String>codewords=newHashMap<Character,String>();
  3. for(NodeleafNode:leafNodes){
  4. Charactercharacter=newCharacter(leafNode.getChars().charAt(0));
  5. Stringcodeword="";
  6. NodecurrentNode=leafNode;
  7. do{
  8. if(currentNode.isLeftChild()){
  9. codeword="0"+codeword;
  10. }else{
  11. codeword="1"+codeword;
  12. }
  13. currentNode=currentNode.parent;
  14. }while(currentNode.parent!=null);
  15. codewords.put(character,codeword);
  16. }
  17. returncodewords;
  18. }

解码

因为Huffman编码算法能够保证任何的二进制码都不会是另外一个码的前缀,解码非常简单,依次取出二进制的每一位,从树根向下搜索,1向右,0向左,到了叶子节点(命中),退回根节点继续重复以上动作。代码如下:

  1. publicstaticStringdecode(StringbinaryStr,
  2. Map<Character,Integer>statistics){
  3. if(binaryStr==null||binaryStr.equals("")){
  4. return"";
  5. }
  6. char[]binaryCharArray=binaryStr.toCharArray();
  7. LinkedList<Character>binaryList=newLinkedList<Character>();
  8. intsize=binaryCharArray.length;
  9. for(inti=0;i<size;i++){
  10. binaryList.addLast(newCharacter(binaryCharArray[i]));
  11. }
  12. List<Node>leafNodes=newArrayList<Node>();
  13. Treetree=buildTree(statistics,leafNodes);
  14. StringBufferbuffer=newStringBuffer();
  15. while(binaryList.size()>0){
  16. Nodenode=tree.root;
  17. do{
  18. Characterc=binaryList.removeFirst();
  19. if(c.charValue()=='0'){
  20. node=node.leftNode;
  21. }else{
  22. node=node.rightNode;
  23. }
  24. }while(!node.isLeaf());
  25. buffer.append(node.chars);
  26. }
  27. returnbuffer.toString();
  28. }

测试以及比较

以下测试Huffman编码的正确性(先编码,后解码,包括中文),以及Huffman编码与常见的字符编码的二进制字符串比较。代码如下:

  1. publicstaticvoidmain(String[]args){
  2. StringoriStr="Huffmancodescompressdataveryeffectively:savingsof20%to90%aretypical,"
  3. +"dependingonthecharacteristicsofthedatabeingcompressed.中华崛起";
  4. Map<Character,Integer>statistics=statistics(oriStr.toCharArray());
  5. StringencodedBinariStr=encode(oriStr,statistics);
  6. StringdecodedStr=decode(encodedBinariStr,statistics);
  7. System.out.println("Originalsstring:"+oriStr);
  8. System.out.println("Huffmanencoedbinarystring:"+encodedBinariStr);
  9. System.out.println("decodedstringfrombinariystring:"+decodedStr);
  10. System.out.println("binarystringofUTF-8:"
  11. +getStringOfByte(oriStr,Charset.forName("UTF-8")));
  12. System.out.println("binarystringofUTF-16:"
  13. +getStringOfByte(oriStr,Charset.forName("UTF-16")));
  14. System.out.println("binarystringofUS-ASCII:"
  15. +getStringOfByte(oriStr,Charset.forName("US-ASCII")));
  16. System.out.println("binarystringofGB2312:"
  17. +getStringOfByte(oriStr,Charset.forName("GB2312")));
  18. }
  19. publicstaticStringgetStringOfByte(Stringstr,Charsetcharset){
  20. if(str==null||str.equals("")){
  21. return"";
  22. }
  23. byte[]byteArray=str.getBytes(charset);
  24. intsize=byteArray.length;
  25. StringBufferbuffer=newStringBuffer();
  26. for(inti=0;i<size;i++){
  27. bytetemp=byteArray[i];
  28. buffer.append(getStringOfByte(temp));
  29. }
  30. returnbuffer.toString();
  31. }
  32. publicstaticStringgetStringOfByte(byteb){
  33. StringBufferbuffer=newStringBuffer();
  34. for(inti=7;i>=0;i--){
  35. bytetemp=(byte)((b>>i)&0x1);
  36. buffer.append(String.valueOf(temp));
  37. }
  38. returnbuffer.toString();
  39. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值