二叉树(顺序存储二叉树、线索化二叉树、赫夫曼树、二叉排序树、平衡二叉树)

这里我就不对树的一些基本名词做解释,有需要的伙伴可以去百度了解树的名词,比如结点、权值等。

树的遍历方式有三种,分别为前序遍历中序遍历后序遍历

在遍历中,我们主要是借助于递归来实现我们的遍历。

前序遍历:先输出父节点,再遍历左子树和右子树

中序遍历:先遍历左子树,在输出父节点,再遍历右子树

后序遍历:先遍历左子树,再遍历右子树,最后输出父节点。

 例如:如上图

前序遍历结果为:5  3  2  4  6

中序遍历结果为:2  3  4  5  6

后续遍历结果为:2  4  3  6  5

小结:主要是看父结点输出的顺序,就可以判断时哪个遍历顺序了。

这里就简单附上前中后序遍历的代码,不做过多的赘述。

/**
     * 前序遍历
     * 前序遍历:先输出父节点,再遍历左子树和右子树
     */
    public void preOrder() {
        System.out.println(this);//先输出父结点
        if (this.left != null) {//左边不为空
            this.left.preOrder();//向左递归
        }
        //向左遍历完,就到右边
        if (this.right != null) {
            this.right.preOrder();//向右递归
        }

    }

    /**
     * 中序遍历
     * 中序遍历:先遍历左子树,然后输出父结点,然后再遍历右子树
     */
    public void midOrder() {
        if (this.left != null) {//左边不为空
            //向左递归
            this.left.midOrder();
        }
        System.out.println(this);//输出父结点
        //向右遍历
        if (this.right != null) {
            this.right.midOrder();//向右递归
        }

    }

    /**
     * 后续遍历
     * 后续遍历:先遍历左子树,然后再遍历右子树,最后输出父结点
     */
    public void suffixOrder() {
        if (this.left != null) {//左边不为空
            //向左递归
            this.left.suffixOrder();
        }
        //向右遍历
        if (this.right != null) {
            this.right.suffixOrder();//向右递归
        }
        System.out.println(this);//输出父结点

    }

接下来我们进入正题。

目录

二叉树的查询、删除

顺序存储二叉树

线索化二叉树

赫夫曼树

二叉排序树(BST)

平衡二叉树(AVL)

总结


二叉树的查询、删除

二叉树:每个结点最多只能有两个子结点的一种形式成为二叉树。二叉树的子结点分为左节点和右节点。

满二叉树:如果该二叉树的所有叶子结点都在最后一层,并且结点总数=2^n -1,n为层数,我们称之为满二叉树。

(该图片来自于韩老师的图片)

完全二叉树:如果该二叉树的所有叶子结点都在最后一层或者倒数第二层,而且最后一层的叶子结点在左边连续,倒数第二层的叶子结点在有点连续,我们称为完全二叉树。

(该图片来自于韩老师的图片)

这里我们便要实现对二叉树的遍历、删除、查找操作;

这里的遍历,便是所谓的前、中、后序遍历,查找、删除也是借助与前、中、后遍历来实现的。

遍历上面已经提及,就不做赘述。

接下来我们来看二叉树的查找

这里我就以前序遍历查找来举例,中序遍历查找与后序遍历查找也类似,大家可以尝试自行完成,代码会贴出来。

 代码附上:

 /**
     * 前序遍历查找
     *
     * @param id 要查找的id
     * @return 如果null,则无该数据,如果有,返回结点对象
     */
    public Node preOrderSearch(int id) {
        if (this.id == id) {//此时找到对应的结点对象,直接返回当前对象
            return this;
        }
        Node temp = null;//记录结果
        if (this.left != null) {
            temp = this.left.preOrderSearch(id);//向左递归查找,将结果返回
        }
        if (temp != null) {
            //如果temp不为空,说明在左子树找到要查找的值,直接返回即可
            return temp;
        }
        if (this.right != null) {//当上述的temp没有被返回,说明左子树没有找到值,进入右子树
            temp = this.right.preOrderSearch(id);//向右递归查找
        }
        return temp;//当前递归结束,都返回temp。

    }

二叉树的删除:由于二叉树是单向指定的,有点类似于单链表,因此我们要对其删除,则需要找到删除结点的父结点,否则无法将其删除。

这里删除我们做一个规定

1)如果要删除的结点是叶子结点,则直接将其删除。

2)如果要删除的结点有两个子结点,我们就将该结点删除,以其左子结点代替该结点

3)如果要删除的结点只有一个子结点,我们就以其子结点代替该删除的结点

这里主要是找要删除结点的父结点。由于较为简单,不多加赘述,直接上代码。

