二叉树(应用)

在二叉树上篇中,我们学习二叉树的一些基本概念和遍历方式,然而在实际应用中,单纯的二叉树的使用并不多,更多的是从二叉树衍生出的树结构,如霍夫曼树二叉排序树平衡二叉树红黑树

1、霍夫曼树

定义:霍夫曼树是一种特殊的二叉树,给定n个权值作为n个叶子结点,若该树的带权路径长度达到最小,这样的二叉树称为最优二叉树,也称为霍夫曼树。即霍夫曼树就是带权路径长度WPL最小的二叉树,权值较大的点离根越近。

集合算法基础--二叉树(下)_java

树的路径长度是从树根到每一结点路径长度之和。

带权路径长度是路径的长度乘以结点上的权值

图中的带权路径长度为:

5*2+4*2+8*1=26

霍夫曼树的构造图解

  • 先对数组中的元素按从小到大进行排序
  • 集合算法基础--二叉树(下)_java_02


  • 取出最小的两个点4、5构造一颗二叉树,将根结点设为两个字节点的和,即4+5=9,并将根结点的值放入数组中重新排序,

集合算法基础--二叉树(下)_java_03

  • 再次取出最小的两个点构造二叉树,并将根结点的值重新放入数组中进行排序

集合算法基础--二叉树(下)_java_04

  • 依次类推,直至数组中仅剩一个元素,该元素就是霍夫曼树的根结点。


集合算法基础--二叉树(下)_java_05


