网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容

如果右边没有就找找左边


一、二叉树

为什么需要树

  1. 数组的优点是,通过下标访问,速度快,有序数组,可以通过二分查找提高检索效率,但是如果要检索某个值,或者从中间插入一个值,需要移动整体,效率就很低了
  2. 链式存储优点是,一定程度上优化数组存储方式,插入和删除效率提高,不用整体移动了,但是检索时效率却异常的低,比如检索特定的一个值,需要遍历整个链表
  3. 树,就是提高数据存储和读取的效率,比如二叉排序树,既可以保证数据检索速度,又保证数据插入,删除,修改速度。

树的概念

  1. 下图中有一个错误,H、E、F、G没有子节点,称为叶子节点,图中将H写成了A

二叉树概念

  1. 二叉树就是每个节点最多有两个子节点,而形成的树形数据结构
  2. 如果二叉树的所有叶子节点(没有子节点的节点称为叶子节点)都在最后一层,并且节点总数=2^n-1(n为层数),则称为满二叉树。
  3. 如果二叉树的所有叶子节点都在倒数第一或倒数第二层,最后一层的叶子节点在左边连续,倒数第二层叶子节点在右边连续,称为完全二叉树

二叉树遍历:前序、中序、后序遍历

以这颗树为例

java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_子树

  1. 前序遍历:先输出父节点,再遍历左子树和右子树
  2. 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
  3. 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
  4. 根据父节点的顺序即可判断是那种遍历,父节点最先遍历就是前序,父节点在左子树后面遍历就是中序,父节点最后遍历就是后序
  5. 通过遍历,我们就可以写出查找,删除的代码
  6. 运行效果
  1. 代码
public class Test {

    public static void main(String[] args) {
        BinaryTreeNode[] binaryTreeNodes = new BinaryTreeNode[7];
        for(int i = 0 ;i<binaryTreeNodes.length;i++){
            binaryTreeNodes[i] = new BinaryTreeNode(i,i+1);
        }

        //人为构建二叉树
        binaryTreeNodes[0].setLeftNode(binaryTreeNodes[1]);
        binaryTreeNodes[0].setRightNode(binaryTreeNodes[2]);
        binaryTreeNodes[1].setLeftNode(binaryTreeNodes[3]);
        binaryTreeNodes[1].setRightNode(binaryTreeNodes[4]);
        binaryTreeNodes[2].setLeftNode(binaryTreeNodes[5]);
        binaryTreeNodes[2].setRightNode(binaryTreeNodes[6]);

        BinaryTree binaryTree = new BinaryTree(binaryTreeNodes[0]);
        System.out.println("前序遍历================");
        binaryTree.preOrder();
        System.out.println("中序遍历================");
        binaryTree.midOrder();
        System.out.println("后序遍历================");
        binaryTree.postOrder();
        System.out.println("前序查找id为5节点"+binaryTree.preFind(5));
        System.out.println("中序查找id为5节点"+binaryTree.midFind(5));
        System.out.println("后序查找id为5节点"+binaryTree.postFind(5));
        System.out.println("删除id为5节点");
        binaryTree.deleteTreeById(5);
        binaryTree.preOrder();
        System.out.println("删除id为2的节点,包括它的所有子节点");
        binaryTree.deleteTreeById(2);
        binaryTree.preOrder();
    }
}

/**
 * 二叉树节点
 */
class BinaryTreeNode{
    private int id;
    private Object data;
    private BinaryTreeNode leftNode;
    private BinaryTreeNode rightNode;

    public BinaryTreeNode(int id, Object data) {
        this.id = id;
        this.data = data;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public BinaryTreeNode getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(BinaryTreeNode leftNode) {
        this.leftNode = leftNode;
    }

    public BinaryTreeNode getRightNode() {
        return rightNode;
    }

    public void setRightNode(BinaryTreeNode rightNode) {
        this.rightNode = rightNode;
    }

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

    /**
     * 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
     */
    public void preOrder(){
        //1. 先输出当前节点
        System.out.println(this+" ");
        //2. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            this.leftNode.preOrder();
        }
        //3. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            this.rightNode.preOrder();
        }
    }
    /**
     * 前序查找,先判断当前节点是否是查找的节点,不是遍历左子树,左子树没找到遍历右子树,都没找到返回null
     */
    public BinaryTreeNode preFind(Integer id){
        //1. 先判断当前节点
        if(id == this.id) return this;

        BinaryTreeNode node = null;//用来保存结果

        //2. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            node =  this.leftNode.preFind(id);
        }
        if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
            return node;
        }
        //3. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            node = this.rightNode.preFind(id);
        }
        return node;
    }
    /**
     * 中序遍历,先遍历左子树,然后输出当前节点,然后遍历右子树
     */
    public void midOrder(){
        //1. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            this.leftNode.midOrder();
        }
        //2. 然后输出当前节点
        System.out.println(this+" ");

        //3. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            this.rightNode.midOrder();
        }
    }
    /**
     * 中序查找
     */
    public BinaryTreeNode midFind(Integer id){

        BinaryTreeNode node = null;//用来保存结果

        //1. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            node =  this.leftNode.preFind(id);
        }
        if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
            return node;
        }

        //2. 没有左子树了,判断是否和自己一致
        if(id == this.id) return this;

        //3. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            node = this.rightNode.preFind(id);
        }
        return node;
    }
    /**
     * 后序遍历,先遍历左子树,然后遍历右子树,最后输出当前节点
     */
    public void postOrder(){
        //1. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            this.leftNode.postOrder();
        }
        //2. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            this.rightNode.postOrder();
        }
        //3. 然后输出当前节点
        System.out.println(this+" ");
    }
    /**
     * 后序查找
     */
    public BinaryTreeNode postFind(Integer id){

        BinaryTreeNode node = null;//用来保存结果

        //1. 如果左子树存在,遍历左子树
        if(this.leftNode!=null){
            node =  this.leftNode.preFind(id);
        }
        if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
            return node;
        }
        //2. 如果右子树存在,遍历右子树
        if(this.rightNode!=null){
            node = this.rightNode.preFind(id);
        }
        if(node != null){//如果node不为null,那么表示在右子树找到了,直接返回,无需执行下面找自己的代码
            return node;
        }
        //3. 判断是否和自己一致
        if(id == this.id) return this;
        return node;
    }

    /**
     * 根据id删除以它为根的子树,就是比如我们要删除5这个结点,那么我们删除它本身以及它所有子节点
     * 因为树是单向数据结构,我们如果找到这个结点本身,无法回到它的父节点断开引用链接,所以我们判断条件为子节点的id是否相匹配
     * 也就是说,这里无法判断root是否是要删除的节点,也没办法删除它
     */
    public void deleteTreeById(Integer id){
        //1、 判断左节点是否就是要删除的节点,如果是直接断开链接
        if(this.leftNode != null && this.leftNode.id == id) {
            this.leftNode = null;
            return;
        }
        //2、 判断右节点是否是要删除结点
        if(this.rightNode != null &&this.rightNode.id == id){
            this.rightNode = null;
            return;
        }
        //3、如果左右结点都不是要删除的节点,那么递归
        if(this.leftNode != null ) this.leftNode.deleteTreeById(id);
        if(this.rightNode != null ) this.rightNode.deleteTreeById(id);
    }

}

