1.Java数据结构:树(Tree)
2.数据结构—哈夫曼树和哈夫曼编码介绍以及Java实现案例
Java数据结构:树(Tree)
在计算机科学领域,树ja树树是码j码一种重要的抽象数据类型或数据结构。它由有限个节点组成,形结节点之间存在层次关系。构代这种结构类似于倒挂的树ja树树,根节点在上,码j码键盘鼠标同步源码叶节点在下。形结树具有以下特点:
为什么需要树?因为树结合了有序数组和链表的构代优点。在树中查找数据项的树ja树速度与有序数组相当,而插入和删除数据项的码j码速度则与链表相似。
在有序数组中插入数据项的形结过程较为缓慢。例如,构代使用二分查找法可以在有序数组中快速查找特定值。树ja树然而,码j码在有序数组中插入新数据项时,形结需要移动大量数据项以腾出空间,这导致效率低下。
在链表中,插入和删除操作非常迅速,但查找操作较为缓慢。由于链表不能直接访问某个数据项,必须从头开始逐个访问,因此查找速度较慢。
作为抽象数据类型,主力平均成本源码树至少要支持以下基本方法。树可以使用数组实现,其中节点的位置对应于其在树中的位置。树也可以使用链表实现,节点之间通过链式引用连接。
树的遍历是指按照某种次序访问树中的节点,每个节点恰好访问一次。常见的遍历算法包括前序遍历、后序遍历和层次遍历。
数据结构—哈夫曼树和哈夫曼编码介绍以及Java实现案例
哈夫曼树原本是为哈夫曼编码服务的一种数据结构,又称最优二叉树,哈夫曼编码常被使用在数据的压缩和解压缩技术之中。本文详细介绍了哈夫曼树的概念,并且提供了Java实现,最后又介绍了哈夫曼编码。1 哈夫曼树1.1 哈夫曼树简介哈夫曼树:给定N个权值作为N个叶子节点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
以下图为例,spring源码包是什么先说明几个概念:
权:
赋予某个实体的一个量,是对实体的某个或某些属性的数值描述。在数据结构中,实体有节点(元素)和边(关系)两大类,所以对应有节点权和边权。
路径:
在一棵树中,一个结点到另一个结点之间的通路,称为路径。上图,从根结点到结点 h之间的通路就是一条路径。
节点路径长度:
在一棵树中,从一个节点到另一个节点所经过的“边”的数量,被我们称为两个结点之间的路径长度;或者说路径上的分支数目称作路径长度。上图中从根结点到结点 h 的路径长度为3。
树的路径长度:
树的路径长度就是从树根到每一结点的路径长度之和。上图的树的路径长度为1+2+3+3+2+3+1+2+2+3=
节点的带权路径长度:
树的每一个结点,都可以拥有自己的“权重”(Weight),权重在不同的算法当中可以起到不同的作用。结点的带权路径长度,是指树的根结点到该结点的路径长度,和该结点权重的乘积。上图中h节点带权路径长度为3 * 3=9
树的带权路径长度:
树中所有叶子结点的带权路径长度之和。也被简称为WPL。安卓相机软件源码上图的树的带权路径长度为++++3 * 5=
1.2 哈夫曼算法哈夫曼树的构建方法被称为哈夫曼算法,其构建步骤为:
根据给定的n个权值{ w1,w2,...,wn}构成n棵二叉树的集合F={ T1,T2,...,Tn},其中每棵二叉树Ti中只有一个带权为wi根结点,其左右子树均为空。
在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
在F中删除这两棵树,同时将新得到的二叉树加入F中。
重复2和3步骤,直到F只含一棵树为止。这棵树便是哈夫曼树。
注意:
叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
最优二叉树中,权越大的叶子离根越近。
最优二叉树的WPL最小,但是形态不唯一。
1.3 案例有五个带权节点{ A5,B,C,D,E}。要求构建哈夫曼树。
先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E,医院管理系统源码eclipseB,D,C。
取头两个最小权值的结点作为一个新节点N1的两个子结点,取相对较小的是左孩子(实际上也可以不遵守,因为哈夫曼树是有多种形状的,但是WPL都是最小的),这里就是A为N1的左孩子,E为N1的右孩子,如下图,新结点的权值为两个叶子权值的和5+=。
将N1替换A与E,插入有序序列中,保持从小到大排列。即:N,B,D,C。
重复步骤2。将N1与B作为一个新节点N2的两个子结点。如下图,N2的权值=+=。
将N2替换N1与B,插入有序序列中,保持从小到大排列。即:N,D,C。
重复步骤2。将N2与D作为一个新节点N3的两个子结点。如下图,N3的权值=+=。
将N3替换N2与D,插入有序序列中,保持从小到大排列。即:C,N。
重复步骤2。将C与N3作为一个新节点T的两个子结点,如下图,由于T即是根结点,完成哈夫曼树的构造。
此时的上图二叉树的带权路径长度WPL=×1+×2+×3+×4+5×4=。这就是最短带权路径长度。
根据最优二叉树形态不唯一的性质和上面的一种形状,我们还可以写出下面形状的最优二叉树:
2 哈夫曼树的实现现提供哈夫曼树的Java实现,下面的类提供了两种构建方法,区别主要在一个排序使用Arrays.sort排序(注意由于是对象排序,它并非双轴快排),另一个使用了最小堆排序。它们的时间效率是不一致的。 实际使用时选取一种即可。
/***哈夫曼树简单实现*/publicclassHuffmanTree<E>{ /***外部保存根节点的引用*/privateBinaryTreeNode<E>root;/***内部节点**@param<E>节点数据类型*/publicstaticclassBinaryTreeNode<E>{ //节点数据Edata;//节点权重Stringweight;//左子结点BinaryTreeNode<E>left;//右子节点BinaryTreeNode<E>right;publicBinaryTreeNode(Edata,Stringweight){ this.data=data;this.weight=weight;}@OverridepublicStringtoString(){ return"Node{ "+"data="+data+",weight='"+weight+'\''+'}';}}/***根据指定的普通node节点集合构建哈夫曼树**@parambinaryTreeNodesnode节点集合,采用普通list集合*@param<E>节点数据类型*@return哈夫曼树*/publicstatic<E>HuffmanTree<E>build(List<BinaryTreeNode<E>>binaryTreeNodes){ //比较器Comparator<BinaryTreeNode<E>>comparator=(o1,o2)->newBigDecimal(o1.weight).subtract(newBigDecimal(o2.weight)).intValue();while(binaryTreeNodes.size()>1){ //手动对集合的节点按照权值大小从大到小进行排序binaryTreeNodes.sort(comparator);//移除并获取权值最小的两个节点BinaryTreeNode<E>left=binaryTreeNodes.remove(0);BinaryTreeNode<E>right=binaryTreeNodes.remove(0);//生成新节点,新节点的权值为两个子节点的权值之和BinaryTreeNode<E>parent=newBinaryTreeNode<>(null,newBigDecimal(left.weight).add(newBigDecimal(right.weight)).toString());//让新节点作为两个权值最小节点的父节点parent.left=left;parent.right=right;//将新节点加入到集合中binaryTreeNodes.add(parent);}//采用循环不断地执行上面的步骤,直到list集合中只剩下一个节点,最后剩下的这个节点就是哈夫曼树的根节点HuffmanTree<E>huffmanTree=newHuffmanTree<>();huffmanTree.root=binaryTreeNodes.remove(0);returnhuffmanTree;}/***根据指定的最小堆构建哈夫曼树**@parambinaryTreeNodesnode节点集合,采用最小堆*@param<E>节点数据类型*@return哈夫曼树*/publicstatic<E>HuffmanTree<E>build(PriorityQueue<BinaryTreeNode<E>>binaryTreeNodes){ while(binaryTreeNodes.size()>1){ //移除并获取权值最小的两个节点BinaryTreeNode<E>left=binaryTreeNodes.poll();BinaryTreeNode<E>right=binaryTreeNodes.poll();//生成新节点,新节点的权值为两个子节点的权值之和BinaryTreeNode<E>parent=newBinaryTreeNode<>(null,newBigDecimal(left.weight).add(newBigDecimal(right.weight)).toString());//让新节点作为两个权值最小节点的父节点parent.left=left;parent.right=right;//将新节点加入到最小堆中,自动排序binaryTreeNodes.add(parent);}//采用循环不断地执行上面的步骤,直到list集合中只剩下一个节点,最后剩下的这个节点就是哈夫曼树的根节点HuffmanTree<E>huffmanTree=newHuffmanTree<>();huffmanTree.root=binaryTreeNodes.poll();returnhuffmanTree;}/***获取根节点**@return根节点或者null-表示空树*/publicBinaryTreeNode<E>getRoot(){ returnroot;}}2.1 测试publicclassHuffmanTreeTest{ /*采用用普通集合和最小堆都行,最大的区别是它们的采用不同的排序算法,效率是不一致的*//***采用普通list作为临时存储节点数据的集合,因此我们需要手动排序*/@Testpublicvoidtest1(){ //采用普通list作为临时存储节点数据的集合,因此我们需要手动排序List<BinaryTreeNode<String>>binaryTreeNodes=newArrayList<>();//A5,B,C,D,EbinaryTreeNodes.add(newBinaryTreeNode<>("A","5"));binaryTreeNodes.add(newBinaryTreeNode<>("B",""));binaryTreeNodes.add(newBinaryTreeNode<>("C",""));binaryTreeNodes.add(newBinaryTreeNode<>("D",""));binaryTreeNodes.add(newBinaryTreeNode<>("E",""));HuffmanTree<String>huffmanTree=HuffmanTree.build(binaryTreeNodes);BinaryTreeNode<String>root=huffmanTree.getRoot();System.out.println(root);}/***采用最小堆--priorityQueue作为临时存储节点数据的集合,priorityQueue的性质就是对集合的元素进行自动排序,因此我们不必手动排序*/@Testpublicvoidtest2(){ //采用最小堆--priorityQueue.作为临时存储节点数据的集合,priorityQueue的性质就是对集合的元素进行自动排序,我们只需要指定排序规则PriorityQueue<BinaryTreeNode<String>>priorityQueueBinaryTreeNodes=newPriorityQueue<>((o1,o2)->newBigDecimal(o1.weight).subtract(newBigDecimal(o2.weight)).intValue());priorityQueueBinaryTreeNodes.add(newBinaryTreeNode<>("A","5"));priorityQueueBinaryTreeNodes.add(newBinaryTreeNode<>("B",""));priorityQueueBinaryTreeNodes.add(newBinaryTreeNode<>("C",""));priorityQueueBinaryTreeNodes.add(newBinaryTreeNode<>("D",""));priorityQueueBinaryTreeNodes.add(newBinaryTreeNode<>("E",""));HuffmanTree<String>huffmanTree=HuffmanTree.build(priorityQueueBinaryTreeNodes);BinaryTreeNode<String>root=huffmanTree.getRoot();System.out.println(root);}}3 哈夫曼编码哈夫曼树的出现主要是为了哈夫曼编码服务的,哈夫曼编码有着非常广泛的应用,哈夫曼编码常被使用在数据的压缩和解压缩技术之中。
哈夫曼编码的基本思想是以字符的使用频率作为权,构造一棵哈夫曼树,然后利用哈夫曼树对字符进行编码。这棵哈夫曼树,是将所要编码的字符作为叶子结点,该字符在文件中的使用频率作为叶子结点的权,以自底向上的方式,通过n-1次合并运算后构造出一棵树,权值越大的叶子离根越近。
比如我们有一段文字内容为“BADCADFEED”要网络传输给别人,显然用二进制的数字(0和1)来表示是很自然的想法。我们现在这段文字只有六个字母ABCDEF,那么我们可以用相应的二进制数据表示。
这样真正传输的数据就是编码后的“”,对方接收时可以按照3位一分来译码。如果一篇文章很长,这样的二进制串也将非常的可怕。而且事实上,不管是英文、中文或是其他语言,字母或汉字的出现频率是不相同的,比如英语中的几个元音字母“ae i o u”,中文中的“的 了 有 在”等汉字都是频率极高。
假设六个字母的频率为A ,B 8,C ,D,E ,F 5,合起来正好是%。那就意味着,我们完全可以重新按照哈夫曼树来规划它们。下左图为构造哈夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的哈夫曼树。
此时,我们对这六个字母用其从树根到叶子所经过路径的0或1来编码,可以得到下表所示这样的定义。
我们将文字内容为“BADCADFEED”再次编码,对比可以看到结果串变小了。
原编码二进制串:(共个字符)
新编码二进制串:(共个字符)
也就是说,我们的数据被压缩了,节约了大约%的存储或传输成本。随着字符的增加和多字符权重的不同,这种压缩会更加显出其优势。
当我们接收到这样压缩过的新编码时,我们应该如何把它解码出来呢?
编码中非0即1,哈夫曼编码是可变字长编码(VLC)的一种。但是如果长短不等的话其实是很容易混淆的,所以若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码又称做前缀编码。
按照上表设计的编码就不存在容易与、混淆的“”和“”编码。实际上哈夫曼编码就是一种前缀编码。
但是在解码时,还是要用到哈夫曼树,即发送方和接收方必须要约定好同样的哈夫曼编码规则。
当我们接收到时,由约定好的哈夫曼树可知,得到第一个字母是B,接下来意味着第二个字符是A,如下图所示,其余的也相应的可以得到,从而成功解码。
一般地,设需要编码的字符集为{ d1,d2,...,dn},各个字符在电文中出现的次数或频率集合为{ w1,w2,...,wn},以d1,d2,...,dn作为叶子结点,以w1,w2,...,wn作为相应叶子结点的权值来构造一棵哈夫曼树。规定哈夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是哈夫曼编码。
作者:刘Java
2024-12-22 14:32
2024-12-22 13:53
2024-12-22 13:41
2024-12-22 12:58
2024-12-22 12:53
2024-12-22 12:47