您的位置:首页 > 理论基础 > 数据结构算法

数据结构之霍夫曼树

2017-06-07 17:27 183 查看
计算机里每个字符在没有压缩的文本文件中都由一个字节(如ASCII码)或两个字节(如Unicode码)表示。这些方案中,每个字符需要相同的位数

下表列出了字母对应的ASCII码

字母 十进制 二进制

A 65 01000001

B 66 01000010

C 67 01000011

……

X 88 01011000

Y 89 01011001

Z 90 01011010

在英文中,字母E的使用频率最高,而字母Z的使用频率最低,但是,无论使用频率高低,我们一律使用相同位数的编码来存储,是不是有些浪费空间呢?试想,如果我们对使用频率高的字母用较少的位数来存储,而对使用频率低的字母用较多的位数来存储,会大大提升存储效率

霍夫曼编码就是根据以上的设想来处理数据压缩的

例如,我们要发送一句消息:

SUSIE SAYS IT IS EASY

统计所有符号出现的频率:

换行符 1次

T 1次

U 1次

A 2次

E 2次

Y 2次

I 3次

空格 4次

S 6次

S出现的频率最多,我们可以将它的编码设为10,其次是空格,我们将它设置为00,接下来的编码必须增加位数了。三位码我们有八种选择:000,001,010,011,100,101,110,111。但是,以10或00开头的是不能使用的,只能从010,011,110,111中选择。因为如果我们用101表示I,用010表示Y,101010既可以分解为101,010两个有意义的编码,也能分解为10,10,10三个有意义的编码,这显然是不允许的。我们必须保证,任何信息编码之后,解码时都不会出现歧义。借助霍夫曼树就能便捷的实现编码

霍夫曼树是最优二叉树。树的带权路径长度规定为所有叶子结点的带权路径长度之和,带权路径长度最短的树,即为最优二叉树。在最优二叉树中,权值较大的结点离根较近。

首先就需要构建一个霍夫曼树,一般利用优先级队列来构建霍夫曼树

以上面的消息为例,我们先来分析一下构建霍夫曼树的过程,下图中,if代表换行符,sp代表空格



首先,将字符按照频率插入一个优先级队列,频率越低越靠近队头,然后循环执行下面的操作:

1、取出队头的两个树

2、以它们为左右子节点构建一棵新树,新树的权值是两者之和

3、将这棵新树插入队列

直到队列中只有一棵树时,这棵树就是我们需要的霍夫曼树

接下来我们用Java来实现上面的过程,代码如下:

[java] view
plain copy

print?

//节点类

public class Node{

private String key; //树节点存储的关键字,如果是非叶子节点为空

private int frequency; //关键字词频

private Node left; //左子节点

private Node right; //右子节点

private Node next; //优先级队列中指向下一个节点的引用

public Node(int fre,String str){ //构造方法1

frequency = fre;

key = str;

}

public Node(int fre){ //构造方法2

frequency = fre;

}

public String getKey() {

return key;

}

public void setKey(String key) {

this.key = key;

}

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;

}

public Node getNext() {

return next;

}

public void setNext(Node next) {

this.next = next;

}

public int getFrequency() {

return frequency;

}

public void setFrequency(int frequency) {

this.frequency = frequency;

}

}

[java] view
plain copy

print?

//用于辅助创建霍夫曼树的优先级队列