/**
 * 二叉树
 */
class BinaryTree{
    private BinaryTreeNode root;//根节点,只要有根节点,就可以找到整棵树

    public BinaryTree(BinaryTreeNode root) {
        this.root = root;
    }

    /**
     * 判断二叉树是否为空
     * @return true 二叉树为空,false 二叉树不为空
     */
    public boolean isNull(){
        if(root == null){
            System.out.println("二叉树为空!!!");
            return true;
        }
        return false;
    }

    //前序遍历
    public void preOrder(){
        if(isNull()) return;//如果二叉树为空,不执行遍历
        root.preOrder();
    }
    //中序遍历
    public void midOrder(){
        if(isNull()) return;
        root.midOrder();
    }
    //后序遍历
    public void postOrder(){
        if(isNull()) return;
        root.postOrder();
    }
    //前序查找
    public BinaryTreeNode preFind(Integer id){
        if(isNull()) return null;//如果二叉树为空,不执行遍历
        return root.preFind(id);
    }
    //中序查找
    public BinaryTreeNode midFind(Integer id){
        if(isNull()) return null;
        return root.midFind(id);
    }
    //后序查找
    public BinaryTreeNode postFind(Integer id){
        if(isNull()) return null;
        return root.postFind(id);
    }

    /**
     * 根据id删除子树
     */
    public void deleteTreeById(Integer id){
        if (isNull()) return;
        if(root.getId() == id){//如果根节点就是要删除的节点,那么直接置为空
            root = null;
        }else{//否则进行root的子树删除
            root.deleteTreeById(id);
        }
    }
}

1. 顺序存储二叉树

顺序存储二叉树

  1. 以数组存放树的节点,遍历数组时,依旧按照前序,中序和后序的顺序遍历
  1. 顺序二叉树通常只考虑完全二叉树
  2. 第n个元素的左子节点为2n+1,右子节点为2n+2,父节点为(n-1)/2. —n表示二叉树的第几个元素,从0开始
  1. 有啥用?
  1. 八大排序算法的堆排序,就会用到,包括类似的场景都可以用,很好用,后面会讲到
  1. 代码
public class ArrBinaryTreeDemo {

	public static void main(String[] args) {
		int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
		//创建一个 ArrBinaryTree
		ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
		arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7
	}

}

//编写一个ArrayBinaryTree, 实现顺序存储二叉树遍历

class ArrBinaryTree {
	private int[] arr;//存储数据结点的数组

	public ArrBinaryTree(int[] arr) {
		this.arr = arr;
	}
	
	//重载preOrder
	public void preOrder() {
		this.preOrder(0);
	}
	
	//编写一个方法,完成顺序存储二叉树的前序遍历
	/**
	 * 
	 * @param index 数组的下标 
	 */
	public void preOrder(int index) {
		//如果数组为空,或者 arr.length = 0
		if(arr == null || arr.length == 0) {
			System.out.println("数组为空,不能按照二叉树的前序遍历");
		}
		//输出当前这个元素
		System.out.println(arr[index]); 
		//向左递归遍历
		if((index * 2 + 1) < arr.length) {
			preOrder(2 * index + 1 );
		}
		//向右递归遍历
		if((index * 2 + 2) < arr.length) {
			preOrder(2 * index + 2);
		}
	}
	
}

2. 线索化二叉树

线索化二叉树

  1. 解决了指针浪费问题
  2. 让空闲的指针指向自己的前后节点
  1. 代码
  1. 首先我们再节点中添加两个属性,分别代表左、右指针的状态,看它是指向子树,还是前驱或后继
  1. 然后,我们再二叉树中,创建一个属性,代表当前节点的上一个节点
  1. 编写方法并测试(注意,线索化二叉树,需要使用新的遍历方式,线性遍历,效率更高,结果和非线索化二叉树遍历结果一样)
//为了实现线索化,需要创建要给指向当前结点的前驱结点的指针
    //在递归进行线索化时,pre 总是保留前一个结点
    private BinaryTreeNode pre = null;

    

    //重载一把threadedNodes方法
    public void threadedNodes() {
        this.threadedNodes(root);
    }

    //编写对二叉树进行中序线索化的方法
    /**
     *
     * @param node 就是当前需要线索化的结点
     */
    public void threadedNodes(BinaryTreeNode node) {

        //如果node==null, 不能线索化
        if(node == null) {
            return;
        }

        //(一)先线索化左子树
        threadedNodes(node.getLeftNode());
        //(二)线索化当前结点[有难度]

        //处理当前结点的前驱结点
        //以8结点来理解
        //8结点的.left = null , 8结点的.leftType = 1
        if(node.getLeftNode() == null) {
            //让当前结点的左指针指向前驱结点
            node.setLeftNode(pre);
            //修改当前结点的左指针的类型,指向前驱结点
            node.setLeftType(1);
        }

        //处理后继结点
        if (pre != null && pre.getRightNode() == null) {
            //让前驱结点的右指针指向当前结点
            pre.setRightNode(node);
            //修改前驱结点的右指针类型
            pre.setRightType(1);
        }
        //!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
        pre = node;

        //(三)在线索化右子树
        threadedNodes(node.getRightNode());


    }

    //遍历线索化二叉树的方法
    public void threadedList() {
        //定义一个变量,存储当前遍历的结点,从root开始
        BinaryTreeNode node = root;
        while(node != null) {
            //循环的找到leftType == 1的结点,第一个找到就是8结点
            //后面随着遍历而变化,因为当leftType==1时,说明该结点是按照线索化
            //处理后的有效结点
            while(node.getLeftType() == 0) {
                node = node.getLeftNode();
            }

            //打印当前这个结点
            System.out.println(node+"left指向"+node.getLeftNode());
            //如果当前结点的右指针指向的是后继结点,就一直输出
            while(node.getRightType() == 1) {
                //获取到当前结点的后继结点
                node = node.getRightNode();
                System.out.println(node+"right指向"+node.getRightNode());
            }
            //替换这个遍历的结点
            node = node.getRightNode();

        }
    }

