哈夫曼(huffman)树,又称最优树,是一类带权路径长度最短的树,有着广泛的应用
路径:从树中的一个节点到另一个节点之间的分支构成两个节点之间的路径,
路径长度:路径上的分支数目称之为路径长度
假设有n个权值{w1, w2, w3, w4 , …wn},试图构造一棵有n个叶子节点的二叉树,每个叶子节点的权值为wi,则其中带权路径最小的二叉树就叫哈夫曼树
实现用户输入一个字符串,统计该字符串中每个字符出现的频率,按照频率的高低来为字符进行编码
在java中,由于没有指针,对于每一个节点与其父亲节点与孩子节点的联系我们使用存储对应的下标来进行表示
设叶子节点的数量为n,则总的节点的数量就为2 * n - 1,它们中间的差值为n - 1,就是我们要动态生成的节点
创建一个节点类(TreeNode),由于该类对应的是哈夫曼树的一般节点,所以它包含4个属性,节点的权值、父节点位置、左孩子和右孩子的位置
/*
* 节点类,该类包括节点的权值,父节点,以及左右孩子节点
* */
public class TreeNode {
private int weight;
private int parent;
private int leftChild, rightChild;
public TreeNode(int weight, int parent, int leftChild, int rightChild) {
this.weight = weight;
this.parent = parent;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getParent() {
return parent;
}
public void setParent(int parent) {
this.parent = parent;
}
public int getLeftChild() {
return leftChild;
}
public void setLeftChild(int leftChild) {
this.leftChild = leftChild;
}
public int getRightChild() {
return rightChild;
}
public void setRightChild(int rightChild) {
this.rightChild = rightChild;
}
}
叶子节点即是我们需要编码的节点,创建一个叶子节点类(CodeNode)它有两个属性,一个是字符,一个是对应的二进制编码
/*
* 编码节点类,该类表示叶子节点的编码,有字符和对应的编码这两个属性
* */
public class CodeNode {
private String character;
private String code;
public CodeNode(String character, String code) {
this.character = character;
this.code = code;
}
public String getCharacter() {
return character;
}
public void setCharacter(String character) {
this.character = character;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
每次我们需要从剩余的可以构建哈夫曼树的节点中寻找两个权值最小的节点构建成一个新的节点,那么我们就需要了解还有多少个节点是可用的,使用这个临时节点类从所有的节点中选择出还有哪些节点在当前情况下是可用的
比如说两个节点构建成了一个新的节点,那么这个新的节点的权值就是这两个节点的权值之和,之后参与构建哈夫曼树的就是这个新的节点了,构建它的子节点就不在选择范围之中了
/*
* 临时节点类,该类表示没有父节点的节点,用于从剩余的节点中选取权值最低的两个节点
* 包含节点的权值以及对应的真实节点的位置
* */
public class TempNode {
private int weight;
private int nodePosition;
public TempNode(int weight, int nodePosition) {
this.weight = weight;
this.nodePosition = nodePosition;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getNodePosition() {
return nodePosition;
}
public void setNodePosition(int nodePosition) {
this.nodePosition = nodePosition;
}
}
Scanner inputScanner = new Scanner(System.in);
//将读取到的字符串转变为一个字符数组的形式,以便取出每一个字符出现的频度
char[] chars = inputScanner.nextLine().toCharArray();
//使用键值对的形式保存每一个字符以及它们出现的频度
Map< Character, Integer> mapData = new LinkedHashMap();
//将字符即字符出现的频度保存到键值对容器中
for (char c : chars) {
//如果容器中不存在当前字符,则将其频度设置为1
if (!mapData.containsKey(c)) {
mapData.put(c, 1);
}
//如果容器中存在当前字符,则将其频度加1
else {
mapData.put(c, mapData.get(c) + 1);
}
}
第一步将读取的字符串转变为一个字符数组,通过遍历将数据存储到LinkedHashMap这个键值对容器中,键是字符,值是该字符出现的频度,当字符第一次出现时,设置频度为1,当容器中已有该字符,并且又读取到时,设置其频度加1,这样用户输入的字符串中每个字符及其频度我们就统计到了
//叶子节点的数量等于需要编码的字符的数量
int leafNodeNumber = mapData.size();
//总的节点的数量等与2倍叶子节点的数量减1
int totalNodeNumber = 2 * leafNodeNumber - 1;
//进行哈夫曼树的初始化操作
TreeNode[] treeNodes = new TreeNode[totalNodeNumber];
for (int i = 0; i < totalNodeNumber; i++) {
treeNodes[i] = new TreeNode(0, -1, -1, -1);
}
//创建并初始化所有需要编码的叶子节点
CodeNode[] codeNodes = new CodeNode[leafNodeNumber];
for (int i = 0; i < leafNodeNumber; i++) {
treeNodes[i].setWeight(valueList.get(i));
codeNodes[i] = new CodeNode(keyList.get(i).toString(), "");
}
3、创建函数用于从当前可用的节点中选出权值最小节点与权值次小节点,我们只需要知道这两个节点的真实下标即可
/* 从当前的节点中选取两个权值最低的节点的下标位置进行返回,这些节点都是没有父节点的节点 */
static int[] selectTwoMinCode(TreeNode[] treeNodes, int nodeNumber) {
//创建数组保存这两个最小权值对应的节点的下标
int[] twoMin = new int[2];
//创建一个节点数组来保存没有父节点的节点,然后我们再在其中进行遍历
TempNode[] tempNodes = new TempNode[nodeNumber];
//第一步求出没有父节点的节点的数量,然后将它们初始化为临时节点对象
int remindNodesNumber = 0;
for (int i = 0; i < nodeNumber; i++) {
if (treeNodes[i].getParent() == -1 && treeNodes[i].getWeight() != 0) {
tempNodes[remindNodesNumber] = new TempNode(treeNodes[i].getWeight(), i);
remindNodesNumber++;
}
}
//开始时我们可以默认第一个没有父节点的节点的权值最小
int min1 = 0,min2 = 0;
//取得第一个最小值
for (int i = 0; i < remindNodesNumber; i++) {
if (tempNodes[i].getWeight() < tempNodes[min1].getWeight()) {
min1 = i;
}
}
for (int i = 0; i < remindNodesNumber; i++) {
//如果min2等于min1,则将min2其后移一位,相当于是从去掉min1的剩余数据中选取一个最小值
if (min1 == min2) {
min2++;
}
if (tempNodes[i].getWeight() <= tempNodes[min2].getWeight() && i != min1) {
min2 = i;
}
}
//得到两个权值最小节点的真实下标并进行返回
twoMin[0] = tempNodes[min1].getNodePosition();
twoMin[1] = tempNodes[min2].getNodePosition();
return twoMin;
}
4、创建哈夫曼数操作
/* 创建哈夫曼树 */
static void createHuffmanTree(TreeNode[] treeNodes, int leafNodeNumber) {
if (leafNodeNumber <= 1) {
System.out.print("No need to code");
System.exit(-1);
}
//总的节点的数量
int totalNodeNumber = 2 * leafNodeNumber - 1;
//叶子节点与所有节点之间的差值节点在这里生成
for (int i = leafNodeNumber; i < totalNodeNumber; i++) {
//首先在当前的节点中选取权值最小的两个节点
int[] twoMin = selectTwoMinCode(treeNodes, i);
int min1 = twoMin[0];
int min2 = twoMin[1];
//设置两个最小权值节点的父节点
treeNodes[min1].setParent(i);
treeNodes[min2].setParent(i);
//设置该父节点的左孩子和右孩子
treeNodes[i].setLeftChild(min1);
treeNodes[i].setRightChild(min2);
//当前节点的权值是两个孩子节点的权值之和
treeNodes[i].setWeight(treeNodes[min1].getWeight() + treeNodes[min2].getWeight());
}
}
5、进行编码操作
/* 进行哈夫曼树的编码,从叶子节点到根节点进行编码 */
static void codeHuffmanTree(TreeNode[] treeNodes, CodeNode[] nodeCodes, int leafNodeNumber) {
//使用一个字符数组保存字符编码后的结果
char[] code = new char[100];
//使用c来表示点当前节点的下标
int c;
int parent;
//start记录编码的位数,以及作为编码时编码数组的下标
int start;
//进行编码操作
for (int i = 0; i < leafNodeNumber; i++) {
StringBuffer stringBuffer = new StringBuffer();
//当有n个节点进行编码时,哈夫曼树的编码的数量不会超过 n - 1个
start = leafNodeNumber - 1;
//使用c表示当前节点
c = i;
//当当前节点存在父节点时,如果当前节点是左孩子则编码为0,如果是右孩子则编码为1
while( (parent = treeNodes[c].getParent()) >= 0){
start--;
code[start]=((treeNodes[parent].getLeftChild()== c)?'0':'1');
c = parent;
}
for(;start < leafNodeNumber - 1; start++){
stringBuffer.append(code[start]);
}
nodeCodes[i].setCode(stringBuffer.toString());
}
}
6、进行 结果的友好界面输出
static void outputCodeResult(CodeNode[] nodeCodes,TreeNode[] treeNodes, int leafCodeNumber,char[] chars, List listMapKey, List listMapValue) {
Map codeMapData = new LinkedHashMap();
for(int i = 0;i < leafCodeNumber;i++){
codeMapData.put(nodeCodes[i].getCharacter(),nodeCodes[i].getCode());
}
System.out.println("\n编码结果如下所示:");
System.out.println("data\tocc\ttotal\tprobablity\tcode");
for (int i = 0; i < leafCodeNumber; i++) {
System.out.print(listMapKey.get(i) + "\t");
System.out.print(listMapValue.get(i)+ "\t");
System.out.print(chars.length+ "\t");
double possibility = (Integer.parseInt(listMapValue.get(i).toString())*1.0 /chars.length) * 100;
System.out.printf("%.3f%%\t\t",possibility);
System.out.print(codeMapData.get(listMapKey.get(i) + ""));
System.out.println();
}
System.out.println();
System.out.print("编码后的数据是:");
String temp = "";
for (char c : chars) {
temp += codeMapData.get(c + "");
System.out.print(codeMapData.get(c + ""));
}
System.out.println("\n原字符串的长度是" + chars.length + "位," + "占用空间为" + chars.length + "个字节" );
System.out.println("编码后的长度是" + temp.length() + "占用空间为" + (int)Math.ceil(temp.length() / 8.0)+ "个字节");
double compressibility = 100 * ((Math.ceil(temp.length() / 8.0)) /chars.length);
System.out.printf("压缩率为%.3f%%\n",compressibility);
System.out.println("-------进行解码操作----------,根据码表以及编码后的结果进行解码");
System.out.print("解码的对应结果是:\n");
char[] codeChar = temp.toCharArray();
int codeIndex = 0;
String character = "";
StringBuffer decodeBuffer = new StringBuffer();
while (codeIndex < codeChar.length){
//临时记录下编码
StringBuffer stringBuffer = new StringBuffer();
//构建成功的哈夫曼树的根节点
TreeNode treeNode = treeNodes[2 * leafCodeNumber - 2];
//当前节点若有子孩子,则说明它不是叶子节点
while (treeNode.getLeftChild() != -1 && treeNode.getRightChild() != -1) {
//当为0时探索左孩子
if (codeChar[codeIndex] == '0') {
treeNode = treeNodes[treeNode.getLeftChild()];
stringBuffer.append("0");
}
//当为1时探索右孩子
else if (codeChar[codeIndex] == '1')
{
treeNode = treeNodes[treeNode.getRightChild()];
stringBuffer.append("1");
}
codeIndex++;
}
for (String key : codeMapData.keySet()) {
if (codeMapData.get(key).equals(stringBuffer.toString())) {
character = key;
decodeBuffer.append(key);
break;
}
}
System.out.println(stringBuffer + "--->" + character);
}
System.out.println("最终解码的结果是:" + decodeBuffer);
}
判断是否为叶子节点的方法:当节点没有左孩子或右孩子时,表示当前的节点为叶子节点,这里我们为-1值的判定就是表示其没有孩子节点
在进行解码的时候,我们从根节点出发,依次读取编码后的二进制字符串,当到达叶子节点时,将该编码带入码表中,得出对应的字符
这里我们输入一个字符串 Hello World!!!,控制台输出的编码结果如图1-1所示,输出的解码结果如图1-2所示
图1-1 编码结果
图1-2 解码效果
贴上Huffman类,将该类与上面的3个节点类置于同一包下可直接运行
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
public class Huffman {
public static void main(String[] args) {
launch();
}
static void launch() {
System.out.print("请输入字符串,我们将为其进行编码:");
Scanner inputScanner = new Scanner(System.in);
//将读取到的字符串转变为一个字符数组的形式,以便取出每一个字符出现的频度
char[] chars = inputScanner.nextLine().toCharArray();
//使用键值对的形式保存每一个字符以及它们出现的频度
Map< Character, Integer> mapData = new LinkedHashMap();
//将字符即字符出现的频度保存到键值对容器中
for (char c : chars) {
//如果容器中不存在当前字符,则将其频度设置为1
if (!mapData.containsKey(c)) {
mapData.put(c, 1);
}
//如果容器中存在当前字符,则将其频度加1
else {
mapData.put(c, mapData.get(c) + 1);
}
}
//由于map无法直接取键值,使用Link容器来保存字符和它们出现的频度
List keyList = new ArrayList();
for (Character key: mapData.keySet()) {
keyList.add(key);
}
//保存值
List valueList = new ArrayList();
for (int value : mapData.values()) {
valueList.add(value);
}
//叶子节点的数量等于需要编码的字符的数量
int leafNodeNumber = mapData.size();
//总的节点的数量等与2倍叶子节点的数量减1
int totalNodeNumber = 2 * leafNodeNumber - 1;
//进行哈夫曼树的初始化操作
TreeNode[] treeNodes = new TreeNode[totalNodeNumber];
for (int i = 0; i < totalNodeNumber; i++) {
treeNodes[i] = new TreeNode(0, -1, -1, -1);
}
//创建并初始化所有需要编码的叶子节点
CodeNode[] codeNodes = new CodeNode[leafNodeNumber];
for (int i = 0; i < leafNodeNumber; i++) {
treeNodes[i].setWeight(valueList.get(i));
codeNodes[i] = new CodeNode(keyList.get(i).toString(), "");
}
/* 开始创建哈夫曼树 */
createHuffmanTree(treeNodes, leafNodeNumber);
/* 在创建完哈夫曼树之后进行编码操作 */
codeHuffmanTree(treeNodes, codeNodes,leafNodeNumber);
/* 进行结果的输出 */
outputCodeResult(codeNodes,treeNodes, leafNodeNumber,chars, keyList,valueList);
}
/* 从当前的节点中选取两个权值最低的节点的下标位置进行返回,这些节点都是没有父节点的节点 */
static int[] selectTwoMinCode(TreeNode[] treeNodes, int nodeNumber) {
//创建数组保存这两个最小权值对应的节点的下标
int[] twoMin = new int[2];
//创建一个节点数组来保存没有父节点的节点,然后我们再在其中进行遍历
TempNode[] tempNodes = new TempNode[nodeNumber];
//第一步求出没有父节点的节点的数量,然后将它们初始化为临时节点对象
int remindNodesNumber = 0;
for (int i = 0; i < nodeNumber; i++) {
if (treeNodes[i].getParent() == -1 && treeNodes[i].getWeight() != 0) {
tempNodes[remindNodesNumber] = new TempNode(treeNodes[i].getWeight(), i);
remindNodesNumber++;
}
}
//开始时我们可以默认第一个没有父节点的节点的权值最小
int min1 = 0,min2 = 0;
//取得第一个最小值
for (int i = 0; i < remindNodesNumber; i++) {
if (tempNodes[i].getWeight() < tempNodes[min1].getWeight()) {
min1 = i;
}
}
for (int i = 0; i < remindNodesNumber; i++) {
//如果min2等于min1,则将min2其后移一位,相当于是从去掉min1的剩余数据中选取一个最小值
if (min1 == min2) {
min2++;
}
if (tempNodes[i].getWeight() <= tempNodes[min2].getWeight() && i != min1) {
min2 = i;
}
}
//得到两个权值最小节点的真实下标并进行返回
twoMin[0] = tempNodes[min1].getNodePosition();
twoMin[1] = tempNodes[min2].getNodePosition();
return twoMin;
}
/* 创建哈夫曼树 */
static void createHuffmanTree(TreeNode[] treeNodes, int leafNodeNumber) {
if (leafNodeNumber <= 1) {
System.out.print("No need to code");
System.exit(-1);
}
//总的节点的数量
int totalNodeNumber = 2 * leafNodeNumber - 1;
//叶子节点与所有节点之间的差值节点在这里生成
for (int i = leafNodeNumber; i < totalNodeNumber; i++) {
//首先在当前的节点中选取权值最小的两个节点
int[] twoMin = selectTwoMinCode(treeNodes, i);
int min1 = twoMin[0];
int min2 = twoMin[1];
//设置两个最小权值节点的父节点
treeNodes[min1].setParent(i);
treeNodes[min2].setParent(i);
//设置该父节点的左孩子和右孩子
treeNodes[i].setLeftChild(min1);
treeNodes[i].setRightChild(min2);
//当前节点的权值是两个孩子节点的权值之和
treeNodes[i].setWeight(treeNodes[min1].getWeight() + treeNodes[min2].getWeight());
}
}
/* 进行哈夫曼树的编码,从叶子节点到根节点进行编码 */
static void codeHuffmanTree(TreeNode[] treeNodes, CodeNode[] nodeCodes, int leafNodeNumber) {
//使用一个字符数组保存字符编码后的结果
char[] code = new char[100];
//使用c来表示点当前节点的下标
int c;
int parent;
//start记录编码的位数,以及作为编码时编码数组的下标
int start;
//进行编码操作
for (int i = 0; i < leafNodeNumber; i++) {
StringBuffer stringBuffer = new StringBuffer();
//当有n个节点进行编码时,哈夫曼树的编码的数量不会超过 n - 1个
start = leafNodeNumber - 1;
//使用c表示当前节点
c = i;
//当当前节点存在父节点时,如果当前节点是左孩子则编码为0,如果是右孩子则编码为1
while( (parent = treeNodes[c].getParent()) >= 0){
start--;
code[start]=((treeNodes[parent].getLeftChild()== c)?'0':'1');
c = parent;
}
for(;start < leafNodeNumber - 1; start++){
stringBuffer.append(code[start]);
}
nodeCodes[i].setCode(stringBuffer.toString());
}
}
static void outputCodeResult(CodeNode[] nodeCodes,TreeNode[] treeNodes, int leafCodeNumber,char[] chars, List listMapKey, List listMapValue) {
Map codeMapData = new LinkedHashMap();
for(int i = 0;i < leafCodeNumber;i++){
codeMapData.put(nodeCodes[i].getCharacter(),nodeCodes[i].getCode());
}
System.out.println("\n编码结果如下所示:");
System.out.println("data\tocc\ttotal\tprobablity\tcode");
for (int i = 0; i < leafCodeNumber; i++) {
System.out.print(listMapKey.get(i) + "\t");
System.out.print(listMapValue.get(i)+ "\t");
System.out.print(chars.length+ "\t");
double possibility = (Integer.parseInt(listMapValue.get(i).toString())*1.0 /chars.length) * 100;
System.out.printf("%.3f%%\t\t",possibility);
System.out.print(codeMapData.get(listMapKey.get(i) + ""));
System.out.println();
}
System.out.println();
System.out.print("编码后的数据是:");
String temp = "";
for (char c : chars) {
temp += codeMapData.get(c + "");
System.out.print(codeMapData.get(c + ""));
}
System.out.println("\n原字符串的长度是" + chars.length + "位," + "占用空间为" + chars.length + "个字节" );
System.out.println("编码后的长度是" + temp.length() + "占用空间为" + (int)Math.ceil(temp.length() / 8.0)+ "个字节");
double compressibility = 100 * ((Math.ceil(temp.length() / 8.0)) /chars.length);
System.out.printf("压缩率为%.3f%%\n",compressibility);
System.out.println("-------进行解码操作----------,根据码表以及编码后的结果进行解码");
System.out.print("解码的对应结果是:\n");
char[] codeChar = temp.toCharArray();
int codeIndex = 0;
String character = "";
StringBuffer decodeBuffer = new StringBuffer();
while (codeIndex < codeChar.length){
//临时记录下编码
StringBuffer stringBuffer = new StringBuffer();
//构建成功的哈夫曼树的根节点
TreeNode treeNode = treeNodes[2 * leafCodeNumber - 2];
//当前节点若有子孩子,则说明它不是叶子节点
while (treeNode.getLeftChild() != -1 && treeNode.getRightChild() != -1) {
//当为0时探索左孩子
if (codeChar[codeIndex] == '0') {
treeNode = treeNodes[treeNode.getLeftChild()];
stringBuffer.append("0");
}
//当为1时探索右孩子
else if (codeChar[codeIndex] == '1')
{
treeNode = treeNodes[treeNode.getRightChild()];
stringBuffer.append("1");
}
codeIndex++;
}
for (String key : codeMapData.keySet()) {
if (codeMapData.get(key).equals(stringBuffer.toString())) {
character = key;
decodeBuffer.append(key);
break;
}
}
System.out.println(stringBuffer + "--->" + character);
}
System.out.println("最终解码的结果是:" + decodeBuffer);
/*
String tempCode = "";
String decodeString = "";
for (int i = 0; i < codeChar.length; i++) {
tempCode += codeChar[i];
if (codeMapData.values().contains(tempCode)) {
for (String key : codeMapData.keySet()) {
if (codeMapData.get(key).equals(tempCode)) {
System.out.println(codeMapData.get(key) + "--->" + key);
decodeString += key;
tempCode = "";
}
}
}
}
*/
// System.out.println("最终解码的结果是:" + decodeString);
}
}