public class PriorityQueue{

private Node first;

private int length;

public PriorityQueue(){

length = 0;

first = null;

}

//插入节点

public void insert(Node node){

if(first == null){ //队列为空

first = node;

}else{

Node cur = first;

Node previous = null;

while(cur.getFrequency()< node.getFrequency()){ //定位要插入位置的前一个节点和后一个节点

previous = cur;

if(cur.getNext() ==null){ //已到达队尾

cur = null;

break;

}else{

cur =cur.getNext();

}

}

if(previous == null){ //要插入第一个节点之前

node.setNext(first);

first = node;

}else if(cur == null){ //要插入最后一个节点之后

previous.setNext(node);

}else{ //插入到两个节点之间

previous.setNext(node);

node.setNext(cur);

}

}

length++;

}

//删除队头元素

public Node delete(){

Node temp = first;

first = first.getNext();

length--;

return temp;

}

//获取队列长度

public int getLength(){

return length;

}

//按顺序打印队列

public void display(){

Node cur = first;

System.out.print("优先级队列:\t");

while(cur != null){

System.out.print(cur.getKey()+":"+cur.getFrequency()+"\t");

cur = cur.getNext();

}

System.out.println();

}

//构造霍夫曼树

public HuffmanTree buildHuffmanTree(){

while(length > 1){

Node hLeft = delete(); //取出队列的第一个节点作为新节点的左子节点

Node hRight = delete(); //取出队列的第二个节点作为新节点的右子节点

//新节点的权值等于左右子节点的权值之和

Node hRoot = new Node(hLeft.getFrequency()+hRight.getFrequency());

hRoot.setLeft(hLeft);

hRoot.setRight(hRight);

insert(hRoot);

}

//最后队列中只剩一个节点,即为霍夫曼树的根节点

return new HuffmanTree(first);

}

}

[java] view
plain copy

print?

import java.util.HashMap;

import java.util.Map;

//霍夫曼树类

public class HuffmanTree {

private Node root;

private Map codeSet = new HashMap(); //该霍夫曼树对应的字符编码集

public HuffmanTree(Node root){

this.root = root;

buildCodeSet(root,""); //初始化编码集

}

//生成编码集的私有方法,运用了迭代的思想

//参数currentNode表示当前节点,参数currentCode代表当前节点对应的代码

private void buildCodeSet(NodecurrentNode,String currentCode){

if(currentNode.getKey() != null){

//霍夫曼树中,如果当前节点包含关键字,则该节点肯定是叶子节点,将该关键字和代码放入代码集

codeSet.put(currentNode.getKey(),currentCode);

}else{//如果不是叶子节点,必定同时包含左右子节点,这种节点没有对应关键字

//转向左子节点需要将当前代码追加0

buildCodeSet(currentNode.getLeft(),currentCode+"0");

//转向右子节点需要将当前代码追加1

buildCodeSet(currentNode.getRight(),currentCode+"1");

}

}

//获取编码集

public Map getCodeSet(){

return codeSet;

}

}

[java] view
plain copy

print?

//测试类

public static void main(String[] args) throws Exception{

PriorityQueue queue = new PriorityQueue();

Node n1 = new Node(1,"if");

Node n2 = new Node(1,"U");

Node n3 = new Node(1,"T");

Node n4 = new Node(2,"Y");

Node n5 = new Node(2,"E");

Node n6 = new Node(2,"A");

Node n7 = new Node(3,"I");

Node n8 = new Node(4,"sp");

Node n9 = new Node(5,"S");

queue.insert(n3);

queue.insert(n2);

queue.insert(n1);

queue.insert(n6);

queue.insert(n5);

queue.insert(n4);

queue.insert(n7);

queue.insert(n8);

queue.insert(n9);

queue.display();

HuffmanTree tree =queue.buildHuffmanTree();

Map map = tree.getCodeSet();

Iterator it =map.entrySet().iterator();

System.out.println("霍夫曼编码结果:");

while(it.hasNext()){

Entry<String,String>entry = (Entry)it.next();

System.out.println(entry.getKey()+"——>"+entry.getValue());

}

}

控制台打印结果如下图所示:



可见,达到了预期的效果

秉承没有最好,只有更好的精神,我们不应该就此止步。现在我们做到的只是得到某个字符对应的霍夫曼编码,但是这个字符的词频仍然需要我们手工去统计、输入,更深入的思考一下,能不能更智能化一点,只要我们输入完整的一段消息,就能给出对应的霍夫曼编码,而且能对编码进行解码呢?

显然是可以的,先面就让我们用神奇的java语言来对上面的底层操作进行更高级的封装,来达到预期的功能