3. 二叉排序树(BST)

  1. BST(Binary Sort(Search) Tree),二叉排序树就是任何一个非叶子结点,要求左子结点的值比当前结点的值小,右子结点的值比当前结点的值大。如果有相同的值,可以将该结点放在左或右子结点

二叉树的构建、添加和删除

  1. 构建
  1. 比如将{7,3,10,12,5,1,9}构建成排序二叉树,依次遍历数据,第一个直接就是根结点,剩下的按照规则,小的在左边,大的在右边,依次添加即可
  1. 添加
  1. 添加就是从根结点开始往下找,如果他比根结点大,就比较右边,如果右子结点没值,直接添加,如果有,就和右子结点比较,依次类推
  1. 删除
  1. 删除叶子结点,比如2,5,9,12,直接删除即可
  2. 删除有一颗子树的节点,比如1,直接让它的子树来到它的位置,然后删掉自己
  3. 删除有两颗子树的节点,比如7,3,10,让它的右子树来到它的位置,然后删掉自己,然后让左子树按排序树规则添加到右子树,比如删除7,让10和它的子树到它的位置,然后3和它的子树添加到9的左子树
  4. 删除结点时,如果当前结点的子结点,就是要删除的结点,那么开始执行逻辑
  5. 首先判断要删除的结点时1,2,3种情况的那种,然后进行相应操作

代码

  1. 运行效果
  1. 代码
import java.util.Arrays;

public class Test {

    public static void main(String[] args) {
        int[] arr = {7,3,10,12,5,1,9};
        System.out.println("将数组"+ Arrays.toString(arr)+"构建成二叉排序树=================");
        BinarySortTree binarySortTree = new BinarySortTree(arr);
        System.out.println("二叉排序树中序遍历结果为======================================");
        binarySortTree.midOrder();
        System.out.println("添加一个结点2");binarySortTree.add(2);binarySortTree.midOrder();
        System.out.println("删除结点2");binarySortTree.deleteNode(2);binarySortTree.midOrder();
        System.out.println("删除结点5");binarySortTree.deleteNode(5);binarySortTree.midOrder();
        System.out.println("删除结点9");binarySortTree.deleteNode(9);binarySortTree.midOrder();
        System.out.println("删除结点12");binarySortTree.deleteNode(12);binarySortTree.midOrder();
        System.out.println("删除结点7");binarySortTree.deleteNode(7);binarySortTree.midOrder();
        System.out.println("删除结点3");binarySortTree.deleteNode(3);binarySortTree.midOrder();
        System.out.println("删除结点10");binarySortTree.deleteNode(10);binarySortTree.midOrder();
        System.out.println("删除结点1");binarySortTree.deleteNode(1);binarySortTree.midOrder();
    }

}

/**
 * 二叉排序树结点
 */
class BinarySortTreeNode{
    private int data;
    private BinarySortTreeNode left;
    private BinarySortTreeNode right;

    public BinarySortTreeNode(int data) {
        this.data = data;
    }

    /**
     * 二叉排序树添加结点方法
     * @param node 一个结点
     */
    public void add(BinarySortTreeNode node){
        if(node == null) return;//如果结点为null,不进行添加
        //如果要添加结点比当前结点小,那么添加到左子结点
        if(node.getData()<this.data){
            if(this.left==null){//如果左子结点为空
                this.left = node;//添加到左子结点
            }else{//如果不为空
                this.left.add(node);//进行递归,让左子树添加
            }
        }else if(node.getData()>this.data){//如果要添加结点比当前结点大,那么添加到右子结点
            if(this.right==null){//如果右子结点为空,将结点添加
                this.right = node;
            }else {//不为空
                this.right.add(node);//递归到右子树添加
            }
        }else{//如果要添加结点等于当前结点,添加到左右都可以
            if(this.left==null){
                this.left = node;
            }else if(this.right == null){
                this.left = node;
            }
            //如果左右子节点都有了,那么放在右子树
            else{
                this.right.add(node);
            }
        }
    }

    /**
     * 中序遍历,先遍历左子树,然后输出当前节点,然后遍历右子树
     */
    public void midOrder(){
        //1. 如果左子树存在,遍历左子树
        if(this.left!=null){
            this.left.midOrder();
        }
        //2. 然后输出当前节点
        System.out.print(this+" ");

        //3. 如果右子树存在,遍历右子树
        if(this.right!=null){
            this.right.midOrder();
        }
    }

    /**
     * 中序查找指定数据的父节点
     */
    public BinarySortTreeNode midFindParent(Integer id){

        BinarySortTreeNode node = null;//用来保存结果

        //1. 如果左子树存在,判断左子结点是否是要找的值,不是继续遍历左子树
        if(this.left!=null&&this.left.getData()!=id){
            node =  this.left.midFindParent(id);
        }
        if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
            return node;
        }

        //2. 没有左子树了,判断是否和自己一致
        if(this.left!=null){
            if(id==this.left.data) return this;
        }
        if(this.right!=null){
            if(id==this.right.data) return this;
        }


        //3. 如果右子树存在,遍历右子树
        if(this.right!=null&&this.right.getData()!=id){
            node = this.right.midFindParent(id);
        }
        return node;
    }

    public int getData() { return data; }


    public void setData(int data) { this.data = data; }

    public BinarySortTreeNode getLeft() { return left; }

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

    public BinarySortTreeNode getRight() { return right; }

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

    @Override
    public String toString() { return "Node{" + "data=" + data + '}'; }
}
/**
 * 二叉排序树
 */
class BinarySortTree{
    private BinarySortTreeNode root;

    public BinarySortTreeNode getRoot() { return root; }

    public BinarySortTree(){}

    //构造方法,通过数组直接构造排序二叉树
    public BinarySortTree(int[] arr){ this.addArrayToBinarySortTree(arr); }
    /**
     * 添加结点方法
     * @param node 一个结点
     */
    public void add(BinarySortTreeNode node){
        if(node == null) throw new RuntimeException("要添加结点为空!!!");
        if(root == null) root = node;
        else root.add(node);
    }

    /**
     * 重载直接将一个数据添加到排序二叉树
     * @param data
     */
    public void add(int data){
        add(new BinarySortTreeNode(data));
    }

