java语言实现对输入的一串电文字符实现哈夫曼编码,并对给定的代码串进行译码,输出电文字符串。具体包括哈夫曼树的建立、哈夫曼编码的生成和编码文件的译码。
时间: 2025-07-25 10:29:27 浏览: 20
<think>我们首先需要构建哈夫曼树。构建哈夫曼树的步骤是:
1. 统计字符频率:读取文件,统计每个字符出现的频率(权重)。
2. 创建节点:为每个字符创建一个节点,节点的权重即为字符的频率。
3. 构建优先队列:将所有节点放入一个优先队列(最小堆),权重小的节点优先级高。
4. 构建哈夫曼树:重复以下步骤直到队列中只剩一个节点:
a. 从队列中取出两个权重最小的节点。
b. 创建一个新节点,其左右子节点分别为这两个节点,新节点的权重为两个子节点权重之和。
c. 将新节点加入队列。
5. 剩下的最后一个节点即为哈夫曼树的根节点。
然后,生成哈夫曼编码:
从根节点开始遍历哈夫曼树(通常使用深度优先遍历),向左走则添加'0',向右走则添加'1',直到叶子节点。此时,每个叶子节点对应的路径就是该字符的哈夫曼编码。
接着,进行编码:
读取源文件,将每个字符替换为其对应的哈夫曼编码,并将编码后的二进制数据写入新文件(注意,这里可能需要处理二进制写入,因为编码后的数据是比特流)。
最后,译码:
读取编码后的文件(二进制文件),利用哈夫曼树从根开始,根据比特位(0向左,1向右)遍历到叶子节点,输出对应的字符,直到处理完所有比特位。
根据引用,我们可以参考以下实现细节:
- 引用[1]提到:在遍历时,每遍历到左孩子,在结点的code变量加上“0”,右孩子加上“1”。这样遍历后,叶子结点的code就是哈夫曼编码。
- 引用[5]提到:使用HashMap统计字符频率,然后构建节点列表,再构建哈夫曼树。
- 引用[4]提到:译码时,根据二进制码字符串,从根节点开始,0向左1向右,直到叶子节点。
我们按照以下步骤实现:
1. 定义节点类(Node),包含字符、权重、左子节点、右子节点、父节点(可选)以及编码字符串(可选,也可以在生成编码时动态生成)。
2. 构建统计字符频率的方法。
3. 构建哈夫曼树的方法。
4. 生成编码表(字符到哈夫曼编码的映射)。
5. 编码方法:读取文件,将每个字符替换为编码,并将编码写入文件(注意:这里写入的是二进制,所以需要将字符串形式的编码转换为比特流)。
6. 译码方法:读取编码文件(比特流),利用哈夫曼树进行译码,并将结果写入新文件。
注意:由于编码后是二进制文件,我们需要处理字节对齐(例如,最后一个字节可能不足8位,需要补0,并在译码时忽略补的0)。因此,我们可能需要在编码文件中保存一些额外信息,如字符频率表(用于重建哈夫曼树)或编码表,以及补位信息。或者,我们也可以在编码文件的开头写入哈夫曼树的结构(如引用[3]提到的层序遍历序列),这样译码时可以直接重建哈夫曼树。
但是,为了简化,我们可以选择在编码文件的开头写入字符频率表(这样译码时可以根据频率表重建哈夫曼树),然后写入补位信息(最后一个字节补了多少位),再写入编码数据。
另一种常见做法是:在编码文件中存储字符及其频率(或权重),这样译码程序可以重建哈夫曼树。具体步骤如下:
编码文件结构:
- 字符频率表(例如:每个字符及其权重,可以用序列化的方式存储)
- 补位数(1个字节,表示最后一个字节的有效位数,如果刚好8位则为0,表示不需要补位?注意:实际上补位数在0-7之间,所以我们可以用1个字节存储)
- 编码后的数据
译码时:
- 先读取字符频率表,重建哈夫曼树。
- 读取补位数。
- 读取剩余的二进制数据,根据补位数忽略最后字节的补位,然后利用哈夫曼树进行译码。
然而,为了更简单,我们可以采用引用[3]的方法:将哈夫曼树按照层序遍历写入文件。但这样可能占用空间较大。我们这里采用存储频率表的方式。
具体实现:
1. 节点类定义:
class Node {
char ch; // 字符,如果是内部节点,可以用特殊字符(如'\0')表示
int freq; // 频率
Node left, right;
// 构造函数
public Node(char ch, int freq, Node left, Node right) {
this.ch = ch;
this.freq = freq;
this.left = left;
this.right = right;
}
// 判断是否是叶子节点
public boolean isLeaf() {
return left == null && right == null;
}
}
2. 统计频率:读取文件,用一个HashMap<Character, Integer>统计。
3. 构建哈夫曼树:
- 创建优先队列(最小堆),将所有字符节点加入。
- 当队列大小大于1时:
取出两个最小节点,创建一个新节点(频率为二者之和,字符设为'\0'),新节点的左右子节点分别为这两个节点。
将新节点加入队列。
- 最后剩下的节点就是根节点。
4. 生成编码表:递归遍历哈夫曼树,在遍历过程中记录路径(字符串),当遇到叶子节点时,将字符和路径字符串存入Map<Character, String>。
5. 编码文件:
- 首先,将频率表写入文件(例如,先写字符总数,然后每个字符和其频率(int))。
- 然后,读取源文件,对每个字符,将其编码字符串拼接到一个大的二进制字符串中。
- 将这个二进制字符串每8位(一个字节)转换成一个byte,写入文件。注意最后不足8位的部分,补0直到8位,并记录补了多少位(补位数=8-剩余位数)。
- 在频率表后,写入补位数(一个字节)。
- 最后写入编码数据。
6. 译码文件:
- 读取频率表,重建哈夫曼树。
- 读取补位数(一个字节)。
- 读取剩余的所有字节数据(即编码后的二进制数据)。
- 将读取的每个字节转换成8位的二进制字符串(注意:最后一个字节要根据补位数去掉多余的0)。
- 从根节点开始,按二进制字符串的每一位(0向左,1向右)遍历哈夫曼树,到达叶子节点时输出字符,并回到根节点继续。
- 将译码得到的字符写入新文件。
注意:由于字符可能是任何字符(包括不可见字符),所以我们在写入频率表时,可以用DataOutputStream和DataInputStream来读写字符(char)和整数(int)。但是注意,char在Java中是2个字节,而实际我们可能只需要处理ASCII,但为了通用性,我们按char处理。
另一种做法:为了避免处理二进制,我们也可以将编码后的二进制字符串按字符写入(每个字符'0'或'1'),但这样文件会变大,失去了压缩的意义。所以我们采用二进制写入。
由于时间关系,我们简化一些步骤,例如我们假设文件不大,可以一次性读取到内存。对于大文件,可能需要流式处理。
下面我们逐步实现代码:
步骤1:节点类
步骤2:统计频率
步骤3:构建哈夫曼树
步骤4:生成编码表
步骤5:编码(包括写入频率表、补位数和编码数据)
步骤6:译码(读取频率表重建树,读取补位数和编码数据,译码)
注意:在编码时,我们可能会遇到内存问题(如果文件很大,二进制字符串会很长),所以我们可以边读边写,即读取一个字符,获取其编码,然后将编码的每个比特位逐步写入字节缓冲区,当缓冲区满8位就写入文件。
因此,我们修改编码步骤:
- 先写入频率表(字符总数,然后每个字符及其频率(int))。
- 初始化一个字节缓冲区(一个byte,8位)和一个计数器(记录当前缓冲区的位数)。
- 读取源文件,对每个字符:
获取其编码字符串(由0和1组成)
遍历编码字符串的每个字符(即每个比特):
将比特(0或1)放入缓冲区的下一位(通过移位操作)
计数器加1
如果计数器满8,则将缓冲区写入文件,并重置缓冲区和计数器。
- 文件读取完后,如果计数器大于0,说明还有数据未写入,那么将缓冲区剩余部分补0,然后写入文件,并记录补位数(8-计数器)。
- 在频率表后写入补位数(一个字节)。
译码步骤:
- 读取频率表,重建哈夫曼树。
- 读取补位数(一个字节)。
- 初始化一个字节缓冲区,读取后续的所有字节(用字节数组读取整个文件剩余部分,或者逐个字节读取)。
- 从根节点开始,遍历每个字节的每一位(从高位到低位):
对于每个字节,从第1位到第8位(注意:最后一个字节可能只有部分有效,所以需要根据补位数处理最后字节的有效位)。
如果当前位是0,则移动到左子节点;是1则移动到右子节点。
当到达叶子节点时,输出字符,并回到根节点。
- 注意:在最后一个字节,如果补位数是k,那么只取前(8-k)位。
由于译码时需要知道文件的总比特数,我们可以在编码时记录总比特数?但这里我们通过补位数可以推算出最后一个字节的有效位数,而前面的字节都是8位。总比特数 = (字节数-1)*8 + (8-补位数)。但是,我们在译码时并不知道总比特数,因为我们不知道编码后有多少个字节(除了补位数那一个字节,后面的都是编码数据)。所以,我们在读取编码数据后,先得到所有字节,然后根据补位数调整最后一个字节。
具体译码:
- 读取所有编码数据(字节数组),设为bytes。
- 总字节数 = bytes.length
- 总比特数 = (bytes.length-1)*8 + (8 - padding) [注意:如果总字节数为0,则没有数据]
- 但实际上,我们不需要总比特数,因为我们可以这样处理:
for (int i=0; i<bytes.length; i++) {
byte b = bytes[i];
int bits = 8;
if (i == bytes.length-1) {
bits = 8 - padding; // 最后一个字节的有效位数
}
for (int j=0; j<bits; j++) {
// 取b的第j位(从高位开始)
int bit = (b >> (7-j)) & 1;
// 根据bit移动节点
if (bit == 0) {
current = current.left;
} else {
current = current.right;
}
if (current.isLeaf()) {
// 输出字符
// 重置current为根节点
current = root;
}
}
}
但是,注意:我们写入的编码数据部分不包括频率表和补位数。我们在写入文件时,先写入频率表,再写入补位数(一个字节),再写入编码数据。所以,译码时:
- 先读取频率表(需要知道有多少个字符,所以可以先读一个整数n,表示字符种类数,然后读n个字符和频率)
- 然后读一个字节(补位数)
- 最后读取剩余的所有字节(编码数据)
下面我们开始写代码框架:
由于代码较长,我们分模块实现。
注意:我们假设文件是文本文件,使用字符流读取。但编码后的文件是二进制文件,必须使用字节流。
具体实现:
1. 节点类(Node)
2. HuffmanCoding类,包含以下方法:
- buildFrequencyMap(String filename) : 统计字符频率
- buildHuffmanTree(Map<Character, Integer> freqMap) : 构建哈夫曼树
- buildCodeMap(Node node, String code, Map<Character, String> codeMap) : 递归生成编码表
- encode(String inputFile, String outputFile) : 编码方法
- decode(String inputFile, String outputFile) : 译码方法
3. 主函数测试
由于引用[5]提供了构建节点列表的代码,我们可以参考。
下面给出具体代码:</think>### Java实现哈夫曼编码与译码
#### 1. 哈夫曼树构建
哈夫曼树是一种带权路径长度最短的二叉树,构建步骤如下:
1. **统计字符频率**:读取文件,统计每个字符出现的频率
2. **创建节点**:为每个字符创建节点,权重为频率
3. **构建优先队列**:将所有节点放入最小堆(优先级队列)
4. **合并节点**:重复取出两个最小节点,合并为新节点(权重为子节点之和),放回队列
5. **形成树结构**:最后剩余的节点即为根节点
```java
// 节点类定义
class Node implements Comparable<Node> {
char ch;
int freq;
Node left, right;
public Node(char ch, int freq, Node left, Node right) {
this.ch = ch;
this.freq = freq;
this.left = left;
this.right = right;
}
public boolean isLeaf() {
return left == null && right == null;
}
public int compareTo(Node other) {
return this.freq - other.freq;
}
}
// 构建哈夫曼树
public static Node buildHuffmanTree(Map<Character, Integer> freqMap) {
PriorityQueue<Node> pq = new PriorityQueue<>();
for (char c : freqMap.keySet()) {
pq.offer(new Node(c, freqMap.get(c), null, null));
}
while (pq.size() > 1) {
Node left = pq.poll();
Node right = pq.poll();
Node parent = new Node('\0', left.freq + right.freq, left, right);
pq.offer(parent);
}
return pq.poll();
}
```
#### 2. 编码生成
通过遍历哈夫曼树生成字符的二进制编码:
- 左路径添加'0'
- 右路径添加'1'
- 叶子节点存储完整编码
```java
// 生成编码表
public static void buildCodeMap(Node node, String code, Map<Character, String> codeMap) {
if (node.isLeaf()) {
codeMap.put(node.ch, code);
return;
}
buildCodeMap(node.left, code + "0", codeMap);
buildCodeMap(node.right, code + "1", codeMap);
}
// 文件编码
public static String encode(String text, Map<Character, String> codeMap) {
StringBuilder encoded = new StringBuilder();
for (char c : text.toCharArray()) {
encoded.append(codeMap.get(c));
}
return encoded.toString();
}
```
#### 3. 文件译码
根据哈夫曼树遍历二进制串:
- '0'移动到左子树
- '1'移动到右子树
- 到达叶子节点输出字符并重置到根节点
```java
public static String decode(String encoded, Node root) {
StringBuilder decoded = new StringBuilder();
Node current = root;
for (char bit : encoded.toCharArray()) {
current = (bit == '0') ? current.left : current.right;
if (current.isLeaf()) {
decoded.append(current.ch);
current = root; // 重置到根节点
}
}
return decoded.toString();
}
```
#### 4. 完整工作流程
```java
// 主流程
public static void main(String[] args) {
// 1. 读取文件内容
String text = Files.readString(Path.of("input.txt"));
// 2. 统计字符频率
Map<Character, Integer> freqMap = new HashMap<>();
for (char c : text.toCharArray()) {
freqMap.put(c, freqMap.getOrDefault(c, 0) + 1);
}
// 3. 构建哈夫曼树
Node root = buildHuffmanTree(freqMap);
// 4. 生成编码表
Map<Character, String> codeMap = new HashMap<>();
buildCodeMap(root, "", codeMap);
// 5. 编码文件
String encoded = encode(text, codeMap);
Files.write(Path.of("encoded.bin"), encoded.getBytes());
// 6. 译码文件
String decoded = decode(encoded, root);
Files.write(Path.of("decoded.txt"), decoded.getBytes());
}
```
#### 文件处理注意事项
1. **二进制文件处理**:编码后文件需按比特写入,Java可用`BitSet`
2. **序列化树结构**:译码需重建哈夫曼树,可存储:
- 字符频率表
- 层序遍历序列
- 特殊标记叶子节点
3. **填充位处理**:最后字节不足8位时需补0,记录填充位数
> 引用[1]提到遍历时动态生成编码:左孩子加"0",右孩子加"1",最终叶子节点存储完整编码[^1]。引用[4]说明译码过程:根据二进制码遍历哈夫曼树,0向左1向右,到达叶子节点输出字符[^4]。
### 相关问题
阅读全文
相关推荐



