/**
     * 删除叶子结点或者子树
     * 这里类似于单链表,我们需要定位到要删除结点的上一个结点
     *
     * @param id
     */
    public void delete(int id) {
        if (this.left != null) {//如果当前结点的左结点不为空
            if (this.left.id == id) {
                if (this.left.left == null && this.left.right == null) {//当前删除的结点为叶子结点时,直接将其删除
                    this.left = null;
                    System.out.println("删除成功");
                    return;
                } else if (this.left.left != null && this.left.right == null) {
                    //要删除的结点为子树,其右节点为空且其左节点不为空,则将其左节点代替其位置
                    this.left = this.left.left;
                    System.out.println("删除成功");
                    return;
                } else if (this.left.left == null && this.left.right != null) {
                    //要删除的结点为子树,其左节点为空且其右节点不为空,则将其右节点代替其位置
                    this.left = this.left.right;
                    System.out.println("删除成功");
                    return;
                } else {
                    //其左右节点都不为空,则将其左节点代替当前位置
                    this.left.left.right = this.left.right;
                    this.left = this.left.left;
                    System.out.println("删除成功");
                    return;
                }
            } else {
                //如果当前结点的左节点不是要删除的结点,则向左递归删除
                this.left.delete(id);
            }
        }
        if (this.right != null) {
            if (this.right.id == id) {
                if (this.right.left == null && this.right.right == null) {//当前删除的结点为叶子结点时,直接将其删除
                    this.right = null;
                    System.out.println("删除成功");
                    return;
                } else if (this.right.left != null && this.right.right == null) {
                    //要删除的结点为子树,其右节点为空且其左节点不为空,则将其左节点代替其位置
                    this.right = this.right.left;
                    System.out.println("删除成功");
                    return;
                } else if (this.right.left == null && this.right.right != null) {
                    //要删除的结点为子树,其左节点为空且其右节点不为空,则将其右节点代替其位置
                    this.right = this.right.right;
                    System.out.println("删除成功");
                    return;
                } else {
                    //其左右节点都不为空,则将其左节点代替当前位置
                    this.right.left.right = this.right.right;
                    this.right = this.right.left;
                    System.out.println("删除成功");
                    return;
                }
            } else {
                //如果当前结点的右结点不是要删除的结点,则向右递归删除
                this.right.delete(id);
            }
        }


    }

顺序存储二叉树

顺序存储二叉树:(顺序存储二叉树只考虑完全二叉树)顺序存储二叉树指的是二叉树与数组之间的相互转化。如下图所示即为一颗顺序存储二叉树。

顺序存储二叉树的特点:

1.顺序二叉树通常只考虑完全二叉树

2.第 n 个元素的左子节点为2  *  n  + 1

3.第 n 个元素的右子节点为2  *  n  + 2

4.第 n 个元素的父节点为(n-1) /  2

5.n:表示二叉树中的第几个元素(按0开始编号如图所示)

其实说白了,顺序存储二叉树就是一种数组的树形化,但是我们这里不创建结点来构建树,而是直接通过数组下标的方式,对其进行遍历。重点在于上述的左子结点、右子结点与父结点的公式

代码附上(包括前、中、后序查找):

package com.liu.tree;

/**
 * @author liuweixin
 * @create 2021-09-14 15:06
 */
//顺序存储二叉树
public class ArrBinaryTree {
    private int[] arr;

    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    public static void main(String[] args) {
        int[] arr = new int[7];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i + 1;
        }
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder();//前序遍历: 1 2 4 5 3 6 7
        System.out.println("======================");
        arrBinaryTree.midOrder();//中序遍历:4 2 5 1 6 3 7
        System.out.println("======================");
        arrBinaryTree.suffixOrder();//后序遍历:4 5 2 6 7 3 1
    }

    /**
     * 封装重载前序遍历存储二叉树
     */
    public void preOrder() {
        preOrder(0);
    }
    /**
     * 封装重载中序遍历存储二叉树
     */
    public void midOrder() {
        midOrder(0);
    }
    /**
     * 封装重载后序遍历存储二叉树
     */
    public void suffixOrder() {
        suffixOrder(0);
    }

    /**
     * 前序遍历顺序存储二叉树
     *
     * @param index 数组的下标
     */
    public void preOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("该顺序存储二叉树为空");
            return;
        }
        //先输出当前节点
        System.out.println(arr[index]);
        //向左递归
        if ((2 * index + 1) < arr.length) {//当该下标值小于数组的长度,说明该处有值
            preOrder(2 * index + 1);//向左递归
        }
        if ((2 * index + 2) < arr.length) {
            //判断其右子结点是否有值
            preOrder(2 * index + 2);
        }
    }

    /**
     * 中序遍历顺序存储二叉树
     * @param index  数组的下标
     */
    public void midOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("该顺序存储二叉树为空");
            return;
        }
        //先向左递归-->再到输出结点-->向右递归
        if ((2 * index + 1) < arr.length) {
            midOrder(2 * index + 1);
        }
        System.out.println(arr[index]);
        if ((2 * index + 2) < arr.length) {
            midOrder(index * 2 + 2);//向右递归
        }
    }
    /**
     * 后续遍历顺序存储二叉树
     * @param index  数组的下标
     */
    public void suffixOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("该顺序存储二叉树为空");
            return;
        }
        //先向左递归-->再到输出结点-->向右递归
        if ((2 * index + 1) < arr.length) {
            suffixOrder(2 * index + 1);
        }
        if ((2 * index + 2) < arr.length) {
            suffixOrder(index * 2 + 2);//向右递归
        }
        System.out.println(arr[index]);
    }
}