    /**
     * 查找指定结点的父节点
     * @param data
     * @return
     */
    public BinarySortTreeNode findParentByData(int data){
        if(root.getData() == data) return null;
        return root.midFindParent(data);
    }

    /**
     * 删除根结点
     */
    private void deleteRoot(){
        if(root.getLeft()==null&&root.getRight()==null){
            root=null;
        }else if(root.getLeft()!=null&&root.getRight()!=null){
            BinarySortTreeNode left = root.getLeft();
            root = root.getRight();
            add(left);
        }else if(root.getLeft()==null&&root.getRight()!=null){
            root=root.getRight();
        }else if(root.getLeft()!=null&&root.getRight()==null){
            root=root.getLeft();
        }
    }
    /**
     * 删除非根结点
     * @param node 要删除结点
     * @param nodeParent 要删除结点的父结点
     * @param flag 要删除结点是父结点的左子结点还是右子结点,左为true
     */
    private void deleteNode(BinarySortTreeNode node,BinarySortTreeNode nodeParent,Boolean flag){
        if (node.getLeft()==null&&node.getRight()==null){//如果要删除结点是叶子结点,直接删除
            if(flag) nodeParent.setLeft(null);//如果它是父结点的左子结点,直接让父结点的left置为null
            else nodeParent.setRight(null);//如果是右子结点,让right置为null
        }else if(node.getLeft()!=null && node.getRight()==null){//如果要删除结点是只有右子树的结点
            //让右子树到它的位置
            if(flag) nodeParent.setLeft(node.getLeft());
            else nodeParent.setRight(node.getRight());
        }else if(node.getRight()!=null&&node.getLeft()==null){//如果要删除结点是只有左子树的结点
            //让左子树到它的位置
            if(flag)nodeParent.setLeft(node.getRight());
            else nodeParent.setRight(node.getRight());
        }else{//如果要删除结点拥有左右两个子树
            //让右子树到它的位置,然后左子树按规则添加到右子树中
            BinarySortTreeNode left = node.getLeft();//先记录左子树
            if(flag){
                nodeParent.setLeft(node.getRight());//然后让它的右子树到它原来的位置
                node.getRight().add(left);//然后将左子树按规则添加到右子树
            }else{
                nodeParent.setRight(node.getRight());
                node.getRight().add(left);
            }
        }
    }
    /**
     * 删除指定单个结点,如果有一样的只删除一个
     * @param data
     */
    public void deleteNode(int data){
        if(root == null) throw new RuntimeException("二叉排序树为空");

        BinarySortTreeNode nodeParent = null;//要删除节点的父节点
        BinarySortTreeNode node = null;//要删除的节点
        boolean flag = true;//要删除节点是父节点左子结点True,还是右子结点False

        if(root.getData()==data){//如果是根结点
            deleteRoot();//删除根结点
            return;
        }
        //获取要删除结点的父结点
        nodeParent = findParentByData(data);
        //获取要删除结点,并用flag记录结点时父结点的左子结点还是右子结点
        if(nodeParent.getLeft()!=null&&nodeParent.getLeft().getData()==data){
           node = nodeParent.getLeft();
           flag = true;
        }else{
            node = nodeParent.getRight();
            flag = false;
        }
        if(node==null){
            System.out.println("没有找到要删除的节点");
            return;
        }else{
            deleteNode(node,nodeParent,flag);
        }

    }

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

    /**
     * 通过数组添加元素到二叉树
     * @param arr
     */
    public void addArrayToBinarySortTree(int[] arr){
        for(int i : arr){
            BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(i);
            add(binarySortTreeNode);
        }
    }
}

4. 平衡二叉树(AVT)

平衡二叉树,就是更完善的二叉排序树,代码也是在二叉排序基础上添加了一些逻辑

  1. 平衡二叉树(AVL),又名平衡二叉搜索树,保证查询效率较高
  2. 它是一课空树或它的左右两个子树的高度差的绝对值小于等于1,并且左右两个子树都是一课平衡二叉树。满足这些特点就是平衡二叉树。(下图就是一课树,右子树高度比左子树高2,不满足,所以进行左旋转操作,后面会讲)
  1. 常用实现算法有红黑树、AVL算法、替罪羊树、Treap、伸展树等

为什么要有平衡二叉树

java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_数据结构_02

  1. 当我们要将一个递增的序列转换成二叉排序树时,那么会变成一个效率很低的单链表,因为需要判断左右子树
  2. 而平衡二叉树可以解决此类问题

左旋转(以右子树比左子树高度,高1以上,比如2,就需要左旋转,反之右旋转)


右旋转


双旋转

  1. 某些序列,无法通过左旋转,或右旋转,转变为平衡二叉树,此时需要双旋转

代码,只贴出和二叉排序树不同的代码块

  1. 运行效果
  1. 代码(以下指出添加的代码,和修改的代码)
/**
     * 返回左子树高度
     */
    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(){
        return Math.max(left == null?0:left.height(),right == null?0:right.height())+1;
    }

    /**
     * 以当前结点为根结点进行左旋转
     */
    public void leftRotate(){
        //1.创建新结点,保存当前根结点
        BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(data);
        //2.左子树变为新结点左子树
        binarySortTreeNode.setLeft(left);
        //3.右子树的左子树变为新结点的右子树
        binarySortTreeNode.setRight(right.getLeft());
        //4.让右子树上移成为根结点
        data = right.getData();
        right = right.getRight();
        //5.新结点作为左子树
        left = binarySortTreeNode;
    }

    /**
     * 以当前结点为根结点进行右旋转
     */
    public void rightRotate(){
        //1.创建新结点,保存当前根结点
        BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(data);
        //2.右子树变为新结点右子树
        binarySortTreeNode.setRight(right);
        //3.左子树的右子树变为新结点的左子树
        binarySortTreeNode.setLeft(left.getRight());
        //4.让左子树上移成为根结点
        data = left.getData();
        left = left.getLeft();
        //5.新结点作为右子树
        right = binarySortTreeNode;
    }

    /**
     * 二叉排序树添加结点方法
     * @param node 一个结点
     */
    public void add(BinarySortTreeNode node){
        if(node == null) return;//如果结点为null,不进行添加
        //如果要添加结点比当前结点小,那么添加到左子结点
        if(node.getData()<this.data){
            if(this.left==null){//如果左子结点为空
                this.left = node;//添加到左子结点
            }else{//如果不为空
                this.left.add(node);//进行递归,让左子树添加
            }
        }else if(node.getData()>this.data){//如果要添加结点比当前结点大,那么添加到右子结点
            if(this.right==null){//如果右子结点为空,将结点添加
                this.right = node;
            }else {//不为空
                this.right.add(node);//递归到右子树添加
            }
        }else{//如果要添加结点等于当前结点,添加到左右都可以
            if(this.left==null){
                this.left = node;
            }else if(this.right == null){
                this.left = 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();
            }
            return;
        }
    }

