哈弗曼压缩的原理在这就不多说了,相信大家都了解(不了解的看有关资料),在这只讨论一下哈弗曼压缩步骤:
哈夫曼压缩:
1.读文件,统计文件中每个字节出现的次数,并用一个数组来保存
2.把数组中的每一个下标作为节点的元素(即为读取到的字节),下标对应的元素作为节点元素出现的次数, 构造节点优先队列
3.根据节点优先队列构建哈树
4.得到每个叶节点(即为字节)的编码
5.读文件,得到有序字符串
6.将有序字符串保存到字节数组中
7.将字节数组中的字节写入到文件当中
压缩:例如,获得了文件中的所有字节的字符串
1.如文件中:abbcccddddeeeee 假设文件是a.txt;
2.读文件,将读到的字节保存到一个int[]数组中
3.创建一个优先级队列将数组中的元素进行排序。
4.获得哈夫曼树的编码为:(且将编码 存到一个字符串数组当中即strs[256])
a="010";--------strs[97]="010";
b="011";--------strs[98]="011";
c="00";---------strs[99]="00";
d="10";---------strs[100]="10";
e="11";---------strs[101]="11";
5.再次读文件得到文件中所有字节的有序编码字符串
allString="010 011011 000000 10101010 111111111111";
6.将allString字符串每8位每8位保存到一个字节数组byte[] 当中
如: byte[0]=77;
byte[1]=-127;
byte[2]=85;
byte[3]=-1;
byte[4]=-128;
byte[5]=7;
7.写文件,把字节数组写入到文件当中 假设写入的文件是b.txt;
----------就完成压缩了!!!
以下是压缩代码:
public class YaSuo {
/**
* 步骤一: 读取文件,统计文件中每个字节出现的次数,将字节出现的次数用一个int[] num = new int[256]保存
* 数组的下标表示字节,下标对应的元素表示该字节出现的次数
*
* @param path
* 要读取的文件路径
* @return 返回用来存储字节次数的数组
*/
public int[] readFile(String path) {
// 创建一个数组用来保存读到的字节 下标i 0-255是所有的字节 t表示每个字节出现的频率
int[] num = new int[256];
try {
// 创建文件输入流用来读文件
FileInputStream fis = new FileInputStream(path);// 此处抛出异常
// 把输入流包装成缓冲流
BufferedInputStream bis = new BufferedInputStream(fis);
// 用缓冲流读字节,并把读到的字节保存到数组当中以便获得每个字节的频率
while (bis.available() > 0) {// 当剩余字节不为0时
int t = bis.read();// 缓冲流读到不同的字节就会返回不同的整型即0-255
num[t]++;// 当读到相同的字节时频率就加1
// 將獨到的字節設置成節點元素
HfmNode node = new HfmNode(t, num[t]);
// System.out.println("節點元素:"+(Integer)node.obj);
}
} catch (Exception ef) {
ef.printStackTrace();
}
return num;
}
/**
* 步骤二: 当字节数组中元素不为0的时候,就创建一个树的节点对象, 将节点对象使用一个优先对象存储,来保证节点按照字节出现的次数排序
*
* @param arr
* 用来保存字节出现次数的数组
* @return 返回保存节点的优先队列
*/
public PriorityQueue<HfmNode> array2Queue(int[] arr) {
// 创建排序队列,自己制定比较器
PriorityQueue<HfmNode> nodeList = new PriorityQueue<HfmNode>(11,
new MyComparator());
for (int i = 0; i < arr.length; i++) {
if (arr[i] != 0) {// 如果出现的次数不是0,则创建节点
// 创建节点对象
HfmNode node = new HfmNode(i, arr[i]);
nodeList.add(node);
}
}
return nodeList;
}
// 实现比较器类
class MyComparator implements Comparator<HfmNode> {// 按频率的大小进行比较
public int compare(HfmNode h1, HfmNode h2) {
return h1.getCount() - h2.getCount();
}
}
/**
* 将优先级队列中的节点创建成哈夫曼树
*
* @param nodeList
* 优先级队列
* @return 返回根节点
*/
public HfmNode createHfmTree(PriorityQueue<HfmNode> nodeList) {
while (nodeList.size() > 1) {// 当优先级队列当中至少有两个节点时
// 取出两个最小的节点
HfmNode node1 = nodeList.poll();
HfmNode node2 = nodeList.poll();
// 有最小的节点创建合并的节点
HfmNode addNode = new HfmNode(null, node1.getCount()
+ node2.getCount());
// 创建引用关系
addNode.setLeft(node1);
node1.setString("0");// 左节点编码为0
addNode.setRight(node2);
node2.setString("1");// 右节点编码为1
node1.setLast(addNode);
node2.setLast(addNode);
// 将合并的节点加到优先队列当中
nodeList.add(addNode);
}
HfmNode root = nodeList.peek();// 取出根结点
return root;
}
/**
* 得到每个叶节点即字节的哈夫曼编码
*
* 1.先得到每个叶节点的编码,将其保存在字符串数组中 如:String[] str=new String[256] 如str[97]="00011"
* str[98]="011111"
*
*
*
*/
public String[] createByteCode(HfmNode root) {
// 用一个字符串数组保存所有字符串编码如strs[97]="00011" str[98]="011111"
// 如果定義為屬性的話,創建類對象時數組也會創建,浪費內存,所以把其放在方法中
String[] strs = new String[256];
Byte2String(strs, root);
return strs;
}
private void Byte2String(String[] strs, HfmNode root) {
if (root != null) {// 如果根節點不為空
if (root.getLeft() != null) {// 如果左節點不為空,得到其編碼
HfmNode leftNode = root.getLeft();// 得到左節點
String str1 = root.getString() + leftNode.getString();// 左節點的編碼是父節點加自身節點
leftNode.setString(str1);// 重新設置左節點編碼
// System.out.println("獨到的字節"+(Integer) leftNode.getObject());
// 將左節點的字節和編碼保存到字符串數組當中
// 注意:是把葉節點的元素加進去,而不是所有創建樹的所有節點
if (leftNode.getObject() != null) {
// System.out.println("葉節點元素:"+(Integer)leftNode.getObject()+"葉節點字符串編碼:"
// +leftNode.getString());
strs[(Integer) leftNode.getObject()] = leftNode.getString();
}
// 遞歸調用
Byte2String(strs, leftNode);
}
if (root.getRight() != null) {// 右節點不為空
HfmNode rightNode = root.getRight();// 得到右節點
// System.out.println("右節點的編碼:"+rightNode.getString());
// 重新設置右節點編碼,即其編碼為父節點編碼加自身編碼
String str2 = root.getString() + rightNode.getString();
rightNode.setString(str2);
// 將右節點的字節和編碼保存到字符串中
if (rightNode.getObject() != null) {
// System.out.println("葉節點元素:"+(Integer)rightNode.getObject()+"葉節點字符串編碼:"
// +rightNode.getString());
strs[(Integer) rightNode.getObject()] = rightNode
.getString();
}
// 遞歸調用
Byte2String(strs, rightNode);
}
}
}
/**
* 需要再次读文件,将文件中的所有字节按顺序用字节对应的编码表示,得到字符串 String allString=""; 如
* allString="0000010010101111100110101001111";
*
* @param strs
* @return
*/
public String readAgain(String[] strs, String path) {
// 文件中所有字節按順序對應的編碼
String allString = "";
try {
// 创建文件输入流用来读文件
FileInputStream fis = new FileInputStream(path);// 此处抛出异常
// 把输入流包装成缓冲流
BufferedInputStream bis = new BufferedInputStream(fis);
// 用缓冲流读字节,并把读到的字节保存到数组当中以便获得每个字节的频率
while (bis.available() > 0) {// 当剩余字节不为0时
int t = bis.read();// 缓冲流读到不同的字节就会返回不同的整型即0-255
// 存放从文件中读到的所有有序的字节编码
allString = allString + strs[t];
}
} catch (Exception e) {
e.printStackTrace();
}
return allString;
}
/**
* 将有序的字符串转化成字节
*
* @param allString
* 所读的文件中字节转化成的字符串
* @return 返回转化的字节
*/
// 将有序的字符串保存到数组当中
public byte[] toByte1(String allString){
//先定义字节数组的长度
int len=allString.length()/8+1;
if(allString.length()%8!=0){
len++;
}
//创建字节数组
byte[] b=new byte[len];
//判断字节数组最后一位的字节
if(allString.length()%8==0){
b[len-1]=0;
}else{
int length=8-allString.length()%8;
b[len-1]=(byte) length;
int buling=8-allString.length()%8;
for(int i=0;i<buling;i++){
allString=allString+"0";
}
}
// 将字符串转化成字节保存到字节数组当中
for (int i = 0; i < len-1; i++) {
String duanString = allString.substring(i * 8, i * 8 + 8);
b[i] = (byte)(Integer.parseInt(duanString, 2));
//System.out.println(duanString+"<>"+b[i]);
}
//System.out.println("字节数组的长度"+b.length);
return b;
}
/**
* 写入文件的方法 输出流
* @param path 写入的文件路径
* @param bs 要写入的数据
*/
public static void writeFile(String path,byte[] bs){
//方法分析:在写文件方法中,例如,在记事本上写数据(是字节),是从内存中将数据流出到记事本中
//步骤:1.建立一个文件输出流对象,指明要输出到的文件路径,相当于一个管道,连接内存和写的文件
// 即java.io.FileOutputStream fos=new java.io.FileOutputStream(path
//2.将内存中的数据写出来,因为事先在方法中已经定义路径和写的数据是字节数组类型
// 即//for(int i=0;i<bs.length;i++){
// byte b=bs[i];
// //写出字节
// //问题write(byte[] b)
// //有的write()方法,并不是一个节一个字节的写出,而是存在缓存区中,当满了就输出
// fos.write(b);
// }
//3.将缓冲区中的数据清空及关闭输出流fos.flush();fos.close()
try{
//创建文件输出流
java.io.FileOutputStream fos=new java.io.FileOutputStream(path);
//将字符串转化成字节数组
//byte[] bs=content.getBytes();
for(int i=0;i<bs.length;i++){
byte b=bs[i];
//写出字节
//问题write(byte[] b)
//有的write()方法,并不是一个字节一个字节的写出,而是存在缓存区中,当满了就输出
fos.write(b);
}
//清空数据(内存缓冲区的)
fos.flush();
//关闭输出流
fos.close();
}catch(Exception df){
df.printStackTrace();
}
}
/**
* 步骤七:将有序字节数组中的字节写入到文件当中
* @param path 写入的文件路径
* @param b 字节数组: 写入的字节
*/
public void writeFile1(String path,byte[] b){
try{
//创建文件输出流
FileOutputStream fos=new FileOutputStream(path);
BufferedOutputStream bos=new BufferedOutputStream(fos);
//遍历字节数组中的字节
//有的write()方法,并不是一个字节一个字节的写出,而是存在缓存区中,当满了就输出
for(int i=0;i<b.length;i++){
bos.write(b[i]);
}
bos.flush();//清空的是缓冲流中的东西,而不是输出流中的东西。注意啊!!!!!!!!!!!
fos.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
感悟:老师给我们讲了哈弗曼压缩原理,让自己写一个压缩软件,一开始不知道怎么入手,一点思路都没有,在老师的耐心指导下, 才真正理解哈弗曼压缩原理,在方法上也要遵循每写一个方法都要测试一下正确性,否则到最后一起测试的时候,找不出到底是哪个方法出错了!