线索化二叉树

当我们遍历二叉树时,会发现叶子结点或者说只有一个叶子结点的子树的左右指针并没有完全利用起来;因此我们希望能充分利用各个结点的左右指针,让各个指针可以指向自己的前后结点。这样也就引出了线索化二叉树

n个结点的二叉链表中含有n+1(2n-(n-1)=n+1)个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱后继结点的指针(这种附加的指针称为“线索“)

一个结点的前一个结点称为前驱结点;一个结点的后一个结点称为后继结点

由于遍历顺序的不同,故其前驱后继结点的指向就不同,故线索化二叉树可分为:前序线索二叉树中序线索二叉树后序线索二叉树

线索化二叉树创建(中序线索二叉树为例)的要点在于

处理前驱结点时,判断当前结点的left结点是否为空,如果为空,则将left结点指向pre结点(pre结点是指向当前遍历的前一个结点)。处理后继结点时,先把当前结点赋给pre,  然后遍历下一个结点,在下一个结点时,判断pre!=null&&pre.right==null,此时将pre的后继结点right=node(当前节点),这样就完成了前驱后继结点的赋值。 

代码附上(包括前序线索二叉树、中序线索二叉树、后续线索二叉树):

package com.liu.tree;


/**
 * @author liuweixin
 * @create 2021-09-14 19:33
 */
//线索化二叉树
public class ThreadedBinaryTree {
    Node1 pre;//记录当前结点的前一个结点
    Node1 root;

    public void setRoot(Node1 root) {
        this.root = root;
    }

    public static void main(String[] args) {
        Node1 root = new Node1(1, "lwx");
        Node1 node3 = new Node1(3, "lwx");
        Node1 node6 = new Node1(6, "lwx");
        Node1 node8 = new Node1(8, "lwx");
        Node1 node10 = new Node1(10, "lwx");
        Node1 node14 = new Node1(14, "lwx");
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        root.left = node3;
        root.right = node6;
        node3.left = node8;
        node3.right = node10;
        node6.left = node14;
//        threadedBinaryTree.midThreadedNodes();
//        System.out.println("遍历中序线索化二叉树");
//        threadedBinaryTree.midShow();
        threadedBinaryTree.preThreadedNodes();
        System.out.println("遍历前序线索化二叉树");
        threadedBinaryTree.preShow();
    }

    /**
     * 对中序线索化的方法进行封装
     */
    public void midThreadedNodes() {
        midThreadedNodes(root);
    }

    /**
     * 对前序线索化的方法进行封装
     */
    public void preThreadedNodes() {
        preThreadedNodes(root);
    }

    /**
     * 线索化传入的结点
     * 前序线索化二叉树
     *
     * @param node1 要线索化的结点
     */
    public void preThreadedNodes(Node1 node1) {
        if (node1 == null) {//若该结点为空,则无法线索化
            return;
        }
        //线索化当前的结点
        if (node1.left == null) {//其左节点为空时才可以线索化,线索化后指向的是前驱节点
            node1.left = pre;
            node1.leftType = 1;//然后修改其左子结点的类型
        }
        if (pre != null && pre.right == null) {
            //前一个结点的右子节点为空时才可以线索化,将其后继结点指向当前结点
            pre.right = node1;
            pre.rightType = 1;
        }
        //每处理完一个结点,让当前结点即为下一个结点的前驱节点
        pre = node1;
        //向左递归
        if (node1.leftType == 0) {//判断当前结点的左节点是否是左子树,如果是,在进入递归;如果不是,则不进入递归(否则会产生死递归的情况)
            preThreadedNodes(node1.left);
        }
        //向右递归
        if (node1.rightType == 0) {//判断当前结点的右节点是否是右子树,如果是,在进入递归;如果不是,则不进入递归(否则会产生死递归的情况)
            preThreadedNodes(node1.right);
        }

    }

    /**
     * 线索化传入的结点
     * 中序线索化二叉树
     *
     * @param node1 要线索化的结点
     */
    public void midThreadedNodes(Node1 node1) {
        if (node1 == null) {
            //若该结点为空,则无法线索化
            return;
        }
        //先线索化左子树
        midThreadedNodes(node1.left);
        //线索化当前的结点
        if (node1.left == null) {
            //其左节点为空时才可以线索化,线索化后指向的是前驱节点
            node1.left = pre;
            //然后修改其左子结点的类型
            node1.leftType = 1;
        }
        if (pre != null && pre.right == null) {
            //前一个结点的右子节点为空时才可以线索化,将其后继结点指向当前结点
            pre.right = node1;
            pre.rightType = 1;
        }
        //每处理完一个结点,让当前结点即为下一个结点的前驱节点
        pre = node1;
        //线索化右子树
        midThreadedNodes(node1.right);

    }