二、赫夫曼树

  1. 给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称为最优二叉树,又称哈夫曼树(Huffman Tree),或霍夫曼树或赫夫曼树
  2. 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
  1. 13这个结点,权值是13,根结点层数为1的情况下,此结点的路径长度是2
  2. 这并不是一课赫夫曼树,请继续往下看

路径和路径长度

  1. 一棵树中,一个结点往下可以到达的孩子或孙子结点之间的通路,称为路径。
  2. 通路中分支的数目称为路径长度,若根节点层数为1,则根节点到第L层结点的路径长度为L-1
  1. 根结点层数为1,13这个结点层数为3,3-1=2,那么13这个结点路径长度为2

结点的权和带权路径长度

  1. 权:树中结点赋予一个具有某种含义的数值,称为该结点的权
  2. 结点带权路径长度:根节点到该结点之间的路径长度与该结点的权的乘积
  1. 依然以上面的图片为例,13这个结点带权路径长度就是路径长度 *=2 *13=26

树的带权路径长度

  1. 所有叶子结点的带权路径长度之和,即为WPL(weighted path length),权值越大的结点离根结点越近的二叉树才是最优二叉树。
  2. WPL最小的就是赫夫曼树
  1. 树的带权路径长度wpl是评价一课二叉树是否为赫夫曼树的指标,wpl最小的就是最赫夫曼树
  2. 树的wpl就是所有叶子节点的带权路径长度相加后的结果
  3. 上图可以看出,中间的树,满足权值越大的叶子结点离根节点越近(层数越靠近根结点层数)的条件,那么它就是最优二叉树

1. 创建赫夫曼树

如何创建赫夫曼树

  1. 假定数列{13,7,8,3,29,6,1}要转换成一棵赫夫曼树
  2. 先从小到大排序,将每一个数据(就是一个结点),看成是一颗最简单的二叉树
  3. 取出根结点权值最小的两颗二叉树(第一次取,就是1和3这两颗)
  4. 组成一棵新的二叉树,此时新二叉树的根结点权值是前面两颗二叉树根结点的权值之和(第一次就是1和3合并,根结点权值为1+3=4,那么此时就有了一颗{1,4,3}的二叉树)
  5. 最后再将新二叉树,以根结点的权值大小再次排序(第一次就是{1,4,3}这颗和{6}这颗),然后不断重复3-4-5的步骤,直到数列中,所有数据都被处理,就得到了赫夫曼树

代码

  1. 运行效果
  1. 代码
import java.util.ArrayList;
import java.util.Collections;

public class Test {

    public static void main(String[] args) {
        int arr[] = {13,7,8,3,29,6,1};
        HuffmanTreeNode huffmanTree = createHuffmanTree(arr);
        System.out.println("====前序遍历结果====");
        preOrder(huffmanTree);
    }

    /**
     *  创建赫夫曼树
     * @param arr 序列
     */
    public static HuffmanTreeNode createHuffmanTree(int[] arr){
        //1.将arr中每个元素构建成结点,放入一个集合中
        ArrayList<HuffmanTreeNode> huffmanTreeNodes = new ArrayList<>();
        for(int value:arr){
            huffmanTreeNodes.add(new HuffmanTreeNode(value));
        }
        //不断重复 取结点---构成新二叉树----重新排序,直到集合中只剩下一个结点
        while(huffmanTreeNodes.size()>1){
            //2.排序,使用Collections接口的sort方法,对集合排序,要求集合中元素,必须实现Comparable接口,并从写compareTo方法,至于从小到大排,还是从大到小排,取决于compareTo方法的实现
            Collections.sort(huffmanTreeNodes);
            //3. 取出根结点最小的两颗二叉树,构建成新二叉树,然后删除最小两颗二叉树,将新二叉树放集合中,然后排序
            HuffmanTreeNode leftNode = huffmanTreeNodes.get(0);//取出集合第一个,也就是最小的
            HuffmanTreeNode rightNode = huffmanTreeNodes.get(1);//取出第二个,也就是第二小的
            HuffmanTreeNode parent = new HuffmanTreeNode(leftNode, rightNode);//然后构建一个新二叉树
            huffmanTreeNodes.remove(leftNode);//删除
            huffmanTreeNodes.remove(rightNode);//删除
            huffmanTreeNodes.add(parent);//将新二叉树放进集合
            Collections.sort(huffmanTreeNodes);//排序
        }

        return huffmanTreeNodes.get(0);
    }
    /**
     * 前序遍历赫夫曼树
     */
    public static void preOrder(HuffmanTreeNode node){
        if(node == null){
            System.out.println("赫夫曼树为空树");
            return;
        }
        node.preOrder();
    }
}

/**
 * 赫夫曼树结点类
 * 继承Comparable接口,用来比较结点之间的权值,需要实现compareTo方法
 */
class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
    private int value;//结点权值
    private HuffmanTreeNode left;//左子结点
    private HuffmanTreeNode right;//右子结点

    /**
     * 根据权值直接构建结点
     * @param value
     */
    public HuffmanTreeNode(int value) {
        this.value = value;
    }

    /**
     * 根据左右结点,构建新的二叉树,权值为左右结点的权值之和
     * @param left
     * @param right
     */
    public HuffmanTreeNode(HuffmanTreeNode left,HuffmanTreeNode right){
        this.left = left;
        this.right = right;
        this.value = left.getValue() + right.getValue();
    }

    @Override
    public int compareTo(HuffmanTreeNode o) {
        //从小到大排序,至于为什么,百度一下Comparable接口
        return this.value - o.value;
        //从大到小排
        //return -(this.value - o.value);
    }

    /**
     * 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
     */
    public void preOrder(){
        //1. 先输出当前节点
        System.out.println(this+" ");
        //2. 如果左子树存在,遍历左子树
        if(this.left!=null){
            this.left.preOrder();
        }
        //3. 如果右子树存在,遍历右子树
        if(this.right!=null){
            this.right.preOrder();
        }
    }

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

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public HuffmanTreeNode getLeft() {
        return left;
    }

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

    public HuffmanTreeNode getRight() {
        return right;
    }

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


}