代码实现

   public static Node  huffmanTree(int[] arr) {
        List<Node> nodes = new ArrayList<>();
        //将数组中的值转换成结点
        for (int vale : arr) {
            nodes.add(new Node(vale));
        }
        while (nodes.size() > 1) {

            //按权重的值从小到大排序
            Collections.sort(nodes);
            //取出权值较小的两个结点组成二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);

            //以这两个结点的权重相加的值组成他们的父节点,
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left=leftNode;
            parent.right=rightNode;
            //将此二叉树的两个子结点从list中移除,并将父节点加入到list中构成新的list
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }

3、霍夫曼编码

定义:霍夫曼编码是可变字长编码方式(VLC)的一种,该方法完全依据字符出现的概率来编码。

定长编码是采用ASCII对字符编码

字母ABCDEF
二进制字符000001010011100101

这样真正传输的数据就是编码后的”000001010011011100101“,对方接收时可以按照3位一分来译码,但是如果传输的数据量很大,这样的二进制串的数量将会是非常庞大。

霍夫曼是将每个字符出现的频率作为权值构造霍夫曼树,按照权值最小路径长度进行编码

假设六个字母的频率A:27,B:8,C:15, D:15, E:30 ,F:5

构建霍夫曼树:

集合算法基础--二叉树(下)_java_06

对从树根到叶子经过的路径的0或1来编码,左路径为0右路径为1,因此可得霍夫曼编码树为


字母ABCDEF
二进制字符01100110100111000

所以采用霍夫曼编码为:01100110100111000

将生成的霍夫曼的编码每8位转成一个byte[]={-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28}

压缩率:(40-17)/40=57%

Note:霍夫曼排序方法的同会造成霍夫曼编码的不同,但是wpl是一样的,都是最小的。

代码实现:

  • 构造霍夫曼树
   public static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node parentNode = new Node(null, leftNode.weight + rightNode.weight);
            parentNode.leftNode = leftNode;
            parentNode.rightNode = rightNode;
            //在集合中删除子结点并加入父结点
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parentNode);
        }
        return nodes.get(0);
    }
  • 根据霍夫曼树生成霍夫曼编码
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        stringBuilder2.append(code);
        if (node != null) {
            if (node.data == null) {
                //向左递归
                getCodes(node.leftNode, "0", stringBuilder2);
                //向右递归
                getCodes(node.rightNode, "1", stringBuilder2);
            } else {
                //表明是叶子结点
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }
  • 对霍夫曼编码进行压缩(每8位压缩成一个byte)
byte[]={-88, -65, -56, -65, -56, -65, -5577, -576, -24, -14, -117, -4, -60, -9028}
  private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
        StringBuilder stringBuilder=new StringBuilder();
        //遍历byte数组
        for(byte b:bytes){
            stringBuilder.append(huffmanCodes.get(b));
        }
        //煤8位转成一个byte
        int len ;
        if(stringBuilder.length()%8==0){
            len=stringBuilder.length()/8;
        }else {
            len=stringBuilder.length()/8+1;
        }
        //创建存储压缩后的数组
        byte[] huffmanCodeBytes=new byte[len];
        int index=0;
        for (int i=0;i<stringBuilder.length();i+=8){
            String strByte;
            if(i+8>stringBuilder.length()){
                strByte=stringBuilder.substring(i);
            }else {
                strByte=stringBuilder.substring(i,i+8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes,二进制每8位表示一个byte
            huffmanCodeBytes[index]= (byte) Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

4、霍夫曼解码

霍夫曼解码是将byte[]数组转成二进制字符串“1000100001..................”,然后对照霍夫曼表解出对应的字符串。

代码实现

  • 将byte数组转成二进制字符串
    /**
     * j将byte转成二进制字符串
     * @param flag 标志着是否需要补高位,为true时需要补高位,false时不需要
     * @param b    传入的byte
     * @return 对应的二进制字符串
     */

    private static String byteToBitString(boolean flag, byte b) {
        //将b转成int
        int temp = b;
        //如果是正数我们需要补高位
        if (flag) {
            temp |= 256;//与256按位或
        }
        String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制补码
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }

    }
  • 对照霍夫曼表生成对应的字符串"1000111000"===>"i like like like java do you like a java"
 private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //1、先得到huffmanBytes,对应的二进制字符串"1000111000"
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //将字符串按照指定的霍夫曼编码进行解码
        //将霍夫曼编码表进行调换,可以进行反向查询,即 a->100,
        Map<String, Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        //创建集合存放Byte
        List<Byte> list = new ArrayList<>();
        //i可以理解为索引扫描StringBuilder
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                //1010100000111.....
                //递增取出key
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                if (b == null) {//说明没有匹配到
                    count++;
                } else {
                    flag = false;
                }
            }
            list.add(b);
            i += count;
        }
        //当for循环结束后,list中就存放了"i like like like java do you like a java"
        //把list中的数据放入到byte[],并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

note:

  • 霍夫曼编码是采用字节来处理的
  • 文件中的字符重复率越高,使用霍夫曼编码效果越明显。

5、二叉排序树(BinarySortTree)

定义:在非叶子结点中,左子结点的值小于结点值,右子结点的值大于等于结点值,这样的二叉树称为二叉排序树。

集合算法基础--二叉树(下)_java_07

代码实现

  • 添加
    //添加结点
    public void add(Node node) {
        //判断添加的结点是否为空
        if (node == null) {
            return;
        }
        if (node.value < this.value) {
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        }

        if (node.value > this.value) {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }
    }
  • 删除(分三种情况)

    叶子结点的删除
    删除结点中只含有一个子树
删除的结点中含有两个子树

 public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            //1、找到要删除的目标结点
            Node targetNode = searchTargetNode(value);
            if (targetNode == null) {
                return;
            }
            //如果发现这颗二叉树只有一个结点
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }
            //2、找到要删除结点的父结点
            Node parentNode = searchParentNode(value);
            //如果要删除的结点为叶子结点
            if (targetNode.left == null && targetNode.right == null) {
                //判断targetNode是parentNode的左子结点还是右子结点
                if (parentNode.left != null && parentNode.left.value == value) {
                    parentNode.left = null;
                } else if (parentNode.right != null && parentNode.right.value == value) {
                    parentNode.right = null;
                }
            }else if(targetNode.left!=null&&targetNode.right!=null){
                //删除的结点含有两颗子树
                int minVal=delRightTreeMin(targetNode.right);
                targetNode.value=minVal;
            }else {
                //删除的结点含有一颗子树的
                //如果目标结点的子结点是左子结点
                if(targetNode.left!=null){
                    if(parentNode!=null){
                        if(parentNode.left.value==value){
                            parentNode.left=targetNode.left;
                        }else {
                            parentNode.right=targetNode.left;
                        }
                    }else {
                        root=targetNode.left;
                    }

                }else {
                    if(parentNode!=null){
                        if(parentNode.left.value==value){
                            parentNode.left=targetNode.right;
                        }else {
                            parentNode.right=targetNode.right;
                        }
                    }else {
                        root=targetNode.right;
                    }

                }
            }

        }
    }

6、平衡二叉树(AVL树)

定义:当向一个二叉排序树中顺序的添加元素时,这时的二叉排序树将会退化成链表,其查询效率就会下降,为了解决这样的问题,引入了一个平衡因子,规定为二叉排序树的左右子树的高度差不能大于1,这样的二叉树排序树称为平衡二叉树。

