数据结构——树结构的应用(堆、赫夫曼树、赫夫曼编码、二叉排序树、平衡二叉树)

一、堆排序

堆排序利用这种数据结构进行排序,是一种选择排序,其时间复杂度是O(n*logn),时间复杂度很低,它是不稳定排序。

堆:它是具有以下性质的完全二叉树:每个节点的值都>=左右孩子节点的值,称为大顶堆;每个节点的值都<=左右孩子节点的值,称为小顶堆。

大顶堆的一个案例如下图:

小顶堆的一个案例如下图:

排序时,一般升序采用大顶堆,降序排序采用小顶堆。

堆排序的基本思想及代码详见文章:数据结构与算法——堆排序-CSDN博客

二、赫夫曼树/哈夫曼树

定义

1)给定n个叶子节点,其有n个权值,构造一颗二叉树,树的带权路径长度(wpl)达到最小,称为最优二叉树,也叫赫夫曼树、哈夫曼树。

2)赫夫曼树是wpl最短的树,其权值较大的节点离根较近。

几个概念

1) 路径路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。

2) 结点的带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积

3) 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。

4) WPL 最小的就是赫夫曼树

下图中第二幅图为赫夫曼树。、

赫夫曼树的创建思路

例如,将数列{13,7,8,3,29,6,1}转成一颗赫夫曼树。

基本步骤:

1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

2) 取出根节点权值最小的两颗二叉树

3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

4) 再将这颗新的二叉树,以根节点的权值大小再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树。

根据上述描述给出图解:

   

根据上述分析,下面给出代码。

代码实现:


public class HuffmanTree {
    public static void main(String[] args) {
        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        Node root = createHuffmanTree(arr);
        //测试
        preOrder(root);

    }

    //编写前序遍历方法
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("树是空树,无法遍历");
        }
    }

    //创建赫夫曼树的方法

    /**
     * @param arr 需要创建成赫夫曼树的数组
     * @return 返回 创建好后赫夫曼树的root节点
     */
    public static Node createHuffmanTree(int[] arr) {
        //数组元素取出,构造成Node里
        //1.遍历arr数组
        //2.将arr的每个元素构成一个Node
        //3.将Node放入到ArrayList中,便于管理
        List<Node> nodes = new ArrayList<>();
        for (int value : arr) {//遍历数组
            nodes.add(new Node(value));
        }

        //处理过程是循环过程
        while (nodes.size() > 1) {

            //排序 从小到大
            Collections.sort(nodes);//排序  Node实现了比较接口
            System.out.println("排序后 nodes = " + nodes);

            //取出根节点权值最小的两颗二叉树
            //(1)取出权值最小的节点
            Node leftNode = nodes.get(0);//作为左节点
            //(2)取出权值次小的节点
            Node rightNode = nodes.get(1);//作为右节点

            //(3)构建一颗新的二叉树
            Node parent = new Node((leftNode.value + rightNode.value));//左右权值相加
            parent.left = leftNode;
            parent.right = rightNode;

            //(4)从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)将parent加到Nodes
            nodes.add(parent);
        }
        //返回赫夫曼树的root节点
        return nodes.get(0);

    }
}

//创建节点类
//为了让Node对象支持排序 Collection集合排序
//让 Node实现 Comparable接口
class Node implements Comparable<Node> {
    int value;//节点权值
    Node left;//左子节点
    Node right;//右子节点

    //前序遍历方法
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }


    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    @Override
    public int compareTo(Node o) {
        return this.value - o.value;//升序排序
    }
}

结果:

可以看到,运行结果与图解分析一致。

三、赫夫曼编码

赫夫曼编码基本介绍

赫夫曼编码也叫哈夫曼编码(Huffman Coding)、霍夫曼编码,广泛用于数据文件压缩,赫夫曼 码是可变字长编码(VLC)的一种,称为最佳编码。

编码基本原理

1)定长编码

直接将字符转化为ASCII码,然后将ASCII码转为二进制,一个字节8位,这种定长方式存储数据时,长度比较长。如下图:

2)变长编码

按照各个字符出现的次数进行编码,出现次数越多,编码越小,这种编码方式会有多义性,可能会匹配到重复的编码。如下图:

3)赫夫曼编码

赫夫曼编码是前缀编码,其按照字符出现的次数,构建一颗赫夫曼树,次数作为权值,根据赫夫曼树,给各个字符编码,向左路径为0,向右路径为1,从而可以得到编码结果,这是一种无损压缩,原理入下:

传输的字符串

1) i like like like java do you like a java

2) d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数

3) 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值

步骤: 构成赫夫曼树的步骤:

1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

2) 取出根节点权值最小的两颗二叉树

3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理, 就得到一颗赫夫曼树。

4) 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 向右的路径为 1 , 编码如下: o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01 5) 按照上面的赫夫曼编码,"i like like like java do you like a java" 字符串对应的编码为 (注意这里使用无损压缩) 10101001101111011110100110111101111010011011110111101000011000011100110011110000110 01111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133 6) 长度为 : 133 说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9

如下图:

需要注意:赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是 一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

数据压缩——生成赫夫曼树

将前述文本“ i like like like java do you like a java”进行赫夫曼编码,压缩。基本思路入下图:

首先就是创建赫夫曼树。

数据压缩——生成赫夫曼编码

1) 生成赫夫曼树对应的赫夫曼编码 , 如下表:

=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011

2) 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下. 10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001010 00101111111100110001001010011011100...

数据解压

1) 前面得到了赫夫曼编码和对应的编码 byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77 , -57, 6, -24, -14, -117, -4, -60, -90, 28]

2) 现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java" 3) 思路:解码过程,就是编码的一个逆向。先将赫夫曼编码得到的字节数组转为二进制字符串,再进行反向赫夫曼查询,得到解码结果。

文件压缩

给一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。、

思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩

文件解压

将前面压缩的文件,重新恢复成原来的文件。

思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复

代码实现将在后续统一给出。

代码实现


public class HuffmanCode {
    public static void main(String[] args) {
        /*
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println("原始字符串的字节长度= " + contentBytes.length);//40

        //测试huffmanZip
        byte[] huffmanCodesBytes = huffManZip(contentBytes);
        System.out.println("压缩后的结果:" + Arrays.toString(huffmanCodesBytes));

        //测试 byteToBitString方法 和 decode方法
        byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
        System.out.println("解码后原来的字符串= " + new String(sourceBytes));

         */

        //测试文件压缩
//        String srcFile = "D:\\idea_java_projects\\A_data_structure\\qie.jpg";
//        String dstFile = "D:\\idea_java_projects\\A_data_structure\\qie1.jpg";
//        zipFile(srcFile, dstFile);
//        System.out.println("压缩文件成功");

        //测试解压文件
        String zipFile = "D:\\idea_java_projects\\A_data_structure\\qie1.jpg";
        String dstFile = "D:\\idea_java_projects\\A_data_structure\\qie2.jpg";
        unZipFile(zipFile, dstFile);
        System.out.println("解压成功");


        /*
        //分布过程;后序封装在一个方法里
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes= " + nodes);//nodes= [Node{data=32, weight=9},
                                               // Node{data=97, weight=5}...

        //测试 二叉树
        System.out.println("赫夫曼树 :");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //前序遍历
        huffmanTreeRoot.preOrder();

        //测试 是否生成了哈夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表:" + huffmanCodes);//{32=01, 97=100, 100=11000,
                                                               // 117=11001, 101=1110...

        //测试 哈夫曼编码的bytes
        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
        System.out.println("huffmanCodeBytes = " + Arrays.toString(huffmanCodeBytes));
                            //-88, -65, -56, -65, -56, ... 共17个  压缩率:(40-17)/40

         */
    }