2. 赫夫曼编码

  1. 又称哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,一种程序算法
  2. 是赫夫曼树在电讯通讯中的经典应用之一
  3. 广泛应用于数据文件压缩,压缩率在20%~90%之间
  4. 是可变字长编码(VLC的一种),Huffman于1952年提出一种编码方法,称为最佳编码

首先看一下定长编码方式,就是将字符转换成Ascii码,然后取到对应2进制编码,传输


变长编码,就是统计每个字符出现的次数,次数越多,编码越小,然后按照编码,将其组成一串2进制编码,但是因为编码的前缀重复性,比如编码1和编码10,计算机不知道什么时候按1解析,什么时候按10解析,也就是说,不能匹配到重复编码


赫夫曼编码,将字符出现次数,作为权值,出现的越多,权值越大,根据权值构建赫夫曼树

java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_java_03

  1. 根据权值创建赫夫曼树,向左的路径为0,向右的路径为1
  1. 然后根据创建好的赫夫曼树,给各字符规定编码,可以发现,每个字符的编码,都不是其它字符编码的前缀,比如o的编码是1000,u的编码10010,如果o的编码是100,那么确实是10010的前缀,但是此编码,不会出现这种情况,那么赫夫曼编码,满足前缀编码的特性,不会造成匹配多义性
  1. 额外的,如果出现权值相同的情况,比如有4个权值为4的二叉树,那么它们的位置并不固定,这样生成的赫夫曼树不一样,编码也完全不一样,但是wpl是一样的,所以如果出现这种情况也无伤大雅,压缩最后的结果长度,还是一样的

实现思路和代码

  1. 实现思路
  2. 赫夫曼树的结点,除了存放权值、左右子树,还要存放数据
  3. 得到"i like like like java do you like a java"对应的byte数组
  4. 生成结点,将结点放到List
  5. 创建赫夫曼树
  6. 获取赫夫曼编码,
  7. 获取字符串对应二进制编码,也就是压缩操作(压缩对应的byte数组,返回的也是byte数组)
  8. 解码,就是解压操作
  9. 运行效果
  10. 代码
import java.util.*;

public class Test {

    public static void main(String[] args) {
        String str = "i like like like java do you like a java";
        //> > 2. 得到"i like like like java do you like a java"对应的byte数组
        byte[] strBytes = str.getBytes();

        System.out.println("源字符串和源字节数组==========================");
        System.out.println(str);
        System.out.println("压缩前字节数组"+Arrays.toString(strBytes));

        //获取压缩后字节数组和赫夫曼树编码表
        List<Object> list = HuffmanZip(strBytes);
        byte[] zip = (byte[]) list.get(0);//获取压缩后的字节数组
        HashMap<Byte, String> byteStringHashMap =(HashMap<Byte, String>) list.get(1);//获取赫夫曼编码表
       
        //> > 7. 解码,就是解压操作
        byte[] bytes = huffmanUnZip(zip, byteStringHashMap);//获取解压后字节数组

        System.out.println("最终解压效果"+new String(bytes));
    }

    /**
     * 将字节数组压缩,返回压缩编码后的字节数组
     * @param strBytes 初始的字节数组
     * @return 压缩后的字节数组
     */
    public static List<Object> HuffmanZip(byte[] strBytes){
        //> > 3. 生成结点,将结点放到List
        //> > 4. 创建赫夫曼树
        HuffmanTreeNode huffmanTree = createHuffmanTree(strBytes);

        //> > 5. 获取赫夫曼编码
        HashMap<Byte, String> byteStringHashMap = HuffmanCode(huffmanTree);

        System.out.println("每个字符对应的编码为==============================");
        for(Map.Entry<Byte,String> entry:byteStringHashMap.entrySet()){
            System.out.println(entry.getKey()+"    "+entry.getValue());
        }

        //> > 6. 获取字符串对应二进制编码,也就是压缩操作(压缩对应的byte数组,返回的也是byte数组)
        byte[] zip = zip(strBytes, byteStringHashMap);

        System.out.println("编码后的字符数组======================================");
        System.out.println(Arrays.toString(zip));

        //封装结果集,将压缩后字节数组,和赫夫曼编码表封装返回
        ArrayList<Object> objects = new ArrayList<>();
        objects.add(zip);
        objects.add(byteStringHashMap);
        return objects;
    }

    /**
     * 将压缩后的字节数组,解压
     * @param zip 压缩后字节数组
     * @param byteStringHashMap 赫夫曼编码表
     * @return 解压后的字节数组
     */
    public static byte[] huffmanUnZip(byte[] zip,HashMap<Byte, String> byteStringHashMap){
        //字节数组解压成二进制编码
        String s = byteToString(zip);
        System.out.println("解压后获取的二进制编码"+s);

        //将字符串按照赫夫曼编码,进行解码
        HashMap<String, Byte> stringByteHashMap = new HashMap<>();//先将赫夫曼编码表进行反转,就是反向查询,比如32->10  转换成 10->32
        for(Map.Entry<Byte,String> entry:byteStringHashMap.entrySet()){
            stringByteHashMap.put(entry.getValue(), entry.getKey());
        }

        System.out.println("将赫夫曼编码表进行反转==============================");
        for(Map.Entry<String,Byte> entry:stringByteHashMap.entrySet()){
            System.out.println(entry.getKey()+"    "+entry.getValue());
        }

        //通过反转后的赫夫曼编码表,将二进制串解析为正常字符,保存到list中
        ArrayList<Byte> list = new ArrayList<>();
        for(int i = 0;i<s.length();){//依次遍历二进制码,注意这里没有i++,因为i的增长在代码中控制
            int count = 1;//用来记录当前判断了多少个二进制码,因为赫夫曼编码的长度都不一样
            boolean flag = true;//while循环的条件
            Byte b = null;//用来保存通过二进制码找到的对应值
            while(flag){
                String key = s.substring(i,i+count);//每次都从当前索引位置,截取count个二进制码
                b = stringByteHashMap.get(key);//匹配map表中的值
                if(b==null){//如果没有匹配到,count++
                    count++;
                }else{//如果匹配到了,结束while循环,进行下一个字符匹配
                    flag = false;
                }
            }
            list.add(b);//将匹配到的值加入list
            i+=count;//让i从匹配完成后的位置开始
        }
        //将list中数据放在byte数组中返回
        byte b[] = new byte[list.size()];
        for(int i =0 ;i< b.length;i++){
            b[i] = list.get(i);
        }

        return b;
    }


