시연 영상 링크 : 클릭하시면 보실 수 있습니다!
아이디어를 완성하는 Zip 압축 소프트웨어
원칙:
UTF-8 인코딩 방법에 따르면 문자는 종종 여러 바이트를 차지하므로 이진 비트로 표현하면(또한 이진 비트를 사용하여 디코딩할 수 있음) 공간을 크게 절약할 수 있습니다.
그리고 발생횟수를 이용하여 허프만트리를 구축하여 대표이진코드를 구하기 때문에(조금생각해봐도 되나, 어떻게 구하는가?), 발생빈도가 높을수록 대표이진코드가 짧아지므로, 더 우수한 압축 효과
지식 포인트 | 근본적인 |
---|---|
파일 입출력 | 입력 및 출력 스트림을 사용하여 완료(OutputStream/InputStream), 바이트 단위 읽기 및 쓰기 작업, 쓰기 및 읽기 방법 |
해시 테이블 | 키를 사용하여 값의 연결된 목록 배열을 찾으십시오. 제가 작성한 기사를 읽을 수 있습니다: hashMap |
허프만 트리 | 각 데이터의 가중치에 따라 트리를 구축합니다. 즉, 가중치가 가장 작은 두 값을 루트 노드가 가중치의 합인 트리로 병합한 다음 부모 노드의 가중치를 나머지 노드에 넣습니다. 데이터, 루프는 숫자만 있을 때까지 계속됩니다. |
문자열 연결 | 기본 논리는 스플라이싱할 때마다 StringBuilder 개체가 생성되어야 하며 그런 다음 Copy 메서드(문자열 순회)가 개체에 할당되고 스플라이싱 후에 값을 문자열로 반환한다는 것입니다. |
성취하다:
압축 방법:
1. 문서를 읽고 hashMap<>을 빌드합니다.
입력 및 출력 스트림을 사용하려고 할 때마다 예외를 throw하도록 주의해야 합니다.
동시에 코드 세그먼트에 작성된 방법으로 텍스트를 얻을 수 있습니다.
마지막으로 hashMap의 사용법도 매우 간단하여 먼저 문자열의 문자배열을 순회하여 해당 문자(키)의 존재여부를 확인한 후 가중치(발생횟수)를 수정하여 저장하면 됩니다. 입금 후 Set을 사용하여 초기 노드를 매핑하고 생성합니다.
//读取待压缩文件
private String readWaitCop(File wait) throws IOException {
//读取文件内容
FileInputStream fip = new FileInputStream(wait);
byte[] buf = new byte[fip.available()];
//**重中之重,一定要先读文本内容,填充这个byte数组,不然出大乱子——————大冤种的劝告 ~_~。
/**
* 在文件读写里:string 和 byte 可以互换,达到input/output都成为可阅读文本
* 写文件:fop.write(String.getBytes());
* 读文件: fip.read(bytes[]);
* new String(bytes[])
*/
fip.read(buf);
return new String(buf);
}
//获取权值和内容,构建初始节点
private String initSet(File wait) throws IOException {
String str = readWaitCop(wait);
HashMap<Character,Integer> hashMap = new HashMap<Character,Integer>();
char[] chars = str.toCharArray();
for (char cc:chars) {
String sinStr = String.valueOf(cc);
if(hashMap.containsKey(cc)){
hashMap.put(cc, hashMap.get(cc) + 1);
}else{
hashMap.put(cc,1);
}
}
Set<Map.Entry<Character, Integer>> entrys = hashMap.entrySet();
for(Map.Entry<Character, Integer> entry:entrys) {
Node node = new Node(null,null, entry.getKey(), entry.getValue(), null);
arrayList.add(node);
}
return str;
2. 허프만 트리를 가져옵니다.
먼저 노드 설정을 이해해야 합니다. 내 내부 노드 클래스를 볼 수 있습니다. 왼쪽
및 오른쪽 노드는 트리를 만드는 데 편리하고 가중치는 비교에 사용되며 도로는 인코딩을 기록할 수 있습니다. 내용은 문자 내용을 기록합니다.
//内部节点类
public class Node{
private Node left;
private Node right;
//出现频率(权值)
private int num;
//记载路径
private String road;
//记载内容
private char content;
public Node(Node left, Node right,char content, int num,String road) {
this.left = left;
this.right = right;
this.content = content;
this.num = num;
this.road = null;
}
}
아이디어는 원리상 허프만 트리와 동일합니다 어떻게 실현했는지 보시면 알겠지만
최소 가중치를 찾을 때마다 반복 비교를 방지하기 위해 노드를 삭제해야 한다는 점은 주목할 만합니다
. 마지막 노드가 남아 있을 때 트리가 구축되고 루트 노드를 기록합니다.
이 노드의 ArrayList는 항상 사용되므로 마지막 노드를 지우는 것을 잊지 마십시오.
//搭建树
private void setTree(){
while(arrayList.size() != 1){
//最小权值结点
Node bro1 = Min();
Node bro2 = Min();
Node father = new Node(bro1,bro2,'0',bro1.num + bro2.num,null);
arrayList.add(father);
}
root = arrayList.get(0);
arrayList.remove(0);
}
//找到最小权值
private Node Min(){
int minIndex = 0;
for (int i = 0; i < arrayList.size(); i++) {
if(arrayList.get(minIndex).num > arrayList.get(i).num){
minIndex = i;
}
}
Node node = arrayList.get(minIndex);
arrayList.remove(minIndex);
return node;
}
3. 문자 코드를 가져와서 hashMap<code, character>에 저장합니다.
여기서 언급한 문자 인코딩은 노드의 길인데 어떻게 구현해야 할까요?
사실 허프만 트리를 순회할 수 있는데, 왼쪽으로 가면 '0', 오른쪽으로 가면 '1'로 인코딩이 풀리고 전에 사용했던 가중치가 좀 많이 쓰이는 데이터 레이어를 좀 더 적게 만들어서 압축 효과를 얻기 위해 인코딩이 더 짧습니다.
다음 코드가 인코딩됩니다:
실제 데이터를 저장하는 노드는 실제로 리프 노드입니다. 즉, 노드에 자식 노드가 없습니다.그 이유를 생각해 볼 수 있습니다.
순회 트리에는 실제로 선순 순회, 후순 순회 등 다양한 방법이 있지만 제가 쓴 것은 재귀를 피하고 생각을 단순화할 수 있는 계층 순회를 사용합니다.
그러나 Queue 대기열은 내가 직접 작성하므로 일반 대기열과 다를 수 있습니다.
계층적 순회를 설명할 수 있습니다. 허프만 트리는 완벽한 이진 트리이므로 각 노드에는 하위 노드가 없거나 두 개 있으므로 Queue 데이터 구조를 사용하여 노드를 팝하고 두 개의 노드를 푸시할 수 있습니다. FIFO 메커니즘, 순회 효과가 달성됩니다.
그렇다면 hashMap에 저장되는 이유는 중요하다.복선, 설명이 이어집니다.
//得到路径
private void setRoad() throws IOException {
Queue<Node> queue = new Queue<>();
queue.enqueue(root);
root.road = "";
while (!queue.isEmpty()){
if(queue.peak().left != null) {
queue.enqueue(queue.peak().left);
queue.peak().left.road = queue.peak().road + '0';
}
if(queue.peak().right != null){
queue.enqueue(queue.peak().right);
queue.peak().right.road = queue.peak().road + '1';
}else{
arrayList.add(queue.peak());
}
queue.dequeue();
}
//得到readMap
for (int i = 0; i < arrayList.size(); i++) {
readMap.put(arrayList.get(i).road,arrayList.get(i).content);
}
}
4. hashMap<Character, String>에 따라 디코딩하여 0/1 문자열을 얻습니다.
여기에서 우리가 읽은 텍스트를 순회하고 hashMap에 따라 문자를 해당 0/1 코드로 바꾸고 새 문자열을 얻는 것은 비교적 간단하지만 1바이트 = 8비트이므로 a에 대한 보완이 필요하다는 것을 이해해야 합니다. 8의 배수, 그렇지 않으면 바이트로 변환하여 파일에 쓸 때 오류가 발생하며 동시에 완료 후 문자열 헤더에 보조 자릿수를 추가해야 합니다.
//存入内容
char[] chars = comContent.toCharArray();
//先将comContent清空,得到有效位数+二进制字符串
/**
* 这是压缩时最耗时间的部分,可以考虑换成string来算,因为这个比char的hashcode算的快,
* 因为算hashCode从map取值,重复过多了
*/
//注意清空啊,嘚吧!
comContent = "";
StringBuilder builder = new StringBuilder();
for (char cc:chars) {
//hashMap.get可查看源码,返回值为V;
builder.append(hashMap.get(cc));
}
//存入补足编码
int num = 8 - builder.length() % 8;
for (int i = 0; i < num; i++) {
builder.append('0');
}
//存入补的位数,直接存数字,eg:……+"num"。
byte b = (byte) num;
//运用 StringBuilder ,出现了heap溢出
builder.insert(0,String.valueOf(b));
comContent = builder.toString();
주목할 가치가 있습니다: comContent는 initSet(wait)[구성 노드 초기화]를 통해 얻어지며, char[] 배열로 변환하는 것을 기억하고, 인코딩된 문자열을 얻기 위해 그것을 지우는 것을 기억하십시오.
5. (0/1 string -> byte[]) 문서에 쓰기(hashMap<encoding, character>가 작성됨):
여기서는 주로 0/1 문자열을 바이트로 변환하는 방법에 대해 이야기하는데, '0'을 읽으면 바이트가 왼쪽으로 이동하고, '1'을 읽으면 바이트를 왼쪽으로 이동하여 +1이 된다. , 8회 읽기 btye 값을 형성하고 바이트【】에 쓸 수 있습니다.
그리고 byte[]의 첫 번째 비트는 압축 해제 프로세스에 편리한 마지막 바이트의 유효 0/1 비트를 저장하므로 텍스트를
파일 작성 요구 사항을 충족하는 byte[] 배열로 변환합니다.
//写入到文件的具体方法
private void outputStream(String comContent,File after) throws IOException {
//得到要写入字符串的字符
char[] chars = comContent.toCharArray();
//得到需要写多少byte,初始位置存num
byte[] bytes = new byte[(chars.length - 1) / 8 + 1];
//得到最后一个字节中的几位有效
int num = 8 - ((int) chars[0] - 48);
//(byte)转型,可以得到int num的最后一个字节
bytes[0] = (byte) (num & 255);
//第一位是存储需要丢弃几个数字,所以可以从下标1开始
for (int i = 1; i < bytes.length; i++) {
byte b = 0;
int bb = i - 1;
for (int j = 1; j <= 8; j++) {
if(chars[bb * 8 + j] == '0'){
b = (byte) (b << 1);
}else{
b = (byte) (b <<1 + 1);
}
bytes[i] = b;
}
}
//开写开写
FileOutputStream fop = new FileOutputStream(after);
/**
* 可以去仔细研究:::
* 写入readMap,只能用ObjectInputStream,写入任何类型
* 但要注意,该类型要实现java.io.Serializable,使其“序列化”
*/
ObjectOutputStream foop = new ObjectOutputStream(fop);
//写入任何类型
foop.writeObject(readMap);
fop.write(bytes);
}
여기서 우리는 3단계를 강조할 필요가 있습니다.복선예, 인코딩 규칙이 다르기 때문에 압축을 풀고 읽으려면 당시 저장한 hashMap인 참조용 디코딩 테이블이 필요합니다. 이 양식이 부족하다면 제2차 세계대전 당시 전보가 도청된 후 그의 얼굴 표정은 마치 폐지된 것처럼 당황하여 할 말을 잃었을 것이라고 상상할 수 있습니다. 디코딩 테이블은 강력하고 Serializable 인터페이스를 구현하는 모든 유형에 쓸 수 있는 ObjectOutputStream(둘 다 파일 I/O 스트림 기반)의 도움으로 작성됩니다.
궁금하신 분들은 확인하시면 됩니다.
압축 해제 방법:
1. (바이트[]->0/1 문자열) 프로세스:
참고로 쉬프트는 7->0에서 연속 쉬프트 되므로 0/1 문자열을 순서대로 얻을 수 있으니 알아서 생각하시면 됩니다.
for (int i = 1; i < bytes.length; i++) {
//将byte转换为字符串
for (int j = 7; j >= 0 ; j--) {
int num = bytes[i];
if(((num >> j) & 1) == 0){
codeBulider.append('0');
}
else{
codeBulider.append('1');
}
}
}
2. read hashMap<code, character>, (0/1 string -> String)에 따라 실제 콘텐츠 문자열을 가져옵니다.
hashMap<encoding, character> 디코딩 테이블 읽기
FileInputStream fip = new FileInputStream(wait);
// Object的输入输出流都是基于file输入输出流
ObjectInputStream fiop = new ObjectInputStream(fip);
디코딩을 시작하고 sinBuilder(단일 문자)를 사용하여 0/1을 연결한 다음 키(코드)가 존재하는지 확인하고 코드가 존재할 때까지 반복한 다음 contentBuilder를 사용하여 키를 통해
hashMap을 연결하여 값을 찾습니다( 문자) , 브라우징이 완료되고 디코딩이 완료되었습니다.在这里插入代码片
for (char cc:chars) {
sinBuilder.append(cc);
if(readMap.containsKey(sinBuilder.toString())){
contentBuilder.append(readMap.get(sinBuilder.toString()));
sinBuilder.delete(0, sinBuilder.length());
}
}
content = contentBuilder.toString();
3. 압축 해제된 파일에 쓰기:
String에 쓸 때 String.getBytes() 메서드를 사용해야 합니다.
FileOutputStream fop = new FileOutputStream(after);
fop.write(content.getBytes());
인터페이스 디자인:
외관 디자인:
양식을 만든 다음 구성 요소를 만드는 것 이상이며 코드는 소스 코드에 있으며 매우 간단합니다.
액션리스너 생성
내부 리스너 클래스를 전달할 수 있으므로 필요한 데이터를 전달하지 않고 마음대로 호출할 수 있습니다.
예를 들어 여기서는 작업하기 전에 주소를 먼저 얻지 않고 텍스트 상자의 텍스트를 직접 사용할 수 있습니다.
참고: 내부 리스너 클래스 끝에 세미콜론이 있으며 긴 문장으로 이해할 수 있습니다.
ActionListener ac = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Color tuWhite = new Color(238,238,238);
g.setColor(tuWhite);
g.fillRect(300,550,400,400);
g.drawImage(starBuf,300,550,400,400,null);
long start = System.currentTimeMillis();
String btname = e.getActionCommand();
File wait = new File(jt1.getText());
File after = new File(jt2.getText());
if(btname.equals("压缩")){
try {
hf.copCode(wait,after);
} catch (IOException ex) {
ex.printStackTrace();
}
}else{
try {
hf.unPack(wait,after);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
long end = System.currentTimeMillis();
double time = (end - start) / 1000;
System.out.println(btname + ": "+ time + " S");
g.setColor(tuWhite);
g.fillRect(300,550,400,400);
g.drawImage(finBuf,300,550,400,400,null);
}
};
실행 중인 그림 로고 추가
버튼을 클릭하면 로딩 바 그림을 그릴 수 있고 종료 후 봉인 성공 그림을 그릴 수 있습니다.이 코드는 모두 위에 있습니다.
최적화
부스트 속도
원래의 스트링 스플라이싱 프로세스를 사용하면 매우 느리고 참을 수 없는 지점에 도달했음을 알 수 있습니다. 여기에서 나만의 솔루션을 제안합니다.
1. 최소 무게 찾기:
솔루션: 버블 정렬과 같은 저수준 정렬을 사용하는 경우 효율성이 낮고 문자가 많을 때 당연히 적용할 수 없습니다. 퀵정렬을 이용하거나 저처럼 최소값의 첨자를 직접 찾아내어 속도를 크게 향상시킬 수 있습니다.
2. 시간 소모적인 문자열 접합:
솔루션: 문자열의 "+" 를 직접 사용하여 연결하는 경우 기본 원칙에 따라 얼마나 많은 시간이 낭비되는지 생각할 수 있습니다.모든 연결을 통과하고 복사한 다음 연결해야 합니다. [시간 복잡도는 (n^2 )]. 따라서 모든 콘텐츠를 연결한 다음 이를 String 유형으로 변환하는 StringBuilder 객체만 생성하면 됩니다.
그 효과는 매우 놀라운데, 당시에는 1M 미만의 파일에 200초 이상이 걸렸지만 지금은 100M 파일의 경우 약 6초 밖에 걸리지 않습니다.
관심이 있는 경우 String의 추가 방법을 살펴볼 수 있습니다.
부스트 용량
StringBuilder에는 제한이 있습니다.
300M 텍스트 압축을 사용하면 힙 오버플로 예외가 보고됩니다.StringBuilder의 소스 코드를 쿼리한 결과 하위 레이어가 char[] 배열이고 길이가 int 유형으로 제한되어 텍스트가 너무 많은 경우 , 메모리 오버플로가 쌓일 것입니다.
해결책: StringBuilder【】의 배열을 생성하고 연결할 문자 수를 지정한 다음 다음 StringBuilder로 이동하고 마지막으로 접합하여 용량을 확장할 수 있습니다.
이것은 개인적인 생각일 뿐 아직 완성되지 않았으니, 높은 이상을 가진 분들이 운영해 주셨으면 합니다.
jar 파일로 패키징: 그림과 같이
소스 코드(모든 코드 포함):
1. 인터페이스
package fcj1028;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class Ui extends JFrame {
public void ui() throws IOException {
Huffman_Compress hf = new Huffman_Compress();
this.setTitle("压缩软件");
this.setSize(1000,1000);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
JLabel jl1 = new JLabel("待处理文件:");
jl1.setFont(new java.awt.Font("Dialog", 1, 30));
JLabel jl2 = new JLabel("处理后文件:");
jl2.setFont(new java.awt.Font("Dialog", 1, 30));
JTextField jt1 = new JTextField();
Dimension dim1 = new Dimension(800,100);
Dimension dim2 = new Dimension(400,150);
jt1.setFont(new java.awt.Font("Dialog", 1, 30));
JTextField jt2 = new JTextField();
jt2.setFont(new java.awt.Font("Dialog", 1, 30));
JButton jb1 = new JButton("压缩");
jb1.setFont(new java.awt.Font("Dialog", 1, 30));
JButton jb2 = new JButton("解压");
jb2.setFont(new java.awt.Font("Dialog", 1, 30));
jt1.setPreferredSize(dim1);
jt2.setPreferredSize(dim1);
jb1.setPreferredSize(dim2);
jb2.setPreferredSize(dim2);
this.add(jl1);
this.add(jt1);
this.add(jl2);
this.add(jt2);
this.add(jb1);
this.add(jb2);
this.setLayout(new FlowLayout());
this.setVisible(true);
//先可视化,再得到画笔
Graphics g = this.getGraphics();
File starPic = new File("C:\\Users\\27259\\Pictures\\java_pic\\9df86687_E848203_9ea3a43f-removebg-preview.png");
BufferedImage starBuf = ImageIO.read(starPic);
File finPic = new File("C:\\Users\\27259\\Pictures\\java_pic\\R-C-removebg-preview(1).png");
BufferedImage finBuf = ImageIO.read(finPic);
ActionListener ac = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Color tuWhite = new Color(238,238,238);
g.setColor(tuWhite);
g.fillRect(300,550,400,400);
g.drawImage(starBuf,300,550,400,400,null);
long start = System.currentTimeMillis();
String btname = e.getActionCommand();
File wait = new File(jt1.getText());
File after = new File(jt2.getText());
if(btname.equals("压缩")){
try {
hf.copCode(wait,after);
} catch (IOException ex) {
ex.printStackTrace();
}
}else{
try {
hf.unPack(wait,after);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
long end = System.currentTimeMillis();
double time = (end - start) / 1000;
System.out.println(btname + ": "+ time + " S");
g.setColor(tuWhite);
g.fillRect(300,550,400,400);
g.drawImage(finBuf,300,550,400,400,null);
}
};
jb1.addActionListener(ac);
jb2.addActionListener(ac);
}
public static void main(String[] args) throws IOException {
new Ui().ui();
}
}
```java
```java
```java
2. 압축 소프트웨어
package fcj1028;
import Algorithm.Linear.Queue;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Huffman_Compress {
//写到压缩文件里,方便之后,文件读出内容(避免程序结束,所存hashMap消失)
private HashMap<String,Character> readMap = new HashMap<>();
//根节点,便于寻找
private Node root;
/**哈夫曼树:a.先对节点排序保存到List中
b.取出List中最小的两个节点,让它们的权值累加,保存新的父节点中
c.让最小两个节点作为左右子树,把父节点添加到List中重新排序
d.直到List只有一个节点
3.设置叶子节点的编码,往左编码为1 往右编码为0
统计每个叶子节点对应的编码
*/
//用途:节省空间存储内容
//原理:使用路径编码即可代表字符,而且越常用字符编码越少
//用来存节点
private ArrayList<Node> arrayList = new ArrayList<Node>();
//内部节点类
public class Node{
private Node left;
private Node right;
//出现频率(权值)
private int num;
//记载路径
private String road;
//记载内容
private char content;
public Node(Node left, Node right,char content, int num,String road) {
this.left = left;
this.right = right;
this.content = content;
this.num = num;
this.road = null;
}
}
//获取权值和内容,构建初始节点
private String initSet(File wait) throws IOException {
String str = readWaitCop(wait);
HashMap<Character,Integer> hashMap = new HashMap<Character,Integer>();
char[] chars = str.toCharArray();
for (char cc:chars) {
String sinStr = String.valueOf(cc);
if(hashMap.containsKey(cc)){
hashMap.put(cc, hashMap.get(cc) + 1);
}else{
hashMap.put(cc,1);
}
}
Set<Map.Entry<Character, Integer>> entrys = hashMap.entrySet();
for(Map.Entry<Character, Integer> entry:entrys) {
Node node = new Node(null,null, entry.getKey(), entry.getValue(), null);
arrayList.add(node);
}
return str;
}
//读取待压缩文件
private String readWaitCop(File wait) throws IOException {
//读取文件内容
FileInputStream fip = new FileInputStream(wait);
/**
* V1.0:一开始的想法是,得到的是byte【】数组,转换为字符串
* 其中借用了fip.available(),可以返回文件的字节数
* 但是问题在于,这样的话byte[]数组并不能转换成字符串内容,反而得到的是其长度
* 原因:当时也忘记了用fip.read(bytes)存放内容,所以读出长度十分正常
* 故,我们的想法可以是用将每个字节转换为char。
* V2.0:由于数字和中文编码并不一样,所以这个方法也要淘汰
* V3.0:在得到bytes[]后,可以使用String()方法,规定好长度自动会帮我们转型
* 值得注意的是:String.valueOf()会得到乱码
*/
byte[] buf = new byte[fip.available()];
//**重中之重,一定要先读文本内容,填充这个byte数组,不然出大乱子——————大冤种的劝告 ~_~。
/**
* 在文件读写里:string 和 byte 可以互换,达到input/output都成为可阅读文本
* 写文件:fop.write(String.getBytes());
* 读文件: fip.read(bytes[]);
* new String(bytes[])
*/
fip.read(buf);
return new String(buf);
}
//搭建树
private void setTree(){
while(arrayList.size() != 1){
//最小权值结点
Node bro1 = Min();
Node bro2 = Min();
Node father = new Node(bro1,bro2,'0',bro1.num + bro2.num,null);
arrayList.add(father);
}
root = arrayList.get(0);
arrayList.remove(0);
}
//找到最小权值
private Node Min(){
int minIndex = 0;
for (int i = 0; i < arrayList.size(); i++) {
if(arrayList.get(minIndex).num > arrayList.get(i).num){
minIndex = i;
}
}
Node node = arrayList.get(minIndex);
arrayList.remove(minIndex);
return node;
}
//得到路径
/**
* 首先表扬一下自己,通过不断输出语句还是找到了bug
* 这里的问题并非是树连接失败,而是我(nt)的忘了把队列里的节点叉出去,导致死循环
*/
private void setRoad() throws IOException {
Queue<Node> queue = new Queue<>();
queue.enqueue(root);
root.road = "";
while (!queue.isEmpty()){
if(queue.peak().left != null) {
queue.enqueue(queue.peak().left);
queue.peak().left.road = queue.peak().road + '0';
}
if(queue.peak().right != null){
queue.enqueue(queue.peak().right);
queue.peak().right.road = queue.peak().road + '1';
}else{
arrayList.add(queue.peak());
}
queue.dequeue();
}
//得到readMap
for (int i = 0; i < arrayList.size(); i++) {
readMap.put(arrayList.get(i).road,arrayList.get(i).content);
}
}
//写入到文件的具体方法
private void outputStream(String comContent,File after) throws IOException {
//得到要写入字符串的字符
char[] chars = comContent.toCharArray();
//得到需要写多少byte,初始位置存num
byte[] bytes = new byte[(chars.length - 1) / 8 + 1];
//得到最后一个字节中的几位有效
int num = 8 - ((int) chars[0] - 48);
//(byte)转型,可以得到int num的最后一个字节
bytes[0] = (byte) (num & 255);
//第一位是存储需要丢弃几个数字,所以可以从下标1开始
for (int i = 1; i < bytes.length; i++) {
byte b = 0;
/**
* V1.0:使用byte存储,思路是想用位运算使得效率更高,但是发现不是想要的效果,存入的都是0
* 原因:暂时还不清楚,可去看这篇文章
* https://blog.csdn.net/weixin_39775428/article/details/114176698
* 所以,现在还是用 * 2.
*/
int bb = i - 1;
for (int j = 1; j <= 8; j++) {
if(chars[bb * 8 + j] == '0'){
b = (byte) (b * 2);
}else{
b = (byte) (b * 2 + 1);
}
/**
* 值得注意:byte表示范围为 -128~127,一旦超出127,会自动转换
* 但是读取时就需要主要判断正负,得到第一位了,问题在于补码中的0,存在 10000000,00000000两种情况,需要特殊判断
*/
bytes[i] = b;
}
}
//开写开写
FileOutputStream fop = new FileOutputStream(after);
/**
* 可以去仔细研究:::
* 写入readMap,只能用ObjectInputStream,写入任何类型
* 但要注意,该类型要实现java.io.Serializable,使其“序列化”
*/
ObjectOutputStream foop = new ObjectOutputStream(fop);
//写入任何类型
foop.writeObject(readMap);
fop.write(bytes);
}
//得到压缩后的编码(除了补足八位,构成byte存入),还需要记录补了几个),并写入到文件
public void copCode(File wait,File after) throws IOException {
String comContent = initSet(wait);
setTree();
setRoad();
//利用HashMap存好字符的数据编码,即路径
HashMap<Character,String> hashMap = new HashMap<>();
for (Node node:arrayList) {
hashMap.put(node.content, node.road);
}
//存入内容
char[] chars = comContent.toCharArray();
//先将comContent清空,得到有效位数+二进制字符串
/**
* 这是压缩时最耗时间的部分,可以考虑换成string来算,因为这个比char的hashcode算的快,
* 因为算hashCode从map取值,重复过多了
*/
//注意清空啊,嘚吧!
comContent = "";
/**
* 神之一手 ———— 郝为高
* 由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
*/
StringBuilder builder = new StringBuilder();
for (char cc:chars) {
//hashMap.get可查看源码,返回值为V;
builder.append(hashMap.get(cc));
}
//存入补足编码
int num = 8 - builder.length() % 8;
for (int i = 0; i < num; i++) {
builder.append('0');
}
//存入补的位数,直接存数字,eg:……+"num"。
byte b = (byte) num;
//运用 StringBuilder ,出现了heap溢出
builder.insert(0,String.valueOf(b));
comContent = builder.toString();
builder.delete(0,builder.length());
outputStream(comContent,after);
}
//解压压缩后的文本
public void unPack(File wait,File after) throws IOException, ClassNotFoundException {
//开始读出readMap(sb,你自己要注意你是用的那个文件啊啊啊啊啊!!!!!)
FileInputStream fip = new FileInputStream(wait);
// Object的输入输出流都是基于file输入输出流
ObjectInputStream fiop = new ObjectInputStream(fip);
HashMap<String,Character> readMap = (HashMap<String, Character>) fiop.readObject();
//将内容都写到byte[]数组里
byte[] bytes = new byte[fip.available()];
fip.read(bytes);
/**
* 神之一手 ———— 郝为高
* 由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
*/
//存储编码后的0/1串
String copCode = "";
StringBuilder codeBulider = new StringBuilder();
for (int i = 1; i < bytes.length; i++) {
//将byte转换为字符串
for (int j = 7; j >= 0 ; j--) {
int num = bytes[i];
if(((num >> j) & 1) == 0){
codeBulider.append('0');
}
else{
codeBulider.append('1');
}
}
}
copCode = codeBulider.toString();
//删除补足‘0’字符串
copCode = copCode.substring(0,copCode.length() - bytes[0] - 1);
//为了提高效率的到str的char[]数组
char[] chars = copCode.toCharArray();
//得到全体字符
String content = "";
/**
* 神之一手 ———— 郝为高
* 由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
*/
StringBuilder sinBuilder = new StringBuilder();
StringBuilder contentBuilder = new StringBuilder();
for (char cc:chars) {
sinBuilder.append(cc);
if(readMap.containsKey(sinBuilder.toString())){
contentBuilder.append(readMap.get(sinBuilder.toString()));
sinBuilder.delete(0, sinBuilder.length());
}
}
content = contentBuilder.toString();
FileOutputStream fop = new FileOutputStream(after);
fop.write(content.getBytes());
}
}