    //解压文件
    public static void unZipFile(String zipFile, String dstFile) {
        //定义文件输入流
        InputStream is = null;
        //定义与输入流关联的对象流
        ObjectInputStream ois = null;
        //定义一个文件的输出流
        OutputStream os = null;
        try {
            //创建文件输入流
            is = new FileInputStream(zipFile);
            //创建和is关联的对象输入流
            ois = new ObjectInputStream(is);
            //读取byte数组 huffmanBytes
            byte[] huffmanBytes = (byte[]) ois.readObject();
            //读取赫夫曼编码表
            Map<Byte, String> huffmanCodes =  ( Map<Byte, String>)ois.readObject();
            //解码
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //将bytes写入到目标文件
            os = new FileOutputStream(dstFile);
            //写出数据到 dstFile文件
            os.write(bytes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }


    //压缩文件

    /**
     * @param srcFile 待压缩的文件全路径
     * @param dstFile 将压缩文件存储的路径
     */
    public static void zipFile(String srcFile, String dstFile) {
        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件输入流
        FileInputStream is = null;
        try {
            is = new FileInputStream(srcFile);
            //创建一个和源文件大小一样的byte[]
            byte[] b = new byte[is.available()];
            //读取文件
            is.read(b);
            //使用赫夫曼编码进行b 源文件 的压缩
            byte[] huffmanBytes = huffManZip(b);
            //创建文件的输出流,存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的 ObjectOutputStream
            oos = new ObjectOutputStream(os);
            //把赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes);
            //采用对象流方式写入赫夫曼编码,在恢复原文件时 更方便
            //一定要把赫夫曼编码 写入压缩文件
            oos.writeObject(huffmanCodes);//为了后面解压


        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                is.close();
                oos.close();
                os.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }


    //方法 完成对压缩数据的解码

    /**
     * @param huffmanCodes 赫夫曼编码表
     * @param huffmanBytes 赫夫曼编码得到的字节数组 [-88, -65, -56, -65,...]
     * @return 返回原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //1.先得到huffmanBytes对应的二进制的字符串 形式: 10101000...
        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  100->a
        Map<String, Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        System.out.println("map = " + map);//map = {000=108, 01=32, 100=97, 101=105, 11010=121,..

        //创建一个集合,存放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) {
                //10101000...
                //递增的取出  key    字符 '1' '0'
                //i不动,count移动,直到匹配到一个字符
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                if (b == null) {//说明没有匹配到
                    count++;
                } else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i直接移动到count位置
        }
        //for循环结束后,list中存放了所有的字符 i like ....的所有单个字符
        //把list中的数据放入到byte[]数组,并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;

    }


    //完成数据的解压
    /*
        思路:
        1.将huffmanCodeBytes 数组[-88, -65, -56, -65, -56, ...]
          先转成赫夫曼编码对应的二进制的字符串:1010100010111111110010001011111111001...
        2.将赫夫曼编码对应的二进制的字符串10101000...对照赫夫曼编码 转成:"i like like like java do you...
     */

    /**
     * 将一个byte转成一个二进制的字符串
     *
     * @param b
     * @param flag 标志 是否需要补高位,如果true,表示需要补高位 ,如果是最后一个字节,无需补位
     * @return 返回b对应的二进制的字符串, 按补码返回!!!!
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用变量保存b
        int temp = b;
        //如果b是正数,则需要补位,补高位
        if (flag) {
            temp |= 256;//按位或, 256:1 0000 0000  1:0000 0001 =或==> 1 0000 0001
        }
        String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制的补码

        if (flag) {
            return str.substring(str.length() - 8);//取最后的8位
        } else {
            return str;
        }
    }


    //将前面的方法封装起来,便于调用

    /**
     * @param bytes 原始字符串对应的字节数组
     * @return 编码处理 压缩后的数组
     */
    private static byte[] huffManZip(byte[] bytes) {
        //1.拿到节点 nodes= [Node{data=32, weight=9}...
        List<Node> nodes = getNodes(bytes);
        //2.创建赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //3.生成对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //4.根据赫夫曼编码进行压缩,得到哈夫曼编码的字节数组
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);

        return huffmanCodeBytes;

    }


    /**
     * @param bytes 接收字节数组
     * @return 返回 List 形式:Node[data=97, weight=5], Node[data=32,weight=9].....
     */
    private static List<Node> getNodes(byte[] bytes) {
        //创建ArrayList
        ArrayList<Node> nodes = new ArrayList<>();
        //遍历bytes,统计 每个byte出现的次数 --->map[key,value]
        HashMap<Byte, Integer> counts = new HashMap<Byte, Integer>();//数据-次数
        for (byte b : bytes) {
            Integer count = counts.get(b);//获取次数
            if (count == null) {//Map还没有字符数据
                counts.put(b, 1);//没有key就赋1
            } else {
                counts.put(b, count + 1);//覆盖  有key就++ 累计
            }
        }
        //把每个键值对 转成一个 Node对象,并加入到nodes集合
        //遍历map
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //通过List创建对应的赫夫曼树
    public static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗、第二颗最小的二叉树
            Node leftNode = nodes.get(0);//权值
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值,因为所有data都放在叶子节点
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;//挂节点

            //将已经处理的两个二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到 nodes
            nodes.add(parent);
        }
        //nodes最后的节点就是哈夫曼树的根节点
        return nodes.get(0);
    }

    //前序遍历
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1.将赫夫曼编码表存放在一个Map<Byte,String>形式
    //  形式如:32->01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要拼接路径,创建StringBuilder 存储 某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    /**
     * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,存放到huffmanCodes集合中
     *
     * @param node          传入节点
     * @param code          code代表路径,左0 右1
     * @param stringBuilder 拼接路径
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将传入的code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//node=null 不处理
            //判断当前节点是叶子节点还是非叶子节点
            if (node.data == null) {//非叶子节点
                //递归处理
                //向左
                getCodes(node.left, "0", stringBuilder2);
                //向右
                getCodes(node.right, "1", stringBuilder2);
            } else {//是叶子节点
                //表示找到了某个叶子节点的最后
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }

    //为了调用方便,重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    //编写方法,将一个字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]数组

    /**
     * @param bytes        原始字符串对应的byte[]
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼压缩后的Byte[]数组
     * 例如: String content = "i like like like java do you like a java";==>
     * byte[] contentBytes = content.getBytes();
     * 返回:字符串“101010001011111111001000101111111100100010111111110010010100110
     * 11100011100000110111010001111001010
     * 00101111111100110001001010011011100”
     * ====>对应的byte[]数组  huffmanCodeBytes ,即8位对应一个byte,放入到huffmanCodeBytes
     * huffmanCodeBytes[0]= 10101000(补码) =>
     * byte 【推导 10101000 - 1=> 10100111(反码) =>11011000(原码)=> -88】
     * huffmanCodeBytes[0]=-88
     */
    public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        //利用huffmanCodes将bytes转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();//拼接的那个133的串串
        //遍历bytes数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));//拼接的那个133的串串
        }
        //将1010100010111111110010001.....转成byte数组
        //统计返回的byte[]数组 huffmanCodeBytes的长度,便于创建数组
        int len;
        //或者一句话:len = stringBuilder.length() + 7) / 8
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建 存储压缩后的 byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {//每8位对应一个byte,所以步长8
            String strByte;
            if (i + 8 > stringBuilder.length()) {//不够8位
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);//将二进制式转为10进制
            index++;
        }
        return huffmanCodeBytes;
    }

}