集合算法基础--二叉树(下)_java_08

那我们可以通过哪些方式对平衡二叉树修改呢?

6.1左旋转

(右子树高度-左子树高度)>1时,进行左旋转

  • 使用当前结点的值创建新的结点
  • 把新结点的左子树设置为当前结点的左子树
  • 把新结点的右子树设置为当前结点右子树的左子树
  • 把当前结点的值设置为右子结点的值
  • 把当前结点的右子树设置为当前结点的右子树的右子树
  • 把当前结点的左子树(左子结点)设置为新的结点
 private void leftRotate(){
        //使用当前根结点的值创建新的结点
        Node newNode= new Node(value);
        //把新结点的左子树设置为当前结点的左子树
        newNode.left=left;
        //把新结点的右子树设为当前结点右子树的左子树
        newNode.right=right.left;
        //把当前结点的值设为右子结点的值
        value=right.value;
        //把当前结点的右子树设置为当前结点右子树的右子树
        right=right.right;
        //把当前结点的左子树(左子结点)设置为新的结点
        left=newNode;
    }

6.2右旋转

(左子树高度-右子树高度)>1时,进行右旋转

  • 以当前结点的值创建新的结点
  • 把新结点的右子树设置为当前结点的右子树
  • 把新结点的左子树设置为当前结点的左子树的右子树
  • 把当前结点的值换为左子结点的值
  • 把当前结点的左子树设置为左子树的左子树
  • 把当前结点的右子(右子结点)设置为新的结点
 private void rightRotate(){
        Node newNode=new Node(value);
        newNode.right=right;
        newNode.left=left.right;
        value=left.value;
        left=left.left;
        right=newNode;
    }

6.3双旋转

  • 左旋转-->右旋转
   if(leftHeight()-rightHeight()>1){
            //如果当前结点的左子树的右子树高度大于当前结点左子树的左子树高度,则需要对当前结点的左子树进行坐旋转,然后对当前结点进行右旋转
            if(left!=null&&left.rightHeight()>left.leftHeight()){
                left.leftRotate();
                rightHeight();
            }
            rightRotate();
        }
    }
  • 右旋转-->左旋转
  if(rightHeight()-leftHeight()>1){
            if(right!=null&&right.leftHeight()>right.rightHeight()){
                rightRotate();
                leftRotate();
            }
            leftRotate();
            return;
        }

7、平衡二叉树(红黑树)

定义:红黑树也是一种平衡二叉树,也是在进行插入和删除操作时通过特定的操作保持二叉查找树的平衡,红黑树是一种含有红黑结点并能自平衡的二叉查找树,它有以下特点

  • 每个结点要么是黑色,要么是红色
  • 根结点是黑色
  • 每个叶子结点(Null)是黑色
  • 每个红色结点的两个子结点一定都是黑色,即从每个叶子到根的所有路径不能有连续的红色结点
  • 任意结点到叶子结点的路径都包含数量相同的黑结点

让我们先看一个标准的红黑树是什么

集合算法基础--二叉树(下)_java_09

以一个添加的例子来讲解红黑树

7.1插入结点56

集合算法基础--二叉树(下)_java_10


判断是否满足红黑树,首先检查是否满足红黑树的条件,很显然,插入结点56点满足红黑树

7.2插入结点51

集合算法基础--二叉树(下)_java_11


插入结点51后,不满足红色结点的子结点都是黑色,所以我们需要调整树结构,我们按照如下步骤来进行

  • 变色

首先把结点为49的改成黑色

改成黑色后,发现不满足任意结点到叶子结点包含数量相同的黑结点,所以将结点45改成红色

发现56-45-43红色相连,所以需要将56和43改成黑色结点

又发现又不满足任意结点到叶子结点包含数量相同的黑结点,调整结点68为黑色(逐渐往上修改,直到符合要求)

集合算法基础--二叉树(下)_java_12

最终调整完成后的树为:

集合算法基础--二叉树(下)_java_13

7.3插入结点65

先看原始图

集合算法基础--二叉树(下)_java_14


在上图中加入结点65

集合算法基础--二叉树(下)_java_15

我们会发现,结点64无论是黑色还是红色,都会违反红黑树的要求,这个就需要在变色的基础上进行旋转

关于旋转我们在AVL树已经讲解的很清楚了,我们可以直接用,这里不再讲解。