zoukankan      html  css  js  c++  java
  • 树的应用

    在之前已经对二叉树进行了详细的学习,这次会升级二叉树,就比之前学习的二叉树要更加的复杂麻烦了,涉及到左悬右悬的概念了。

    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;
            }
        }
    }

    这种思想用一个具体的二叉树再来看一下左旋转:

    知道了左旋转的实现之后,右旋转就比较简单了,这个在下一次再继续学习。

  • 相关阅读:
    python目录
    Django的认证系统
    Django logging配置
    Django 中间件
    Django Form和ModelForm组件
    AJAX
    Django中ORM介绍和字段及字段参数
    Django ORM那些相关操作
    Python中应该使用%还是format来格式化字符串?
    利用Python win32api 模拟键盘输入,打开浏览器,实现网页全屏,并移到第二屏幕
  • 原文地址:https://www.cnblogs.com/webor2006/p/11610034.html
Copyright © 2011-2022 走看看