//创建 Node 带 数据 和 权值
class Node implements Comparable<Node> {
    Byte data;//存放数据(字符)本身 如 'a'==> 97   ' '==> 32 空格
    int weight;//权值,数据/字符 出现的次数
    Node left;
    Node right;

    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(Node o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

代码运行结果:

原始字符串的字节长度= 40
nodes= [Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]
赫夫曼树 :
Node{data=null, weight=40}
Node{data=null, weight=17}
Node{data=null, weight=8}
Node{data=108, weight=4}
Node{data=null, weight=4}
Node{data=106, weight=2}
Node{data=111, weight=2}
Node{data=32, weight=9}
Node{data=null, weight=23}
Node{data=null, weight=10}
Node{data=97, weight=5}
Node{data=105, weight=5}
Node{data=null, weight=13}
Node{data=null, weight=5}
Node{data=null, weight=2}
Node{data=100, weight=1}
Node{data=117, weight=1}
Node{data=null, weight=3}
Node{data=121, weight=1}
Node{data=118, weight=2}
Node{data=null, weight=8}
Node{data=101, weight=4}
Node{data=107, weight=4}
生成的赫夫曼编码表:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
huffmanCodeBytes = [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

压缩后的结果:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}
解码后原来的字符串= i like like like java do you like a java
 

Process finished with exit code 0

注意事项

1) 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]

2) 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml 文件]

3) 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显

四、二叉排序树(BST)

二叉排序树基本介绍

二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点。

举例:数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:

二叉排序树的创建和遍历

思路:利用一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为:

按照二叉排序树的要求进行创建:二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

实现代码:


public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环添加节点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历
        System.out.println("中序二叉排序树");
        binarySortTree.infixOreder();
    }
}

//创建二叉排序树
class BinarySortTree {
    private Node root;

    public void add(Node node) {
        if (root == null) {
            root = node;//root若为空,直接指向node即可
        } else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOreder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉排序树为空,不能遍历");
        }
    }
}


//创建Node节点
class Node {
    int value;
    Node left = null;
    Node right = null;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加节点的方法
    //递归添加。满足二叉排序树的要求
    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);//递归向左添加
            }
        } else {//添加的节点值 > 当前节点的值
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);//递归向右添加
            }
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }
}

创建和遍历结果:

可以看到,结果刚好是升序排列。

二叉排序树的删除

二叉排序树的删除有3种情况,要求删除后仍有序,3种情况如下:

1) 删除叶子节点 (比如:2, 5, 9, 12)

思路:

(1) 先找要删除的结点 targetNode

(2) 找到 targetNode 的 父结点 parent

(3) 确定 targetNode 是 parent 的左子结点 还是右子结点

(4) 根据前面的情况来对应删除

     左子结点 parent.left = null           右子结点 parent.right=null

2) 删除只有一颗子树的节点 (比如:1)

思路:

(1) 先找要删除的结点 targetNode

(2) 找到 targetNode 的 父结点 parent

(3) 确定 targetNode 的子结点是左子结点还是右子结点

(4) targetNode 是 parent 的左子结点还是右子结点

(5) 如果 targetNode 有左子结点:

     5. 1 如果 targetNode 是 parent 的左子结点:parent.left = targetNode.left;

     5.2 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.left;

(6) 如果 targetNode 有右子结点

      6.1 如果 targetNode 是 parent 的左子结点 parent.left = targetNode.right;

      6.2 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.right

3) 删除有两颗子树的节点. (比如:7, 3,10 ,下图给出一个演示。

(1) 先找到要删除的结点 targetNode

(2) 找到 targetNode 的 父结点 parent

(3) 从 targetNode 的右子树找到最小的结点

(4) 用一个临时变量,将 最小结点的值保存 temp = 11

(5) 删除该最小结点

(6) targetNode.value = temp

上述就是各种情况下删除节点的分析,再编写代码时,首先编写方法:查找要删除的节点、以及查找要删除节点的父节点。接着编写删除节点的方法,首先:编写删除的节点是叶子节点的情况;再编写删除有两颗子树的叶子节点;最后编写只有一颗子树的情况(过五关斩六将)即可完成最终的删除功能。

注意:在删除只有一个子节点且没有父节点的节点时,如:

   

需要加上逻辑判断,待删节点的父节点为空,直接令左节点或右节点成为根节点即可。

root = targetNode.left;
root = targetNode.right;

代码实现

通过前述分析,所有代码实现如下:


public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环添加节点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历
        System.out.println("中序二叉排序树");
        binarySortTree.infixOreder();

        //测试 删除叶子节点
        binarySortTree.delNode(7);
        System.out.println("删除叶子节点后的结果:");
        binarySortTree.infixOreder();//测试结果正确

        System.out.println("root=" + binarySortTree.getRoot());
    }
}

//创建二叉排序树
class BinarySortTree {
    private Node root;

    public void add(Node node) {
        if (root == null) {
            root = node;//root若为空,直接指向node即可
        } else {
            root.add(node);
        }
    }

    public Node getRoot() {
        return root;
    }