    /**
     * 遍历线索二叉树(前序遍历)
     */
    public void preShow() {
        Node1 node1 = root;
        if (node1 == null) {
            System.out.println("数空,无法遍历");
            return;
        }
        System.out.println(node1);//先输出当前结点
        while (node1 != null) {//只要当前结点不为空,就可以进入循环遍历
            while (node1.leftType == 0) {//先找到左叶子节点
                node1 = node1.left;//向左递加
                System.out.println(node1);//输出当前结点
            }
            while (node1.rightType == 1) {//找寻左子节点的后继结点
                node1 = node1.right;//向右递进
                System.out.println(node1);//输出当前结点
            }
            //当循环结束,此时左叶子结点的后继结点已遍历完毕
            //然后当前结点继续向右遍历
            node1 = node1.right;
        }
    }

    /**
     * 遍历线索二叉树(中序遍历)
     */
    public void midShow() {
        Node1 node1 = root;
        if (node1 == null) {
            System.out.println("数空,无法遍历");
            return;
        }
        while (node1 != null) {//只要当前结点不为空,就可以进入循环遍历
            while (node1.leftType == 0) {//先找到左叶子节点
                node1 = node1.left;//向左递加
            }
            System.out.println(node1);//此时已找到了左叶子结点,直接将其输出
            while (node1.rightType == 1) {//找寻左子节点的后继结点
                node1 = node1.right;//向右递进
                System.out.println(node1);//输出当前结点
            }
            //当循环结束,此时左叶子结点的后继结点已遍历完毕
            //然后当前结点继续向右遍历(因为中序遍历:左->中->右的顺序)
            node1 = node1.right;
        }
    }
}

//创建一个节点类
class Node1 {
    int id;
    String name;
    Node1 left;//左节点
    Node1 right;//右节点
    int leftType;//1表示为前驱结点,0表示指向的是左子树
    int rightType;//1表示为后继结点,0表示指向的是右子树