    /**
     * > > 4. 创建赫夫曼树
     * @param bytes 字节数组,存储了字符串的对应字节,我们需要统计每个byte出现次数,当做权值,构建赫夫曼树
     */
    public static HuffmanTreeNode createHuffmanTree(byte[] bytes){
        //1.将arr中每个元素构建成结点,放入一个集合中
        ArrayList<HuffmanTreeNode> huffmanTreeNodes = new ArrayList<>();
        //存储每一个byte出现的次数,获取权值
        HashMap<Byte, Integer> counts = new HashMap<>();
        for(byte data:bytes){
            Integer count = counts.get(data);//从map中获取byte对应的值
            if(count == null){//如果当前map还没有统计过当前byte的出现次数,那么进行第一次存储
                counts.put(data,1);
            }else{
                counts.put(data,count+1);//如果当前map已经统计过,那么次数加1
            }
        }//此时每个字符的权值已经获取完毕
        //把每一个key-value队,转成一个结点对象加入到集合中
        for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
            huffmanTreeNodes.add(new HuffmanTreeNode(entry.getKey(),entry.getValue()));
        }

        //不断重复 取结点---构成新二叉树----重新排序,直到集合中只剩下一个结点
        while(huffmanTreeNodes.size()>1){
            //2.排序,使用Collections接口的sort方法,对集合排序,要求集合中元素,必须实现Comparable接口,并从写compareTo方法,至于从小到大排,还是从大到小排,取决于compareTo方法的实现
            Collections.sort(huffmanTreeNodes);
            //3. 取出根结点最小的两颗二叉树,构建成新二叉树,然后删除最小两颗二叉树,将新二叉树放集合中,然后排序
            HuffmanTreeNode leftNode = huffmanTreeNodes.get(0);//取出集合第一个,也就是最小的
            HuffmanTreeNode rightNode = huffmanTreeNodes.get(1);//取出第二个,也就是第二小的
            HuffmanTreeNode parent = new HuffmanTreeNode(leftNode, rightNode);//然后构建一个新二叉树
            huffmanTreeNodes.remove(leftNode);//删除
            huffmanTreeNodes.remove(rightNode);//删除
            huffmanTreeNodes.add(parent);//将新二叉树放进集合
            Collections.sort(huffmanTreeNodes);//排序
        }

        return huffmanTreeNodes.get(0);
    }

    /**
     * > > 5. 获取赫夫曼编码
     * 生成赫夫曼树对应编码
     * @param node
     */
    public static HashMap<Byte,String> HuffmanCode(HuffmanTreeNode node){

        if(node == null) throw new RuntimeException("赫夫曼树为空!!!");

        //根据路径拼接每个叶子节点的赫夫曼编码
        StringBuilder stringBuilder = new StringBuilder();

        //将赫夫曼编码表放在Map中,比如空格,32 对应编码为01
        HashMap<Byte, String> huffmanCodes = new HashMap<>();

        getCodes(node,"",stringBuilder,huffmanCodes);

        return huffmanCodes;
    }

    /**
     * > > 5. 获取赫夫曼编码
     * 将结点的所有叶子节点的赫夫曼编码得到,并放入map中
     * @param node 赫夫曼树的结点
     * @param code 路径编码,左路径是0,右路径是1
     * @param stringBuilder 拼接路径
     * @param huffmanCodes 最终结果存放到这个map
     */
    private static void getCodes(HuffmanTreeNode node,String code,StringBuilder stringBuilder,HashMap<Byte,String> huffmanCodes){

        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);

        stringBuilder2.append(code);//将code路径拼接

        if(node != null){//如果node!=null,说明还可以遍历,这个结点不是一个无效结点
            //判断当前结点是否是叶子结点,如果不是叶子结点,它的data是null
            if(node.getData() == null){//如果是非叶子结点
                //向左递归
                getCodes(node.getLeft(),"0",stringBuilder2,huffmanCodes);
                //向右递归
                getCodes(node.getRight(),"1",stringBuilder2,huffmanCodes);
            }else{//如果是叶子结点
                //将其添加到hashMap中
                huffmanCodes.put(node.getData(),stringBuilder2.toString());
            }
        }
    }

    /**
     * > > 6. 获取字符串对应二进制编码,也就是压缩操作
     * @param bytes 压缩前字节数组
     * @param huffmanCodes 赫夫曼树编码表
     * @return 压缩后的字节数组
     */
    public static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
        //用于拼接压缩后的二进制
        StringBuilder stringBuilder = new StringBuilder();
        for(byte b : bytes){//遍历byte数组
            //为每个byte查询相应编码,然后拼接到stringBuilder中
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("压缩后的二进制编码"+stringBuilder.toString());

        //将二进制码,转换成byte[],如果不转,反而变大了,起不到压缩效果
        //获取byte[]长度,因为每个byte是8位,所以我们将stringBuilder/8就可以了
        int len;
//        if(stringBuilder.length()%8 == 0){//如果取余8==0,说明通过length/8获取的长度,刚好存放
//            len = stringBuilder.length()/8;
//        }else{//如果不等于0,说明/8后,还有余下的没地方存放,那么我们多加一个长度,用来存放这些多出来的
//            len = stringBuilder.length()/8+1;
//        }
        //下面这句的效果,等同于上面注释的代码效果
        len = (stringBuilder.length()+7)/8;


        //创建,存储压缩后的byte数组
        byte[] by = new byte[len];
        int index = 0;//by数组下标
        for(int i = 0;i<stringBuilder.length();i+=8){//字节数组,一个元素可以存储8为,所以步长为8
            String strByte;
            if(i+8>stringBuilder.length())//如果不够8为了,直接将当前下标到末尾的二进制代码取出来
                strByte = stringBuilder.substring(i);
            else//如果还够8位,直接从当前下标取8位
                strByte = stringBuilder.substring(i,i+8);
            //将取出的字串,放在字节数组中
            by[index] = (byte)Integer.parseInt(strByte,2);
            index++;//下标自增
        }

        return by;
    }


    /**
     * > > 7. 解码,就是解压操作
     * 将字符数组,转换成对应二进制字符串
     * @return 按补码形式返回
     */
    public static String byteToString(byte[] zip){
        StringBuilder stringBuilder = new StringBuilder();//保存最终结果
        boolean flag = true;//用于标记一个字节是否需要补高位
        for(int i = 0;i<zip.length;i++){
            int intNum = zip[i];//将byte转成一个int值

            flag = (i != zip.length-1);//如果是最后一个字节,无需补高位

            if(flag){//如果是true,我们需要补高位
                intNum |= 256;//按位与,举个例子:256的二进制是 1 0000 0000 ,1的二进制是0000 0001 | 1 0000 0000 = 1 0000 0001
            }

            String s = Integer.toBinaryString(intNum);//返回i对应的二进制补码
            //如果是true,我们取字符串后8位二进制
            if(flag){
                stringBuilder.append(s.substring(s.length()-8));
            }else{//如果不是,无需补高位,那么直接连接即可
                stringBuilder.append(s);
            }
        }


        return stringBuilder.toString();
    }

    /**
     * 前序遍历赫夫曼树
     */
    public static void preOrder(HuffmanTreeNode node){
        if(node == null){
            System.out.println("赫夫曼树为空树");
            return;
        }
        node.preOrder();
    }
}

