注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明
目录
前言
今天是Huffman树内容的最后一天,通过强两天的学习,我们已经能构建出一个Huffman树,并且也明白了为什么要采用贪心的构造以及其原理,具体大家可见:
日撸 Java 三百行(28 天: Huffman编码实现原理与代码基本准备)_LTA_ALBlack的博客-CSDN博客注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明https://blog.csdn.net/qq_30016869/article/details/124037897日撸 Java 三百行(29 天: Huffman树的建立)_LTA_ALBlack的博客-CSDN博客注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明
https://blog.csdn.net/qq_30016869/article/details/124056865
一、基于Huffman树的自底向上编码及其实现的代码
1、逐个信息元编码
我们构造出Huffman树不是为了好看的,而是其能实实在在能去解决问题的。首先一点,我们要会去根据Huffman树去设计编码。
昨天我们构造Huffman树时根据的逻辑是由Huffman树的分支来模拟二元逻辑0-1从而设计编码,具体实现上来说我们可以定义左分支为0右分支1或者反过来,这里我们依前者而来,得到这样的Huffman树:
依据路径无前缀的特性构造出来的Huffman树,在具体编码时也需要依据路径信息构成编码。因此我们可以提取路径中分支的选择来构造编码中的0-1,这里我们采用自底向上的思路,通过遍历node数组,依次取出每个叶子,从叶子开始想顶遍历(因为树是一个一对多的结构,所以一个叶子向上的路径唯一,因此选择的编码结果也唯一)这里我直接放出代码:
/**
*********************
* Generate codes for each character in the alphabet.
*********************
*/
public void generateCodes() {
huffmanCodes = new String[alphabetLength];
HuffmanNode tempNode;
for (int i = 0; i < alphabetLength; i++) {
tempNode = nodes[i];
// Use tempCharCode instead of tempCode such that it is unlike
// tempNode.
// This is an advantage of long names.
String tempCharCode = "";
while (tempNode.parent != null) {
if (tempNode == tempNode.parent.leftChild) {
tempCharCode = "0" + tempCharCode;
} else {
tempCharCode = "1" + tempCharCode;
} // Of if
tempNode = tempNode.parent;
} // Of while
huffmanCodes[i] = tempCharCode;
System.out.println("The code of " + alphabet[i] + " is " + tempCharCode);
} // Of for i
}// Of generateCodes
alphabetLength表示了信息元的个数,其数目与叶子个数是对应的,因此通过遍历node数组的前alphabetLength个元素以访问叶子。
while循环中我们以tempNode为工作结点,通过tempNode = tempNode.parent不断向根的方向迭代,这个过程中以tempNode.parent为下一个父级,若满足当前tempNode是tempNode.parent的左儿子,那么当前tempNode就要向右上去靠近自己的父级,从父级的角度来看,tempNode结点是向左的分支,于是编码为0。右孩子同理。这里其实也解释了为什么我建立的不是单一的带权链树而是三叉链表的原因:我们采用的自底向上的编码方案。
这里要记住,我们进行的向上回溯,所以获得的编码信息应当是编码的前面部分,所以总是把获得的编码加入到已经找到的编码的前端。最后将每个编码信息存放在huffmanCodes之中(下标对应的)
试着分析复杂度,假设当前每个叶子所在的高度分别为:,为了方便我们假设其平均树高为
,中叶子树为
,那么其复杂度就为
。
那么按到这种思想,我们上面这颗Huffman树的的编码表如下如所示:
2、替换源信息
完成了每个字符的编码工作,下面就是将我们的编码逐一代替原字符,形成对原完成信息(字符串)的编码替换工作:
/**
*********************
* Encode the given string.
*
* @param paraString The given string.
*********************
*/
public String coding(String paraString) {
String resultCodeString = "";
int tempIndex;
for (int i = 0; i < paraString.length(); i++) {
// From the original char to the location in the alphabet.
tempIndex = charMapping[(int) paraString.charAt(i)];
// From the location in the alphabet to the code.
resultCodeString += huffmanCodes[tempIndex];
} // Of for i
return resultCodeString;
}// Of coding
编码信息是按照我们构造的下标逐一存放在huffmanCodes之中的,所以说在遍历信息源逐一取出字符时,我们需要查阅这个字符在我们自定义的下标序号体系中的什么的位置。于是我们利用了charMapping(从字符到alphabet下标的映射表)实现这个过程:
信息源遍历 -> 取得信息元(字符) -> 通过映射哈希 -> 得到alphabet中这个字符的下标 -> 通过huffmanCodes查到其编码。
二、基于Huffman树的自顶向下解码及其实现的代码
1.Huffman树的不唯一性
接收双发若要采用成功发送并接收,那么必须采用同一颗Huffman树进行编码解码,而不仅仅是相同WPL的Huffman树,因为Huffman树会因为构造的选择不同而呈现不同的形状和结构,但是WPL是不变的。这就是Huffman树的不唯一性。上
面构造的Huffman树在一次选择最小的时候,可以选择g和e也可以选择g和c,这两种抉择会直接导致e和c的编码长度出现偏差,这是一种情况。另一种就是我们编码时,选出两个最小的结点后,这俩结点链入父级后,是较大的在左还是较小的在左?万一相等了这么抉择?这些判断可能会因为不同的编程而出现偏差。
比如下面这颗Huffman树也是根据这个编码表的信息元与权构造出来树。但是在c与e的选择上就与我们刚刚构造到树不同,而且总权为10的分支结点的左右儿子顺序也与刚刚不同,但是这样的树的WPL却是与原来的Huffman树完全一致的。最终的编码存在一定的差异。
2.通过Huffman树解码
解码的具体过程不可能给你离散的逐个信息元的变长二进制编码,其肯定是给你全部信息的二进制合并的形式。所以这部分的难点在于从一连串二进制信息中按照Huffman树中不同信息元的路径长度不同进行信息进行挑选。
/**
*********************
* Decode the given string.
*
* @param paraString The given string.
*********************
*/
public String decoding(String paraString) {
String resultCodeString = "";
HuffmanNode tempNode = getRoot();
for (int i = 0; i < paraString.length(); i++) {
if (paraString.charAt(i) == '0') {
tempNode = tempNode.leftChild;
System.out.println(tempNode);
} else {
tempNode = tempNode.rightChild;
System.out.println(tempNode);
} // Of if
if (tempNode.leftChild == null) {
System.out.println("Decode one:" + tempNode);
// Decode one char.
resultCodeString += tempNode.character;
// Return to the root.
tempNode = getRoot();
} // Of if
} // Of for i
return resultCodeString;
}// Of decoding
但是好在我们使用的是Huffman树,这个过程并没有那么难:Huffman编码之前不互为前缀,所以只需不断地从根开始,按照0-1的不同不断向下遍历左右儿子即可,若遍历到叶子(不存在左右儿子的任意一个就到了叶子了,因为Huffman树没有度为1结点),则取出叶子中的信息元(字符)即实现二进制到字符的解码过程。然后,我们无法再向下遍历了,那么重新从根开始遍历即可。
三、其他代码以及模拟测试
到这里,我们的Huffman的代码实现终于是完全完成了,这里我在贴上几个用于数据测试的代码:
基于我们创建的Huffman结点的前序遍历:
/**
*********************
* Pre-order visit.
*********************
*/
public void preOrderVisit(HuffmanNode paraNode) {
System.out.print("(" + paraNode.character + ", " + paraNode.weight + ") ");
if (paraNode.leftChild != null) {
preOrderVisit(paraNode.leftChild);
} // Of if
if (paraNode.rightChild != null) {
preOrderVisit(paraNode.rightChild);
} // Of if
}// Of preOrderVisit
数据测试部分这次不一次性全部放出代码,因此需要测试的内容有些多,首先给出信息源字符流(其实就是txt文档)。这里的文档我们其实是在仿造本篇的第一个Huffman树,从而来看我们得到的输出结果是否满足这个树。
Huffman tempHuffman = new Huffman(
"D:\\eclipse-workspace\\Java self-study\\src\\datastructure\\tree\\HuffmanInput.txt");
之后提取信息中的内容完善数据结构,构建Huffman树,获取根结点,这部分代码在我第29天的内容中。这里输出的charMapping有这么多的-1的原因是因为我们将其初始化为-1了,同时因为其要保证ASCII的全映射,所以其有256多项。
tempHuffman.constructAlphabet();
tempHuffman.constructTree();
HuffmanNode tempRoot = tempHuffman.getRoot();
之后来验证今天代码的内容,首先通过前序遍历序以及上面建立Huffman的子节点选择时我们输出的辅助信息,把这颗树画出来。(可得,画出的树与我们之前假设的树完全一致)
System.out.println("The root is: " + tempRoot);
System.out.println("Preorder visit:");
tempHuffman.preOrderVisit(tempHuffman.getRoot());
System.out.println("");
之后是验证我们的编码与解码效果。这里我们也贴出我们预想的编码表(我们编码的目标是abceg,编码权值来源于aaaaabbbbccceeegg)
tempHuffman.generateCodes();
String tempCoded = tempHuffman.coding("abceg");
System.out.println("Coded: " + tempCoded);
String tempDecoded = tempHuffman.decoding(tempCoded);
System.out.println("Decoded: " + tempDecoded);
编码与解码完全符合预期。
总结
今天完成了Huffman编码的最后任务,至此Huffman树的内容就完结了。通过这次编码希望无论是我还是诸位阅读者都能对于树形结构在编码相关应用中的重要程度,深深理解树这种结构的关键。
当然,今天我们还是继续看有些代码有没有其他优化可能:今日编写的基于Huffman树的编码思路是从每个叶子开始逐步向上遍历直到发现根为止的逆向遍历。通过上文解释,这种逆向遍历的复杂度为 (这里的
代表了平均树高,
代表了叶子数目)。
下面我的想法是,既然能逆向,自然正向也应该可以的:从根节点开始自顶向下的进行遍历,遍历过程中维护一个字符串的变长,一旦遇到叶子节点,就将此维护的字符串赋予给这个叶子节点对应的信息元,以此完成编码。下面是代码:
/**
*********************
* Using the pre-Order to get code.
*********************
*/
public void preOrderCode(HuffmanNode curHuffmanNode, String codeString) {
if (curHuffmanNode.leftChild == null) {
int index = charMapping[curHuffmanNode.character];
huffmanCodes[index] = codeString;
} // Of if
if (curHuffmanNode.leftChild != null) {
preOrderCode(curHuffmanNode.leftChild, codeString + "0");
} // Of if
if (curHuffmanNode.rightChild != null) {
preOrderCode(curHuffmanNode.rightChild, codeString + "1");
} // Of if
}
/**
*********************
* Generate codes for each character in the alphabet with pre-Order.
*********************
*/
public void generateCodesWithPreOrder() {
huffmanCodes = new String[alphabetLength];
preOrderCode(this.getRoot(), "");
}
因为这个算法本质是树前序遍历的改造,所以这个算法其实是对树中所有结点的访问,假设树的结点个数为M,那么总的复杂度就是。根据Huffman树的特性,这里应该有
,那么复杂度也可以写作
。再与原算法的
比较一番可以基本得出一个可靠的结论,在Huffman树的平均高度高于2时,我们改进的算法在耗时上是要优于原算法的,当然,评价高度低于2的Huffman树叶也没什么使用之必要。
从复杂度的量级上分析的话近似可以写作
,是线性级的;而
中的
并非常量,因此此算法的复杂度基本上是平方量级。所以改进算法普遍来说是要优秀些的,而且这样操作之后我们就没有必要再使用指向父级的指针了。
造成这种问题的主要原因可能是因为采用从叶子开始逆向向上思路的话,部分路径出现了冗余重叠,而且这种重叠对于那些水平方向越相近的兄弟叶子们来说会更严重。