    public Node1(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public void setLeft(Node1 left) {
        this.left = left;
    }

    public void setRight(Node1 right) {
        this.right = right;
    }

    /**
     * 前序遍历查找
     *
     * @param id 要查找的id
     * @return 如果null,则无该数据,如果有,返回结点对象
     */
    public Node1 preOrderSearch(int id) {
        if (this.id == id) {//此时找到对应的结点对象,直接返回当前对象
            return this;
        }
        Node1 temp = null;//记录结果
        if (this.left != null) {
            temp = this.left.preOrderSearch(id);//向左递归查找,将结果返回
        }
        if (temp != null) {
            //如果temp不为空,说明在左子树找到要查找的值,直接返回即可
            return temp;
        }
        if (this.right != null) {//当上述的temp没有被返回,说明左子树没有找到值,进入右子树
            temp = this.right.preOrderSearch(id);//向右递归查找
        }
        return temp;//当前递归结束,都返回temp。

    }

    /**
     * 中序遍历查找
     *
     * @param id 要查找的id
     * @return 返回结点对象
     */
    public Node1 midOrderSearch(int id) {
        Node1 temp = null;//记录结果
        if (this.left != null) {//判断当前节点是否为空,若不为空,则递归中序查找
            temp = this.left.midOrderSearch(id);//向左递归查找,将结果返回
        }
        if (temp != null) {
            //如果temp不为空,说明在左子树找到要查找的值,直接返回即可
            return temp;
        }
        if (this.id == id) {//如果找到,则返回当前对象
            return this;
        }
        if (this.right != null) {//当上述的temp没有被返回且未被找到,说明左子树没有找到值,进入右子树
            temp = this.right.midOrderSearch(id);//向右递归查找
        }
        return temp;//此时无论右循环找不找得到值,都返回temp。
    }


    /**
     * 后续遍历查找
     *
     * @param id 要查找的id
     * @return 返回结点对象
     */
    public Node1 suffixOrderSearch(int id) {
        Node1 temp = null;//记录结果
        if (this.left != null) {//判断当前节点是否为空,若不为空,则递归中序查找
            temp = this.left.suffixOrderSearch(id);//向左递归查找,将结果返回
        }
        if (temp != null) {
            //如果temp不为空,说明在左子树找到要查找的值,直接返回即可
            return temp;
        }
        if (this.right != null) {//当上述的temp没有被返回且未被找到,说明左子树没有找到值,进入右子树
            temp = this.right.suffixOrderSearch(id);//向右递归查找
        }
        if (temp != null) {
            //如果temp不为空,说明在右子树找到要查找的值,直接返回即可
            return temp;
        }
        if (this.id == id) {//如果找到,则返回当前对象
            return this;
        }
        return temp;

    }

    /**
     * 前序遍历
     * 前序遍历:先输出父节点,再遍历左子树和右子树
     */
    public void preOrder() {
        System.out.println(this);//先输出父结点
        if (this.left != null) {//左边不为空
            this.left.preOrder();//向左递归
        }
        //向左遍历完,就到右边
        if (this.right != null) {
            this.right.preOrder();//向右递归
        }

    }

    /**
     * 中序遍历
     * 中序遍历:先遍历左子树,然后输出父结点,然后再遍历右子树
     */
    public void midOrder() {
        if (this.left != null) {//左边不为空
            //向左递归
            this.left.midOrder();
        }
        System.out.println(this);//输出父结点
        //向右遍历
        if (this.right != null) {
            this.right.midOrder();//向右递归
        }

    }

    /**
     * 后续遍历
     * 后续遍历:先遍历左子树,然后再遍历右子树,最后输出父结点
     */
    public void suffixOrder() {
        if (this.left != null) {//左边不为空
            //向左递归
            this.left.suffixOrder();
        }
        //向右遍历
        if (this.right != null) {
            this.right.suffixOrder();//向右递归
        }
        System.out.println(this);//输出父结点

    }

    /**
     * 删除叶子结点或者子树
     * 这里类似于单链表,我们需要定位到要删除结点的上一个结点
     *
     * @param id
     */
    public void delete(int id) {
        if (this.left != null) {//如果当前结点的左结点不为空
            if (this.left.id == id) {
                if (this.left.left == null && this.left.right == null) {//当前删除的结点为叶子结点时,直接将其删除
                    this.left = null;
                    System.out.println("删除成功");
                    return;
                } else if (this.left.left != null && this.left.right == null) {
                    //要删除的结点为子树,其右节点为空且其左节点不为空,则将其左节点代替其位置
                    this.left = this.left.left;
                    System.out.println("删除成功");
                    return;
                } else if (this.left.left == null && this.left.right != null) {
                    //要删除的结点为子树,其左节点为空且其右节点不为空,则将其右节点代替其位置
                    this.left = this.left.right;
                    System.out.println("删除成功");
                    return;
                } else {
                    //其左右节点都不为空,则将其左节点代替当前位置
                    this.left.left.right = this.left.right;
                    this.left = this.left.left;
                    System.out.println("删除成功");
                    return;
                }
            } else {
                //如果当前结点的左节点不是要删除的结点,则向左递归删除
                this.left.delete(id);
            }
        }
        if (this.right != null) {
            if (this.right.id == id) {
                if (this.right.left == null && this.right.right == null) {//当前删除的结点为叶子结点时,直接将其删除
                    this.right = null;
                    System.out.println("删除成功");
                    return;
                } else if (this.right.left != null && this.right.right == null) {
                    //要删除的结点为子树,其右节点为空且其左节点不为空,则将其左节点代替其位置
                    this.right = this.right.left;
                    System.out.println("删除成功");
                    return;
                } else if (this.right.left == null && this.right.right != null) {
                    //要删除的结点为子树,其左节点为空且其右节点不为空,则将其右节点代替其位置
                    this.right = this.right.right;
                    System.out.println("删除成功");
                    return;
                } else {
                    //其左右节点都不为空,则将其左节点代替当前位置
                    this.right.left.right = this.right.right;
                    this.right = this.right.left;
                    System.out.println("删除成功");
                    return;
                }
            } else {
                //如果当前结点的右结点不是要删除的结点,则向右递归删除
                this.right.delete(id);
            }
        }


    }

    @Override
    public String toString() {
        return "Node1{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

赫夫曼树

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,则称这样的二叉树为最优二叉树,也称哈夫曼树。赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

结点的带权路径长度(wpl):从根节点到该结点之间的路径长度乘以该结点的权值。

树的带权路径长度:所有叶子结点的带权路径长度之和。

而wpl最小的树,就是赫夫曼树。

赫夫曼树的构建思路

  1. 将数组的数据分别创建结点对象,储存到ArrayLists中(下面简称list)
  2. 先对list排序。
  3. 创建一个父结点对象,该父结点对象的权值即为list前两个数据(即最小的两个数据的)权值之和,然后再将父结点的左右结点分别指向这两个数据。
  4. 将父结点存储到list中,且删除该父结点对应的在list中的两个子结点
  5. 重复上述步骤,最终list会只剩下一个数据,该数据即为该赫夫曼树的根节点。

这里还有个赫夫曼树的编码与解码,有兴趣的朋友可以去了解一下。

代码附上:

package com.liu.treealgorithm;

import java.util.ArrayList;
import java.util.Collections;

/**
 * @author liuweixin
 * @create 2021-09-16 22:29
 */
public class HuffmanTree {
    public static void main(String[] args) {
        int[]arr = new int[]{3,13,29,1,7,8,6};
        HuffmanTree huffmanTree = new HuffmanTree();
        Node root = huffmanTree.GetHuffmanTree(arr);
        root.preOrder();
    }

    /**
     * 形成赫夫曼树
     * @param arr 要形成赫夫曼树的数组
     * @return  返回该赫夫曼树的根节点
     */
    public Node GetHuffmanTree(int[] arr) {
        if (arr == null) {
            throw new RuntimeException("传入数组为空");
        }
        ArrayList<Node> lists = new ArrayList<>();//创建一个list存放结点数据
        for (int data : arr) {
            lists.add(new Node(data));//添加到lists集合中
        }
        while (lists.size()>1) {//当lists内的数据长度大于1时,此时仍可以添加到赫夫曼树中
            Collections.sort(lists);//在对lists集合操作之前对lists中的数据进行排序
            Node parent = new Node(lists.get(0).value + lists.get(1).value);//父结点的权值等于其两个子结点的权值
            //设置父结点的结点指向
            parent.left=lists.get(0);
            parent.right=lists.get(1);
            //然后添加到lists集合中
            lists.add(parent);
            //而且要把lists集合内合并的子结点删除
            lists.remove(0);
            lists.remove(0);//两次都是0,因为上面的移除操作之后,下一个结点的下标就往前移动了一位,所以仍是0
        }
        //循环结束时,此时剩下lists集合内剩下一个结点,即为根节点,将其返回
        return lists.get(0);
    }
}

class Node implements Comparable {
    int value;
    Node left;
    Node right;

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

    @Override
    public int compareTo(Object o) {
        Node node = (Node) o;
        return this.value - node.value;
    }

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

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


二叉排序树(BST)

二叉排序树:即对于二叉树的任何一个非叶子结点,其左子结点都比当前结点小,右子结点比当前节点大。特别说明:如果有相同的值,可以将该结点放置到左子结点或右子结点。在我们前面的画图中的也都是二叉排序树。

添加结点的思路

1、传入结点的权值与当前结点的权值进行比较。

1.1、如果小于当前结点的权值,则判断当前结点的左结点是否为空。

1.1.1如果为空,则当前结点的左子结点直接指向传入的结点;

1.1.2若不为空,则继续向左递归;

1.2、若大于当前结点的权值,则判断当前结点的右节点是否为空

1.2.1如果为空,则当前结点的右子结点直接传入的结点;

1.2.2若不为空,则继续向右递归。

代码附上:

/**
     * 添加结点
     *
     * @param node1 要添加的结点
     */
    public void addNode(Node1 node1) {
        if (node1 == null) {
            return;
        }
        if (node1.id < this.id) {//如果传进来的结点的id值比当前调该方法的结点的id值要小
            if (this.left == null) {
                this.left = node1;
            } else {//如果不为空,则递归添加
                this.left.addNode(node1);
            }
        } else {//如果传进来的结点的id值比当前调该方法的结点的id值要大或等于
            if (this.right == null) {
                this.right = node1;
            } else {
                //递归添加
                this.right.addNode(node1);
            }
        }
    }

删除结点的思路:

注意:这里类似于单链表,我们做删除操作,需要找到该结点的父结点,才能进行删除。
1、删除叶子结点。
(1)需要先去找到要删除的结点 targetNode
(2)找到targetNode的父结点parent
(3)确定targetNode是parent的左子结点还是右子结点
(4)根据前面的情况来对应删除   
      左子结点 parent.left=null;
      右子结点 parent.right=null;
2、删除只有一棵子树的结点。
(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、删除有两颗子树的结点。
(1)首先先找到要删除的结点targetNode
(2)找到targetNode的父结点parent
(3)从targetNode的右子树找到最小的结点
(4)用一个临时变量,将最小结点的值保存 temp
(5)删除该最小结点

完整代码附上:

package com.liu.treealgorithm;

/**
 * @author liuweixin
 * @create 2021-09-17 11:07
 */
//排序二叉树
public class BinarySortTree {
    Node1 root;

    public static void main(String[] args) {
        Node1 root = new Node1(7);
        Node1 node3 = new Node1(3);
        Node1 node1 = new Node1(1);
        Node1 node5 = new Node1(5);
        Node1 node10 = new Node1(10);
        Node1 node9 = new Node1(9);
        Node1 node12 = new Node1(12);
        Node1 node11 = new Node1(11);
        Node1 node2 = new Node1(2);
        BinarySortTree binarySortTree = new BinarySortTree();
        binarySortTree.add(root);
        binarySortTree.add(node3);
        binarySortTree.add(node1);
        binarySortTree.add(node5);
        binarySortTree.add(node10);
        binarySortTree.add(node9);
        binarySortTree.add(node12);
        binarySortTree.add(node11);
        binarySortTree.add(node2);
        //中序遍历
        System.out.println("删除前:");
        binarySortTree.root.midOrder();//1 2 3 5 7 9 10 11 12
        System.out.println("删除后:");
        binarySortTree.delNode(3);
        binarySortTree.root.midOrder();
    }

    //查找要删除的结点
    public Node1 search(int id) {
        if (root == null) {
            return null;
        } else {
            return root.search(id);
        }
    }

    //查找要删除结点的父结点
    public Node1 searchParent(int id) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(id);
        }
    }

    /**
     * 返回以node为根节点的二叉排序树的最小节点的值
     * 删除node为根节点的二叉排序树的最小结点
     *
     * @param node1
     * @return
     */
    public int delRightTreeMin(Node1 node1) {
        Node1 target = node1;
        //循环地查找左子结点,就会找到最小值
        while (target.left != null) {
            target = target.left;
        }
        //这是target就指向了最小结点
        //删除最小结点
        delNode(target.id);
        return target.id;

    }

    //删除结点
    public void delNode(int id) {
        if (root == null) {
            return;
        } else {
            //1.需求先去找到要删除地结点 targetNode
            Node1 targetNode = search(id);
            //如果没有找到要删除地结点
            if (targetNode == null) {
                return;
            }
            //如果我们发现当前这颗二叉排序树只有一个结点
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }
            //去找到targetNode的父结点
            Node1 parent = searchParent(id);
            //如果要删除的结点是叶子结点
            if (targetNode.left == null && targetNode.right == null) {
                //判断targetNode是父结点的左子结点还是右子结点
                if (parent.left != null && parent.left.id == id) {
                    //是左子结点
                    parent.left = null;
                } else if (targetNode.left != null && parent.right.id == id) {
                    //是右子结点
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null) {//删除有两颗子树的结点
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.id = minVal;
            } else {
                //删除只有一颗子树的结点
                if (targetNode.left != null) {
                    if (parent != null) {
                        //如果targetNode是parent的左子结点
                        if (parent.left.id == id) {
                            parent.left = targetNode.left;
                        } else {//targetNode是parent的右子结点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else {
                    //如果要删除的结点有右子结点
                    if (parent != null) {
                        //如果targetNode是parent的左子结点
                        if (parent.left.id == id) {
                            parent.left = targetNode.right;
                        } else {
                            //如果targetNode是parent的右子结点
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }
        }
    }

    /**
     * 向排序二叉树添加节点
     *
     * @param node1 要添加的结点
     */
    public void add(Node1 node1) {
        if (root == null) {
            root = node1;
        } else {
            this.root.addNode(node1);
        }
    }


}

class Node1 {
    int id;
    Node1 left;
    Node1 right;

    public Node1(int id) {
        this.id = id;
    }

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

    /**
     * 添加结点
     *
     * @param node1 要添加的结点
     */
    public void addNode(Node1 node1) {
        if (node1 == null) {
            return;
        }
        if (node1.id < this.id) {//如果传进来的结点的id值比当前调该方法的结点的id值要小
            if (this.left == null) {
                this.left = node1;
            } else {//如果不为空,则递归添加
                this.left.addNode(node1);
            }
        } else {//如果传进来的结点的id值比当前调该方法的结点的id值要大或等于
            if (this.right == null) {
                this.right = node1;
            } else {
                //递归添加
                this.right.addNode(node1);
            }
        }
    }

    /**
     * 查找要删除的结点
     *
     * @param id 要删除结点的权值
     * @return 如果找到,则返回该结点,否则返回null
     */
    public Node1 search(int id) {
        if (id == this.id) {//找到就是该结点
            return this;
        } else if (id < this.id) {//如果查找的值小于当前结点,向左子树递归查找
            if (this.left == null) {//如果左子结点为空
                return null;
            }
            //否则向左递归查找
            return this.left.search(id);
        } else {
            //如果查找的值不小于当前结点,向右子树递归查找
            if (this.right == null) {
                return null;
            }
            //否则向右递归查找
            return this.right.search(id);
        }
    }

    /**
     * 查找要删除结点的父结点
     *
     * @param id 要查找结点的权值
     * @return 若找到,返回该结点,否则返回null
     */
    public Node1 searchParent(int id) {
        //如果当前结点就是要删除结点的父结点,就返回
        if ((this.left != null && this.left.id == id) ||
                (this.right != null && this.right.id == id)) {
            return this;
        } else {
            //如果查找的值小于当前结点的值,并且当前结点的左子结点不为空
            if (id < this.id && this.left != null) {
                return this.left.searchParent(id);//向左子树递归查找
            } else if (id >= this.id && this.right != null) {
                return this.right.searchParent(id);//向右子树递归查找
            } else {
                return null;//没有找到父结点
            }
        }
    }

    /**
     * 删除结点
     *
     * @param id 要删除结点的权值
     */
    public void deleteNode(int id) {


    }

    @Override
    public String toString() {
        return "Node1{" +
                "id=" + id +
                '}';
    }
}

平衡二叉树(AVL)

平衡二叉树(AVL):平衡二叉树出现是为了解决排序二叉树出现的一些小问题,例如,我们将{1,2,3,4,5}这五个数据构成一个排序二叉树,则形成的排序二叉树像一个单链表;这样的话虽然添加速度没有影响,但是查询速度明显降低,不能发挥排序二叉树(BST)的优势,甚至查询速度比单链表还慢。所以这就提供了一种方案—平衡二叉树。

平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树,可以保证查询效率较高。具有以下特点:它是一棵空树它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。

举例看看那些树是平衡二叉树?为什么?

第一二棵树的左右两树,满足左右子树高度差不超过1。而第三棵树的左右子树的高度差为3 -1=2>1所以不满足平衡二叉树的定义

这里我就借助韩老师的图来解释如何实现,因为韩老师的图很经典。

我们需要对顺序二叉树进行一个调整,调整成一个平衡二叉树。

下图为右子树的高度大于左子树高度的情况:

左旋转操作

 那当左子树高度大于右子树高度的情况该如何处置呢?

这时候我们就需要对该树进行右旋转操作

此时也有可能出现另一种情况,以右旋转为例,当左子树的高度大于右子树的高度时,我们需要左旋转,但是此时如果该结点的左子树的右子树高度大于左子树高度时,我们会发现,对该结点直接进行左旋转,得出来的二叉树仍不是平衡二叉树,原因在于该结点的左子树的右子树的高度大于左子树的高度,为了解决该问题,我们需要先对该结点的左子树先进行左旋转,然后在对该结点进行右旋转,此时就会发现已经形成了一个平衡二叉树。 

 即先对7这个结点左旋转,然后再对整棵树进行右旋转。便能得到一个平衡二叉树。 

 

实现代码附上:

package com.liu.treealgorithm;


/**
 * @author liuweixin
 * @create 2021-09-19 10:15
 */
//AVL树,即平衡二叉树
public class AVLTree {
    Node2 root;

    public static void main(String[] args) {
        int[] arr = new int[]{10, 11, 7, 6, 8, 9};
        AVLTree avlTree = new AVLTree();
        for (int i = 0; i < arr.length; i++) {
            avlTree.add(new Node2(arr[i]));
        }
//        System.out.println("左旋转前");
//        System.out.println("树的高度为:"+avlTree.root.height());
//        System.out.println("左子树高度为:"+avlTree.root.leftHeight());
//        System.out.println("右子树高度为:"+avlTree.root.rightHeight());
//        System.out.println("树的根节点为:"+avlTree.root);
//        System.out.println("左旋转后");
//        avlTree.root.leftRotate();
        //在插入结点就实现平衡操作
        System.out.println("树的高度为:" + avlTree.root.height());
        System.out.println("左子树高度为:" + avlTree.root.leftHeight());
        System.out.println("右子树高度为:" + avlTree.root.rightHeight());
        System.out.println("树的根节点为:" + avlTree.root);
        //前序排列
        avlTree.preOrder();
    }

    public void preOrder() {
        if (root == null) {
            return;
        }
        this.root.preOrder();
    }

    /**
     * 向排序二叉树添加节点
     *
     * @param node1 要添加的结点
     */
    public void add(Node2 node1) {
        if (root == null) {
            root = node1;
        } else {
            this.root.addNode(node1);
            if (this.root.rightHeight() - this.root.leftHeight() > 1) {
                //如果当前树的右子树的高度与左子树的差值大于1,则需要左旋转
                //如果它的右子树的左子树高度大于它的右子树的右子树的高度
                if (this.root.right != null && this.root.right.leftHeight() > this.root.right.rightHeight()) {
                    //先对右子结点进行右旋转,然后在对当前结点进行左旋转
                    this.root.right.rightRotate();
                    this.root.leftRotate();
                } else {
                    this.root.leftRotate();
                }
                return;
            }
            if (this.root.leftHeight() - this.root.rightHeight() > 1) {
                //如果当前树的左子树的高度与右子树的差值大于1,则需要右旋转
                //如果它的右子树的左子树高度大于它的右子树的右子树的高度
                if (this.root.left != null && this.root.left.rightHeight() > this.root.left.leftHeight()) {
                    //先对左子结点进行左旋转,然后在对当前结点进行右旋转
                    this.root.left.leftRotate();
                    this.root.rightRotate();
                } else {
                    this.root.rightRotate();
                }
                return;
            }
        }
    }


    static class Node2 {
        int id;
        Node2 left;
        Node2 right;

        public Node2(int id) {
            this.id = id;
        }

        public void preOrder() {
            System.out.println(this);
            if (this.left != null) {
                this.left.preOrder();
            }
            if (this.right != null) {
                this.right.preOrder();
            }
        }

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

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

        //返回以该结点为根节点的数的高度
        public int height() {
            //这里使用了递归,+1的原因是还要加上该结点的这个层数
            return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
        }

        //处理左旋转
        public void leftRotate() {
            //先创建一个新结点
            Node2 newNode2 = new Node2(this.id);
            //把新节点的左子树指向当前结点的左子树
            newNode2.left = this.left;
            //把新节点的右子树指向当前结点的右子树的左子树
            newNode2.right = this.right.left;
            //把当前结点的值改为当前结点的右子树的值
            this.id = this.right.id;
            //当前结点的左子树指向新结点,右子树指向右子树的右子树
            this.left = newNode2;
            this.right = this.right.right;
        }

        //处理右旋转
        public void rightRotate() {
            //其实右旋转与左旋转同理
            Node2 newNode = new Node2(this.id);
            newNode.right = this.right;
            newNode.left = this.left.right;
            this.id = this.left.id;
            this.left = this.left.left;
            this.right = newNode;
        }

        /**
         * 添加结点
         *
         * @param node1 要添加的结点
         */
        public void addNode(Node2 node1) {
            if (node1 == null) {
                return;
            }
            if (node1.id < this.id) {//如果传进来的结点的id值比当前调该方法的结点的id值要小
                if (this.left == null) {
                    this.left = node1;
                } else {//如果不为空,则递归添加
                    this.left.addNode(node1);
                }
            } else {//如果传进来的结点的id值比当前调该方法的结点的id值要大或等于
                if (this.right == null) {
                    this.right = node1;
                } else {
                    //递归添加
                    this.right.addNode(node1);
                }
            }
        }

        @Override
        public String toString() {
            return "Node2{" +
                    "id=" + id +
                    '}';
        }
    }
}

总结

   树这部分的知识难度相对前面的数组、链表要难,所以需要大家勤敲勤练,我也是在复习时,才慢慢让自己的理解更加深入,上面的基本都是我自己敲下来的理解,供大家参考,希望大家都能有所收获。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值