就是说,我们要利用上面已经写出的代码来封装一个编码类,这个类的一个方法接受一个字符串消息,返回霍夫曼编码,此外,还有一个解码类,接受一段完整的霍夫曼编码,返回解码后的消息内容。这实际上就是压缩与解压的模拟过程

[java] view
plain copy

print?

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

//霍夫曼编码器

public class HuffmanEncoder {

private PriorityQueue queue; //辅助建立霍夫曼树的优先级队列

private HuffmanTree tree; //霍夫曼树

private String [] message; //以数组的形式存储消息文本

private Map keyMap; //存储字符以及词频的对应关系

private Map codeSet; //存储字符以及代码的对应关系

public HuffmanEncoder(){

queue = new PriorityQueue();

keyMap = new HashMap<String,Integer>();

}

//获取指定字符串的霍夫曼编码

public String encode(String msg){

resolveMassage(msg);

buildCodeSet();

String code = "";

for(inti=0;i<message.length;i++){//将消息文本的逐个字符翻译成霍夫曼编码

code =code+codeSet.get(message[i]);

}

return code;

}

//将一段字符串消息解析成单个字符与该字符词频的对应关系,存入Map

private void resolveMassage(String msg){

char [] chars =msg.toCharArray(); //将消息转换成字符数组

message = new String[chars.length];

for(int i =0;i<chars.length;i++){

String key = "";

key =chars[i]+""; //将当前字符转换成字符串

message [i] = key;

if(keyMap.containsKey(key)){//如果Map中已存在该字符,则词频加一

keyMap.put(key,(Integer)keyMap.get(key)+1);

}else{//如果Map中没有该字符,加入Map

keyMap.put(key,1);

}

}

}

//建立对应某段消息的代码集

private void buildCodeSet(){

Iterator it =keyMap.entrySet().iterator();

while(it.hasNext()){

Entry entry =(Entry)it.next();

//用该字符和该字符的词频为参数,建立一个新的节点,插入优先级队列

queue.insert(new Node((Integer)entry.getValue(),(String)entry.getKey()));

}

queue.display();

tree =queue.buildHuffmanTree(); //利用优先级队列生成霍夫曼树

codeSet = tree.getCodeSet(); //获取霍夫曼树对应的代码集

}

//打印该段消息的代码集

public void printCodeSet(){

Iterator it =codeSet.entrySet().iterator();

System.out.println("代码集:");

while(it.hasNext()){

Entry entry =(Entry)it.next();

System.out.println(entry.getKey()+"——>"+entry.getValue());

}

System.out.println();

}

//获取该段消息的代码集

public Map getCodeSet(){

return codeSet;

}

}

[java] view
plain copy

print?

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

//霍夫曼解码器

public class HuffmanDecoder {

private Map codeSet; //代码段对应的代码集

public HuffmanDecoder(Map map){

codeSet = map;

}

//将代码段解析成消息文本

public String decode(String code){

String message = "";

String key = "";

char [] chars = code.toCharArray();

for(int i=0;i<chars.length;i++){

key += chars[i];

if(codeSet.containsValue(key)){ //代码集中存在该段代码

Iterator it =codeSet.entrySet().iterator();

while(it.hasNext()){

Entry entry = (Entry)it.next();

if(entry.getValue().equals(key)){

message+= entry.getKey(); //获取该段代码对应的键值,即消息字符

}

}

key =""; //代码段变量置为0

}else{

continue; //该段代码不能解析为文本消息,继续循环

}

}

return message;

}

}

[java] view
plain copy

print?

//测试类

public static void main(String[] args){

String message = "chen long fei is hero !";

HuffmanEncoder encoder = new HuffmanEncoder();

String code =encoder.encode(message);

encoder.printCodeSet();

System.out.print("编码结果:");

System.out.println(code);

HuffmanDecoder decoder = new HuffmanDecoder(encoder.getCodeSet());

String message2 =decoder.decode(code);

System.out.print("解码结果:");

System.out.println(message);

}

运行结果如下图所示:



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: