在之前已经对二叉树进行了详细的学习,这次会升级二叉树,就比之前学习的二叉树要更加的复杂麻烦了,涉及到左悬右悬的概念了。
Haffman树:
概念:
Haffman树主要是用在压缩技术上,来看一张图:
压缩之后的大小相比原文件要小了很多,其它的压缩思想下面简单简述一下:
先看一下代码:
这种代码在平常中会经常见到,那如果有一个班上的成绩占比图:
假设有100个人,那么根据上面的占比可以得到:
0~59有5个人、60~69占用15个人、70~79占用40个人、80~89占用30个人、90~100占用10个人。 那面来大致一下其要判断的总条件的个数:
0~59有5个人,只会执行这么一个条件:
所以5个人的数量的话,这个比例执行条件的总数量为5x1;
而60~69占用15个人,当要执行到这个区间断时,会要经过2个条件判断才行,如下:
所以15个人的数量的话,这个比例执行条件的总数量为15x2;
以此类推,整个100人要执行的总条件个数就为:5*1 + 15 * 2 + 40 * 3 + 30 * 4 + 10 * 5 = 5 + 30 + 120 + 120 + 50 = 325次。
那如果咱们优化一下条件的顺序呢?
下面再来看一下对于100人的一个总的条件执行次数:
这三个都只会执行一次条件:
而
它会执行2次条件:
最后一个则需要执行3次:
所此经过条件顺序改造之后的总执行条件个数就变为了:5 * 1 + 15 * 1 + 40 * 1 + 30 * 2 + 10 * 3 = 150,以上整个过程就是一个压缩的非常标准的启萌思想。依据它我们可以用二叉树来表示这个条件,如下:
其中第一个概念为“路径长度”,再解释一下:
所以整个结果点的路径长度为:
所以如图所示:
而对于改良之后的二叉树此时的路径长度则为:
所以如图中说明:
很明显右边的整个树的路径长度就变短了。
其中还有一段话,也来理解一下:
其中标红处道出了Haffman树的一个定义了,那啥叫“带树路径长度”呢?比如说拿这个图来说:
其中每个结点上标出了其权重值,类似于分数的一个比例:,而这棵树的带权路径长度就为30【权值】x1【总共要经过的路径数,也就是边数】+30x1+15x2+15x2+5x3+10x3。
其中树上标的数字代表节点出现的次数,如:
树的构造:
上面对Haffman树的特点有了一个理论化的了解,那么。。该树的生成规则是如何生成的呢?下面来看下:
对于字母“A B C D E”,我们知道如果在Java中的Unicode编码一个字符占三个字节,而在ASCII码中一个英文字符占1个字节、中文字符占2个字节,而1个字节就是8位二进制,这里拿ASCII码来存这五个字符的话则需要5x8=40位二进制才能存储,接下来咱们来优化一下这种存储方式,比如:
A用1表示、B表0表示、C用10表示、D用11表示、E用101表示,此时存储这5个字节就只需要101011101(9位)这么多位就可以存储它们了,大大的缩减的存储空间,但是!!存储是优化了,但是如何来还原则ABCDE这样的数据呢?所以这种存法是不可逆的,接着Haffman编码就诞生了。
下面先来看一下Haffman树的构造理论说明,来自于书上:
看了上面的说明还是一头雾水,下面依照上面的理论步骤一条条来动手来构造一下,咱们来领略一下Haffman树构造的完美过程,其数列就如图中所说:
也就是:
此时树的形态就变为了:
此时树的形态就变为:
以上就是Haffman树的整个完整构建过程。
Haffman编码:
先来看一下它的一个思想:
解释一下:
这样数据就大大的压缩了,而且也能被逆序正确的读出来的。
好,上面了解了Haffman树的思想之后,下面则用代码来实现一下,首先定义好树的结点结构:
接下来则开始定义一个方法来实现Haffman树,如下:
接下来开始具体实现一下,按着步骤一步步来:
1、首先需要将结点进行排序,所以排下就成了:
2、从有序数列中取出最小的两个结点:
接下来根据左右结点的权重相加生成一个它的父结点,如下:
接着将这两个结点从列表中删除,并用这个新结点替换之:
这样不断进行循环,则就将整个完整的Haffman树给建立起来啦,最后则将第一个结点做为整个树的根结点返回既可,如下:
好,接下来则需要打印生成的Haffman树,那如何打印呢?咱们按一层层的来打印,如下:
其中的N结果其实对于输出是没啥意义的,这个可以在输出时过滤掉,不过这里先不管过滤的事,先按这种规则来打印,具体如何实现呢,这里采用队列的方式,如下:
比较容易理解,下面来调用一下看下效果:
public class HaffmanTree { private TreeNode root; public TreeNode createHaffmanTree(ArrayList<TreeNode> list) { while (list.size() > 1) { //首先对结点进行排序 Collections.sort(list); //取出最小的2个结点 TreeNode left = list.get(list.size() - 1);//小的做为左结点 TreeNode right = list.get(list.size() - 2);//大的做为右结点 //根据左右结点生成一个新父结点 TreeNode parent = new TreeNode("p", left.weight + right.weight); left.parent = parent; right.parent = parent; parent.leftChild = left; parent.rightChild = right; //将左右结点从集合中移除掉,并将新生成的结点替换添加进去 list.remove(left); list.remove(right); list.add(parent); } //最后将第一个结点做为根节点 root = list.get(0); return root; } public void showHaffman(TreeNode root) { LinkedList<TreeNode> linkedList = new LinkedList<>(); linkedList.offer(root);//先将根结点入列 while (!linkedList.isEmpty()) { TreeNode treeNode = linkedList.pop(); System.out.println(treeNode.data); if (treeNode.leftChild != null) linkedList.offer(treeNode.leftChild); if (treeNode.rightChild != null) linkedList.offer(treeNode.rightChild); } } /** * 结点 */ public static class TreeNode<T> implements Comparable<TreeNode<T>> { private T data; private int weight;//权值 private TreeNode leftChild; private TreeNode rightChild; private TreeNode parent; public TreeNode(T data, int weight) { this.data = data; this.weight = weight; leftChild = null; rightChild = null; parent = null; } @Override public int compareTo(TreeNode<T> o) {//比较权重从大到小排 if (this.weight > o.weight) return -1; else if (this.weight < o.weight) return 1; return 0; } } @Test public void test() { ArrayList<TreeNode> list = new ArrayList<>(); TreeNode<String> node = new TreeNode<>("good", 50); list.add(node); list.add(new TreeNode("morning", 10)); list.add(new TreeNode("afternoon", 20)); list.add(new TreeNode("hell", 110)); list.add(new TreeNode("hi", 200)); HaffmanTree haffmanTree = new HaffmanTree(); haffmanTree.createHaffmanTree(list); haffmanTree.showHaffman(haffmanTree.root); } }
运行一下:
那下面咱们来手动梳理一下这个打印是否是对的:
按层次打印确实如输出的一样。
好接下来咱们来对此Haffman得到它某个结点的编码,其规则是左子树则用0,右子树则用1,如下:
比如我想找A结点,则是从根搜索起,会经过01,所以A的Haff编码为01:
以此规则我们就可以把每个Haffman树上的结点得到相应的编码,如下:
那代码怎么来实现呢,先来挼一下其实现思路,还是以图中的A为例,我们可以先逆着找:
结果为10,然后将其找到的按顺序存到Stack当中,接着再取出元素则就逆过来输出来,也就是10,好,有了思路之后下面来实现一下,不难:
public class HaffmanTree { private TreeNode root; public TreeNode createHaffmanTree(ArrayList<TreeNode> list) { while (list.size() > 1) { //首先对结点进行排序 Collections.sort(list); //取出最小的2个结点 TreeNode left = list.get(list.size() - 1);//小的做为左结点 TreeNode right = list.get(list.size() - 2);//大的做为右结点 //根据左右结点生成一个新父结点 TreeNode parent = new TreeNode("p", left.weight + right.weight); left.parent = parent; right.parent = parent; parent.leftChild = left; parent.rightChild = right; //将左右结点从集合中移除掉,并将新生成的结点替换添加进去 list.remove(left); list.remove(right); list.add(parent); } //最后将第一个结点做为根节点 root = list.get(0); return root; } public void showHaffman(TreeNode root) { LinkedList<TreeNode> linkedList = new LinkedList<>(); linkedList.offer(root);//先将根结点入列 while (!linkedList.isEmpty()) { TreeNode treeNode = linkedList.pop(); System.out.println(treeNode.data); if (treeNode.leftChild != null) linkedList.offer(treeNode.leftChild); if (treeNode.rightChild != null) linkedList.offer(treeNode.rightChild); } } /** * 获得某个结点的二进制编码 */ public void getCode(TreeNode node) { TreeNode tNode = node; Stack<String> stack = new Stack<>(); while (tNode != null && tNode.parent != null) { if (tNode.parent.leftChild == tNode) stack.push("0"); else if (tNode.parent.rightChild == tNode) stack.push("1"); tNode = tNode.parent; } System.out.print("node【" + node.data + "】的Haffman编码为: "); //然后从栈中读出来则为结果 while (!stack.isEmpty()) { System.out.print(stack.pop()); } } /** * 结点 */ public static class TreeNode<T> implements Comparable<TreeNode<T>> { private T data; private int weight;//权值 private TreeNode leftChild; private TreeNode rightChild; private TreeNode parent; public TreeNode(T data, int weight) { this.data = data; this.weight = weight; leftChild = null; rightChild = null; parent = null; } @Override public int compareTo(TreeNode<T> o) {//比较权重从大到小排 if (this.weight > o.weight) return -1; else if (this.weight < o.weight) return 1; return 0; } } @Test public void test() { ArrayList<TreeNode> list = new ArrayList<>(); TreeNode<String> node = new TreeNode<>("good", 50); list.add(node); list.add(new TreeNode("morning", 10)); list.add(new TreeNode("afternoon", 20)); list.add(new TreeNode("hell", 110)); list.add(new TreeNode("hi", 200)); HaffmanTree haffmanTree = new HaffmanTree(); haffmanTree.createHaffmanTree(list); haffmanTree.showHaffman(haffmanTree.root); System.out.println("---------------"); haffmanTree.getCode(node); } }
AVL树(平衡二叉树):
概念:
先看一下它的概念:
而二叉排序树咱们在之前https://www.cnblogs.com/webor2006/p/11575167.html已经学习过了,其规则就是小的结点都往左放,而大的结点都往右放,但是!!在数据量比较大的情况下,很容易出现连续的数字,比如说:
如果用二叉排序树来表示就会成这样:
有木有发现都集中到右侧去了,当大数据量时其查找性能肯定会大大的降低,但是如果想办法能将这棵树:
那性能也能保证,最终它也属于一棵排序二叉树,像这样的树其实就是属于平衡二叉树。根据概念的描述:
这是啥意思呢,这里拿上图来解释一下:
好,在了解了平衡二叉树的概念之后,下面来看几个树,来判断是否是平衡二叉树:
很明显不是的,比如说看"58",它的左子树的深度为2,而右子树木有,那它们之间的差值为2,不满足“每一个节点的左子树和右子树的高度差至多等于1”。
这个也很明显不是的,看下58这个结点。
这棵肯定属于平衡二叉树。
这棵也是的,同样属于一个平衡二叉树。
下面需要再了解两个概念:
平衡因子:
二叉树上节点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),举例说明:
对于这个结点的平衡因子为左子树深度2-右子树尝试2=0。
而对于上面这个结点的平衡因子为左子树深度2-右子树尝试0=2。
最小不平衡树:
下面来解释一下,如图中所示我们新插入的节点是37:
而接下来根据它来算各个结点的平衡因子,其中算到了58这个节点时,发现它的平衡因为2,大于1了:
那么最小不平衡子树就产生了,就是以这个58为根的子树则就叫做小小不平衡子树:
构建AVL树【开始涉及到左旋右旋的概念了】:
好,那AVL平衡二叉树的构建过程就变得很重要了,下面以这个数咱们来将其生成一棵AVL树:
按着排序二叉树的规则拿“3,2,1”这三数其二叉树就会成这样:
目前3的平衡因子就变成了2,就打破了平衡二叉树的平衡规则了,所以此时为了让其平衡不被打破,就需要对它进行一下旋转,那是左旋还是右旋呢?这里画一个三角形:
很明显我们应该是要将左斜边的节点往右斜边进行平衡,也就是:
那此时就需要对它进行右旋转了:
此时旋转之后就会变成:
经过右旋之后,一下又回到平衡树的状态了,接着继续添加第四个元素:
还是按照排序树的规则来添加,如下:
继续再添加第5个节点:
此时平衡二叉树又被打破了,接着当然又要开始旋转处理喽:
打破平衡的树为标三角形的地方,很明显应该是三角的右边往三角的左边来旋转,也就是:
此时就变成:
此时又回到了平衡状态了,接下来再来添加元素:
二叉树的形态就变为了:
此时平衡又被打破了,又得要做旋转处理了,此时发现跟之前旋转的形态有些变化了,不像之前的左旋和右旋都是没有相印子树的,比较好旋转,比如:
没有右子树,则往右旋。
没有左子树,则往左旋。
但是此时咱们这个没法旋了,为啥?
如果往左旋最终还是不平衡的,那此时怎么办呢?当不可旋转时,则可以往上延身一个结点来进行旋转,咱们这种情况其实也就是回到根结点来进行旋转,如下:
此时形态就会变为:
原来4的左子树的3结点就需要跟4进行比大小,由于它比4小,则应该放在4的左子树下,所以最后3就会放到:
此时又恢复平衡了,那此时有个问题,看图:
最终旋转时不管这么多,都会带着整个这个子树进行移动的,这里需要注意。
好,继续再添加元素:
此时又不平衡了:
这个比较简单,直接左旋转就成了:
所以:
此时又回到平衡状态了,接下来再继续添加:
还是平衡的,继续再添加:
此时又不平衡了,接下来这个形态有点麻烦了,因为旋转之后看:
往左旋转之后,看:
此时就不是一棵二叉排序树了。。那咋办呢?此时又有一个新规则出现了,此时先将这俩数通过右旋转交换一下:
那此时就变为:
此时再进行左旋转:
又恢复平衡啦,继续再添加:
此时又不平衡了,然后进行旋转呗:
发现9这个结点已经有了左右子数了,貌似没法进行旋转,这又是一种新规则,这样来搞:
往右一转,此时再基于它的父结点进行左旋转,最后就成了这样了:
呃,真的是最后这一个旋转有点懵逼,先了解一下吧,到时还得想办法变成代码,想想就觉得可怕。。其实关于旋转是有套路可循的,咱们先来对左旋转这个套路先将其代码化,先搭建个树的框架:
接下来就集中精力来实现左旋的代码,先来看一下左旋的示例图:
咱们则根据上图具体来实现一下:
public class AVLBTree<E extends Comparable<E>> { Node<E> root; int size; public class Node<E extends Comparable<E>> { E element; int balance = 0;//平衡因子 Node<E> left; Node<E> right; Node<E> parent; public Node(E element, Node<E> parent) { this.element = element; this.parent = parent; } } public void left_rotate(Node<E> x) { if (x != null) { Node<E> y = x.right;//先取到Y //1.把贝塔作为X的右孩子 x.right = y.left; if (y.left != null) { y.left.parent = x; } //2。把Y移到原来X的位置上 y.parent = x.parent; if (x.parent == null) { root = y; } else { if (x.parent.left == x) { x.parent.left = y; } else if (x.parent.right == x) { x.parent.right = y; } } //3.X作为Y的左孩子 y.left = x; x.parent = y; } } }
这种思想用一个具体的二叉树再来看一下左旋转:
知道了左旋转的实现之后,右旋转就比较简单了,这个在下一次再继续学习。