    //中序遍历
    public void infixOreder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉排序树为空,不能遍历");
        }
    }

    //封装Node里的方法:search、searchParent方法
    //查找待删节点 targetNode
    public Node search(int value) {
        if (root == null) {
            return null;
        } else {
            return root.search(value);
        }
    }
    //查找父节点 parent
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }
    //删除节点
    public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            //1.先去找到要删除的结点 targetNode
            Node targetNode = search(value);
            //如果没有找到
            if (targetNode == null) {
                return;
            }
            //如果当前排序二叉树只有一个节点,且就是要删的
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }
            //找待删节点的父节点
            Node parent = searchParent(value);
            ///---
            //如果要删除的节点是叶子节点
            if (targetNode.left == null && targetNode.right == null) {
                //判断待删节点是其父节点的左子节点还是右子节点
                if (parent.left != null && parent.left.value == value) {//待删节点是其父节点的左子节点
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) {//待删节点是其父节点的右子节点
                    parent.right = null;
                }
                /-----
                //删除有两颗子树的节点
            } else if (targetNode.left != null && targetNode.right != null) {
                //从 targetNode 的右子树
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.value = minVal;
                -----///
                //删除只有1颗子树的节点
            } else {
                //待删节点有左子节点
                if (targetNode.left != null) {
                    if (parent != null) {
                        //targetNode 是 parent 的左子结点
                        if (parent.left.value == value) {
                            parent.left = targetNode.left;
                        } else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else {//待删节点有右子节点
                    if (parent != null) {
                        //targetNode 是 parent 的左子结点
                        if (parent.left.value == value) {
                            parent.left = targetNode.right;
                        } else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }


        }
    }
    //编写一个方法_ 删除有两颗子树的节点时,需要调用
    /**
     * 1.返回以 node 为根节点的二叉排序树的最小节点的值
     * 2.删除以 node 为根节点的二叉排序树的最小节点的值
     * @param node  传入的节点(当作二叉树的根节点)
     * @return  返回以 node 为根节点的二叉排序树的最小节点的值
     */
    public int delRightTreeMin(Node node) {
        Node target = node;
        //循环的查找左节点,就会找到最小值
        while (target.left != null) {
            target = target.left;
        }
        //这时target指向最小节点
        //删除最小节点
        delNode(target.value);
        return target.value;
    }
}


//创建Node节点
class Node {
    int value;
    Node left = null;
    Node right = null;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加节点的方法
    //递归添加。满足二叉排序树的要求
    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);//递归向左添加
            }
        } else {//添加的节点值 > 当前节点的值
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);//递归向右添加
            }
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //方法:查找要删除的节点
    /**
     * @param value 希望删除的节点的值
     * @return 如果找到,返回该节点,否则返回null
     */
    public Node search(int value) {
        if (value == this.value) {//找到待删
            return this;
        } else if (value < this.value) {//值<当前值,应当向左子树递归查找
            //如果左子节点为空,就不能再找了,返回空了,因为没有找到
            if (this.left == null) {
                return null;
            }
            return this.left.search(value);
        } else {//值>当前值
            if (this.right == null) {
                return null;
            }
            return this.right.search(value);
        }
    }
    //查找要删除节点的父节点
    public Node searchParent(int value) {
        if ((this.left != null && this.left.value == value) ||
                (this.right != null && this.right.value == value)) {
            return this;//当前节点就是要删除节点的父节点
        } else {
            //如果查找的值<当前节点的值,并且当前节点的左子节点不为空,则递归向左子树查找
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value);//递归向左子树查找
            } else if (value >= this.value && this.right != null) {//=防止相同值
                return this.right.searchParent(value);//递归向右子树查找
            } else {
                return null;//没有父节点
            }
        }
    }
}

五、平衡二叉树(AVL)

引出AVL

平衡二叉树AVL是二叉排序树BST的一种优化。给一个数列{1,2,3,4,5,6},创建一颗二叉排序树,如下:

可以看到,上述BST左子树全为空,看起来像一个单链表,其查询速度会明显降低,不能发挥BST的优势,每次查询还需要比较左子树,速度比单链表还慢,因此需要对BST进行平衡。

AVL基本介绍

1) 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树, 查询效率较高。

2) 特点:是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵 平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。

前2个图就是AVL。

若一个二叉排序树的(右子树高度-左子树高度)>1,则需要左旋转,形成AVL。

若一个二叉排序树的(左子树高度-右子树高度)>1,则需要右旋转,形成AVL。

下面给出具体分析。

左旋转方法

给一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}。思路如下图:

具体的一个操作过程如下分析:

为了生成AVL,左右子树的高度差值很关键,因此需要统计树的高度、左子树高度、右子树高度。

统计当前节点高度:

    //返回当前节点的高度,以该节点为根节点的树的高度
    public int height() {
        return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
    }

统计左子树高度:

    //返回左子树的高度
    public int leftHeight() {
        if (left == null) {
            return 0;
        }
        return left.height();
    }

统计右子树高度:

    //返回右子树的高度
    public int rightHeight() {
        if (right == null) {
            return 0;
        }
        return right.height();
    }

左旋转代码:

    //方法:左旋转
    //在添加完节点后,判断高度,从而确定是否需要左旋转
    private void leftRotate() {
        //创建新节点,以当前节点值为其值
        Node newNode = new Node(value);
        //把新节点左子树设为当前节点左子树
        newNode.left = left;
        //把新节点右子树设为当前节点右子树的左子树
        newNode.right = right.left;
        //当前节点值 赋值 为 右子节点的值
        value = right.value;
        //当前节点是右子树 设为 右子树的右子树
        right = right.right;
        //当前节点的左子树 设为 新节点
        left = newNode;
    }

在添加节点后,根据高度,判断是否需要左旋转:

Node类的add方法里增加:

        //当添加完一个节点后,如果 (右子树高度-左子树高度)>1,则需要左旋转
        if (rightHeight() - leftHeight() > 1) {
           leftRotate();//左旋转
        }

右旋转方法

对一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9,7,6}

右旋转的代码:

    //方法:右旋转
    private void rightRotate() {
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }

在添加节点后,根据高度,判断是否需要右旋转:

Node类的add方法里增加:

        //当添加完一个节点后,如果 (左子树高度-右子树高度)>1,则需要右旋转
        if (leftHeight() - rightHeight() > 1) {
            rightRotate();//右旋转
        }

双旋转方法

分析:在某些情况下,单旋转不能完成平衡二叉树的转换。

比如数列 int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树.

int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL。双旋转的思路如下:

将分析步骤逐步图解如下:

也就是说,在根节点进行左右旋转时,需要先对其子节点的左右节点的高度进行判断,从而进行子节点的旋转处理,防止旋转结束后不是平衡二叉树。

该模块代码实现如下:写在Node类的add方法里

        //当添加完一个节点后,如果 (右子树高度-左子树高度)>1,则需要左旋转
        if (rightHeight() - leftHeight() > 1) {
            //先要判断当前节点的右子树的 左子树的高度 > 右子树高度
            //要先右旋转
            if (right != null && right.leftHeight() > right.rightHeight()) {
                right.rightRotate();
                leftRotate();
            } else {
                leftRotate();//左旋转
            }
            return;//!!!!必须处理完后返回 因为已经平衡,后面不能再平衡处理了
        }
        //当添加完一个节点后,如果 (左子树高度-右子树高度)>1,则需要右旋转
        if (leftHeight() - rightHeight() > 1) {
            //先要判断当前节点的左子树的 左子树的高度 < 右子树高度
            //要先左旋转
            if (left != null && left.rightHeight() > left.leftHeight()) {
                //对当前节点 的 左子树 进行 左旋转
                left.leftRotate();
                //再对当前节点进行右旋转
                rightRotate();//右旋转
            } else {
                //直接右旋转即可
                rightRotate();
            }

        }
    

完整代码实现

前述左旋转、右旋转、双旋转的完整代码实现如下:


public class AVLTreeDemo {
    public static void main(String[] args) {
//        int[] arr = {4, 3, 6, 5, 7, 8};//测试左旋转
//        int[] arr = {10, 12, 8, 9, 7, 6};//测试右旋转
        int[] arr = {10, 11, 7, 6, 8, 9};//测试双旋转
        //创建一个AVLTree对象
        AVLTree avlTree = new AVLTree();
        //添加节点
        for (int i = 0; i < arr.length; i++) {
            avlTree.add(new Node(arr[i]));
        }
        //遍历
        System.out.println("中序遍历结果");
        avlTree.infixOreder();

        System.out.println("平衡处理~~~");
        System.out.println("当前树的高度:" + avlTree.getRoot().height());//
        System.out.println("左子树的高度:" + avlTree.getRoot().leftHeight());//
        System.out.println("右子树的高度:" + avlTree.getRoot().rightHeight());//
        System.out.println("当前根节点: " + avlTree.getRoot());//
    }
}

//创建AVL树
class AVLTree {
    private Node root;

    public void add(Node node) {
        if (root == null) {
            root = node;//root若为空,直接指向node即可
        } else {
            root.add(node);
        }
    }

    public Node getRoot() {
        return root;
    }