/**
 * 赫夫曼树结点类
 * 继承Comparable接口,用来比较结点之间的权值,需要实现compareTo方法
 */
class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
    private Byte data;//存放数据本身,比如'a' = 97
    private int value;//结点权值
    private HuffmanTreeNode left;//左子结点
    private HuffmanTreeNode right;//右子结点

    /**
     * 根据权值和数据直接构建结点
     * @param value
     */
    public HuffmanTreeNode(Byte data, int value) {
        this.data = data;
        this.value = value;
    }

    /**
     * 根据左右结点,构建新的二叉树,权值为左右结点的权值之和
     * @param left
     * @param right
     */
    public HuffmanTreeNode(HuffmanTreeNode left,HuffmanTreeNode right){
        this.left = left;
        this.right = right;
        this.value = left.getValue() + right.getValue();
    }

    @Override
    public int compareTo(HuffmanTreeNode o) {
        //从小到大排序,至于为什么,百度一下Comparable接口
        return this.value - o.value;
        //从大到小排
        //return -(this.value - o.value);
    }

    /**
     * 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
     */
    public void preOrder(){
        //1. 先输出当前节点
        System.out.println(this+" ");
        //2. 如果左子树存在,遍历左子树
        if(this.left!=null){
            this.left.preOrder();
        }
        //3. 如果右子树存在,遍历右子树
        if(this.right!=null){
            this.right.preOrder();
        }
    }

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

    public Byte getData() {return data;}

    public void setData(Byte data) {this.data = data;}

    public int getValue() {return value;}

    public void setValue(int value) {this.value = value;}

    public HuffmanTreeNode getLeft() {return left; }

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

    public HuffmanTreeNode getRight() {return right; }

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


}

3. 使用赫夫曼编码压缩解压文件

这里只是介绍如何压缩文件与解压,解压文件时,需要根据不同文件类型来响应式解码,比如高清图片,中文字符,都需要特殊处理,与赫夫曼树编码无关,这里不做介绍

java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_链表_04

  1. 运行效果
  1. 代码(只贴出了修改过和新增的代码)
public static void main(String[] args) {
        String srcFile = "E://a.txt";
        String dstFile = "E://a.zip";
        zipFile(srcFile,dstFile);
        System.out.println("压缩文件完成");

        String zipFile = "E://a.zip";
        String dstFile2 = "E://unZip_a.txt";
        unZipFile(zipFile,dstFile2);

    }

    /**
     * 压缩文件
     * @param srcFile 要压缩的文件路径
     * @param dstFile 压缩后保存路径
     */
    public static void zipFile(String srcFile,String dstFile){
        //创建文件输入流
        FileInputStream is = null;
        //创建文件输出流
        OutputStream os = null;
        //创建一个和文件输出流关联的对象输出流,这样我们可以利用对象流将一个对象写入压缩文件(一起输出),比如我们将赫夫曼编码表输出出去,那么日后想要解压,就可以直接拿到编码表解压
        ObjectOutputStream objectOutputStream=null;
        try {
            is = new FileInputStream(srcFile);//获取指定文件输入流
            byte[] b = new byte[is.available()];//创建对应字节数组
            is.read(b);//将文件内容读取到字节数组中

            //压缩,获取赫夫曼编码表和压缩后字节数组
            List<Object> list = HuffmanZip(b);
            byte[] zip = (byte[]) list.get(0);//获取压缩后的字节数组
            HashMap<Byte, String> byteStringHashMap =(HashMap<Byte, String>) list.get(1);//获取赫夫曼编码表

            //创建文件输出流,存放压缩后文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            objectOutputStream = new ObjectOutputStream(os);
            //首先将压缩后字节数组写入输出流,这样就完成了压缩文件的创建
            objectOutputStream.writeObject(zip);
            //以对象流的方式,将赫夫曼编码写入压缩文件,恢复源文件时使用
            objectOutputStream.writeObject(byteStringHashMap);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                objectOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                is.close();//关闭流
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
    }

    public static void unZipFile(String zipFile,String dstFile){
        //创建文件输入流
        InputStream is = null;
        //定义和文件输入流关联的对象输入流
        ObjectInputStream ois = null;
        //文件输出流
        OutputStream os = null;
        try {
            is = new FileInputStream(zipFile);
            ois = new ObjectInputStream(is);
            //获取压缩后文件
            byte[] zip = (byte[])ois.readObject();
            //获取赫夫曼编码表
            HashMap<Byte,String> huffmanCodes = (HashMap<Byte,String>)ois.readObject();

            //解压,获取解压后字节数组
            byte[] unZip = huffmanUnZip(zip, huffmanCodes);
            //创建文件输出流
            os = new FileOutputStream(dstFile);
            //将解压后字节数组,写入输出流,完成解压操作,恢复源文件
            os.write(unZip);

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

三、多叉树,多路查找树

java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_链表_05


java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_数据结构_06


java维护树形结构添加某一个节点时如何编写节点序号 java树结构优化效率_java_07

1. 2-3树

  1. 最简单的B树结构,特点如下(就是每个结点可以保存两个数据,最多可以有3个子结点,但是需要满足如下规则)
  1. 2-3树的所有叶子结点都在同一层.(只要是B树都满足这个条件)
  2. 有两个子结点的结点叫二结点,二结点要么没有子结点,要么有两个子结点
  3. 有三个子结点的结点叫三结点,三结点要么没有子结点,要么有3个结点
  4. 2-3树是由二结点和三结点构成的树


另外还有一些2-3-4树等等,和2-3树差不多,就是每个结点最多存3个元素,最多有4个子结点


2. B树,B+树,B*树

  1. B-tree,B树,Balanced-tree,平衡的意思,有人翻译成B-树,容易让人误认为叫B减树,或者是B杠树,和B树不一样。其实就是B树
  1. B+树
  1. B*树