    //中序遍历
    public void infixOreder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉排序树为空,不能遍历");
        }
    }

    //封装Node里的方法:search、searchParent方法
    //查找待删节点 targetNode
    public Node search(int value) {
        if (root == null) {
            return null;
        } else {
            return root.search(value);
        }
    }

    //查找父节点 parent
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    //删除节点
    public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            //1.先去找到要删除的结点 targetNode
            Node targetNode = search(value);
            //如果没有找到
            if (targetNode == null) {
                return;
            }
            //如果当前排序二叉树只有一个节点,且就是要删的
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }
            //找待删节点的父节点
            Node parent = searchParent(value);
            ///---
            //如果要删除的节点是叶子节点
            if (targetNode.left == null && targetNode.right == null) {
                //判断待删节点是其父节点的左子节点还是右子节点
                if (parent.left != null && parent.left.value == value) {//待删节点是其父节点的左子节点
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) {//待删节点是其父节点的右子节点
                    parent.right = null;
                }
                /-----
                //删除有两颗子树的节点
            } else if (targetNode.left != null && targetNode.right != null) {
                //从 targetNode 的右子树
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.value = minVal;
                -----///
                //删除只有1颗子树的节点
            } else {
                //待删节点有左子节点
                if (targetNode.left != null) {
                    if (parent != null) {
                        //targetNode 是 parent 的左子结点
                        if (parent.left.value == value) {
                            parent.left = targetNode.left;
                        } else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else {//待删节点有右子节点
                    if (parent != null) {
                        //targetNode 是 parent 的左子结点
                        if (parent.left.value == value) {
                            parent.left = targetNode.right;
                        } else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }


        }
    }
    //编写一个方法_ 删除有两颗子树的节点时,需要调用

    /**
     * 1.返回以 node 为根节点的二叉排序树的最小节点的值
     * 2.删除以 node 为根节点的二叉排序树的最小节点的值
     *
     * @param node 传入的节点(当作二叉树的根节点)
     * @return 返回以 node 为根节点的二叉排序树的最小节点的值
     */
    public int delRightTreeMin(Node node) {
        Node target = node;
        //循环的查找左节点,就会找到最小值
        while (target.left != null) {
            target = target.left;
        }
        //这时target指向最小节点
        //删除最小节点
        delNode(target.value);
        return target.value;
    }
}

//创建Node节点
class Node {
    int value;
    Node left = null;
    Node right = null;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加节点的方法
    //递归添加。满足二叉排序树的要求
    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);//递归向左添加
            }
        } else {//添加的节点值 > 当前节点的值
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);//递归向右添加
            }
        }
        /旋转
        //当添加完一个节点后,如果 (右子树高度-左子树高度)>1,则需要左旋转
        if (rightHeight() - leftHeight() > 1) {
            //双旋转
            //先要判断当前节点的右子树的 左子树的高度 > 右子树高度
            //要先右旋转
            if (right != null && right.leftHeight() > right.rightHeight()) {
                right.rightRotate();
                leftRotate();
            } else {
                leftRotate();//左旋转
            }
            return;//!!!!必须处理完后返回 因为已经平衡,后面不能再平衡处理了
        }
        //当添加完一个节点后,如果 (左子树高度-右子树高度)>1,则需要右旋转
            //双旋转
        if (leftHeight() - rightHeight() > 1) {
            //先要判断当前节点的左子树的 左子树的高度 < 右子树高度
            //要先左旋转
            if (left != null && left.rightHeight() > left.leftHeight()) {
                //对当前节点 的 左子树 进行 左旋转
                left.leftRotate();
                //再对当前节点进行右旋转
                rightRotate();//右旋转
            } else {
                //直接右旋转即可
                rightRotate();
            }

        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //方法:查找要删除的节点

    /**
     * @param value 希望删除的节点的值
     * @return 如果找到,返回该节点,否则返回null
     */
    public Node search(int value) {
        if (value == this.value) {//找到待删
            return this;
        } else if (value < this.value) {//值<当前值,应当向左子树递归查找
            //如果左子节点为空,就不能再找了,返回空了,因为没有找到
            if (this.left == null) {
                return null;
            }
            return this.left.search(value);
        } else {//值>当前值
            if (this.right == null) {
                return null;
            }
            return this.right.search(value);
        }
    }

    //查找要删除节点的父节点
    public Node searchParent(int value) {
        if ((this.left != null && this.left.value == value) ||
                (this.right != null && this.right.value == value)) {
            return this;//当前节点就是要删除节点的父节点
        } else {
            //如果查找的值<当前节点的值,并且当前节点的左子节点不为空,则递归向左子树查找
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value);//递归向左子树查找
            } else if (value >= this.value && this.right != null) {//=防止相同值
                return this.right.searchParent(value);//递归向右子树查找
            } else {
                return null;//没有父节点
            }
        }
    }

    //返回当前节点的高度,以该节点为根节点的树的高度
    public int height() {
        return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
    }

    //返回左子树的高度
    public int leftHeight() {
        if (left == null) {
            return 0;
        }
        return left.height();
    }

    //返回右子树的高度
    public int rightHeight() {
        if (right == null) {
            return 0;
        }
        return right.height();
    }

    //方法:左旋转
    //在添加完节点后,判断高度,从而确定是否需要左旋转
    private void leftRotate() {
        //创建新节点,以当前节点值为其值
        Node newNode = new Node(value);
        //把新节点左子树设为当前节点左子树
        newNode.left = left;
        //把新节点右子树设为当前节点右子树的左子树
        newNode.right = right.left;
        //当前节点值 赋值 为 右子节点的值
        value = right.value;
        //当前节点是右子树 设为 右子树的右子树
        right = right.right;
        //当前节点的左子树 设为 新节点
        left = newNode;
    }

    //方法:右旋转
    private void rightRotate() {
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值