[java]-算法与数据结构-第十章-树结构

十、树结构

1. 基础部分

1)引出

  1. 数组存储方式的分析
    • 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
    • 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低

在这里插入图片描述

  1. 链式存储方式的分析

    • 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。
    • 缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
      在这里插入图片描述
  2. 树存储方式的分析
    能提高数据存储,读取的效率,比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

在这里插入图片描述

2)常用术语

  1. 节点
  2. 根节点
  3. 父节点
  4. 组节点
  5. 叶子节点(没有子节点的节点)
  6. 节点的权(节点的值)
  7. 路径(从root 节点找到该节点的路线)
  8. 子树
  9. 树的高度(最大层数和)
  10. 森林:多棵子树构成森林

在这里插入图片描述

2. 二叉树

1)概念

  1. 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树

  2. 二叉树的子节点分为左节点和右节点。
    在这里插入图片描述

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

  4. 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
    在这里插入图片描述

2)遍历

说明:

  1. 前序遍历:先输出父节点,再遍历左子树和右子树
  2. 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
  3. 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点

小结:看输出父节点的顺序,就确定是前序,中序还是后序

遍历步骤:

  1. 创建二叉树
  2. 前序遍历
    • 先输出当前节点(初始的时候是root节点)
    • 如果左子节点不为空,则递归继续前序遍历
    • 如果右子节点不为空,则递归继续前序遍历
  3. 中序遍历
    • 如果当前节点的左子节点不为空,则递归中序遍历
    • 输出当前节点
    • 如果当前节点的右子节点不为空,则递归中序遍历
  4. 后序遍历
    • 如果当前节点的左子节点不为空,则递归后序遍历
    • 如果当前节点的右子节点不为空,则递归后序遍历
    • 输出当前节点

代码

public class 二叉树 {
    
    
    public static void main(String[] args) {
    
    

        BinaryTree binaryTree = new BinaryTree();
        HeroNode node1 = new HeroNode(1, "A");
        HeroNode node2 = new HeroNode(2, "B");
        HeroNode node3 = new HeroNode(3, "C");
        HeroNode node4 = new HeroNode(4, "D");
        HeroNode node5 = new HeroNode(5, "E");
        binaryTree.setRoot(node1);
        HeroNode root = binaryTree.getRoot();
        root.setLeft(node2);
        root.setRight(node3);
        node3.setLeft(node5);
        node3.setRight(node4);
        System.out.println("---------前序遍历---------");
        binaryTree.preOrder();  // 1,2,3,4
        System.out.println("---------中序遍历---------");
        binaryTree.infixOrder(); // 2,1,3,4
        System.out.println("---------后序遍历---------");
        binaryTree.postOrder(); // 2,4,3,1

    }
}

  • BinaryTree
// 二叉树
class BinaryTree {
    
    
    private HeroNode root;

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

    public HeroNode getRoot() {
    
    
        return root;
    }

    // 前序遍历
    public void preOrder() {
    
    
        if (this.root != null) {
    
    
            this.root.preOrder();
        } else {
    
    
            System.out.println("二叉树为空,无法遍历");
        }
    }

    public void infixOrder() {
    
    
        if (this.root != null) {
    
    
            this.root.infixOrder();
        } else {
    
    
            System.out.println("二叉树为空,无法遍历");
        }
    }

    public void postOrder() {
    
    
        if (this.root != null) {
    
    
            this.root.postOrder();
        } else {
    
    
            System.out.println("二叉树为空,无法遍历");
        }
    }

}
  • HeroNode
// node
class HeroNode {
    
    
    private int NO;
    private String name;
    private HeroNode left;
    private HeroNode right;

    public HeroNode(int NO, String name) {
    
    
        super();
        this.NO = NO;
        this.name = name;
    }

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

    public int getNO() {
    
    
        return NO;
    }

    public void setNO(int NO) {
    
    
        this.NO = NO;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public HeroNode getLeft() {
    
    
        return left;
    }

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

    public HeroNode getRight() {
    
    
        return right;
    }

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

    // 前序遍历
    public void preOrder() {
    
    
        // 1. 先输出父节点
        System.out.println(this);
        // 2. 递归左子树
        if (this.left != null) {
    
    
            this.left.preOrder();
        }
        // 3. 递归右子树
        if (this.right != null) {
    
    
            this.right.preOrder();
        }
    }

    // 中序遍历
    public void infixOrder() {
    
    
        // 1. 递归左子树
        if (this.left != null) {
    
    
            this.left.infixOrder();
        }
        // 2. 输出父节点
        System.out.println(this);
        // 3. 向右子树遍历
        if (this.right != null) {
    
    
            this.right.infixOrder();
        }
    }


    // 后序遍历
    public void postOrder() {
    
    
        // 1. 递归左子树
        // 2. 向右子树遍历
        // 3. 输出父节点
        if (this.left != null) {
    
    
            this.left.postOrder();
        }
        if (this.right != null) {
    
    
            this.right.postOrder();
        }
        System.out.println(this);
    }
}

3)查找

  • 代码

    // 前序查找
    public HeroNode preSearch(int value) {
    
    
        // 1. 比较当前节点是不是
        if (this.getNO() == value) {
    
    
            return this;
        }
        // 2. 判断左节点
        HeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.preSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 3. 判断右节点
        if (this.right != null) {
    
    
            resNode = this.right.preSearch(value);
        }
        return resNode;
    }

    // 中序查找
    public HeroNode infixSearch(int value) {
    
    

        HeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.infixSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 2. 判断当前节点
        if (value == this.NO) {
    
    
            return this;
        }
        // 3. 向右
        if (this.right != null) {
    
    
            resNode = this.right.infixSearch(value);
        }
        return resNode;
    }

    // 后序查找
    public HeroNode postSearch(int value) {
    
    

        // 1. 向左遍历
        HeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.postSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 2. 向右
        if (this.right != null) {
    
    
            resNode = this.right.postSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 3. 判断当前节点
        if (value == this.NO) {
    
    

            return this;
        }
        return null;
    }

4)删除

  1. 要求

    如果删除的节点是叶子节点,则删除该节点

    如果删除的节点是非叶子节点,则删除该子树.

    测试,删除掉5号叶子节点和3号子树.

  • HeroNode
    // 删除
    public void delete(int NO) {
    
    
        // 1. 左节点
        if (this.left != null && this.left.getNO() == NO) {
    
    
            this.left = null;
            return;
        }
        // 2. 右节点
        if (this.right != null && this.right.getNO() == NO) {
    
    
            this.right = null;
            return;
        }
        // 3. 递归删除
        if(this.left != null){
    
    
            this.left.delete(NO);
        }
        if(this.right != null){
    
    
            this.right.delete(NO);
        }

    }
  • BinaryTree
    // 删除
    public void delete(int no){
    
    
        if(this.root == null){
    
    
            System.out.println("此树为空");
            return;
        }else {
    
    
            if(this.root.getNO() == no){
    
    
                this.root = null;
            }else {
    
    
                this.root.delete(no);
            }
        }
    }

3. 顺序存储二叉树

1)概念

  1. 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看右面的示意图。
    在这里插入图片描述

  2. 特点

    • 顺序二叉树通常只考虑完全二叉树
    • 第n个元素的左子节点为 2*n +1;
    • 第n个元素的右子节点为 2*n +2;
    • 第n个元素的父节点为 (n-1)/2;

2)遍历

  • 代码
public class 数组二叉树 {
    
    
    public static void main(String[] args) {
    
    
        int[] arr = {
    
    1, 2, 3, 4, 5, 6, 7};
        ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
        arrayBinaryTree.preOrder();
    }
}

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

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


    public void preOrder() {
    
    
        this.preOrder(0);
    }

    // 顺序存储
    // 前序  index 为数组下标
    public void preOrder(int index) {
    
    
        if (arr == null || arr.length == 0) {
    
    
            System.out.println("数组二叉树为空");
            return;
        }
        // 输出当前元素
        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);
        }
    }
}

4. 线索化二叉树

1)概念

  1. n个结点的二叉链表中含有n+1【公式2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
  2. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  3. 一个结点的前一个结点,称为前驱结点
  4. —个结点的后一个结点,称为后继结点

2)图解

在这里插入图片描述

说明:当线索化二叉树后,Node节点的属性left和righ,有如下情况:

  1. left指向的是左子树,也可能是指向的前驱节点.比如节点 ileft指向的左子树,而⑩节点的 left指向的就是前驱节点.
  2. right指向的是右子树,也可能是指向后继节点,比如①节点right指向的是右子树,而⑩节点的right指向的是后继节点.

3)实现

public class 线索二叉树 {
    
    
    public static void main(String[] args) {
    
    
        NewHeroNode node1 = new NewHeroNode(1, "A");
        NewHeroNode node2 = new NewHeroNode(3, "B");
        NewHeroNode node3 = new NewHeroNode(6, "C");
        NewHeroNode node4 = new NewHeroNode(8, "D");
        NewHeroNode node5 = new NewHeroNode(10, "E");
        NewHeroNode node6 = new NewHeroNode(14, "F");

        node1.left = node2;
        node1.right = node3;
        node2.left = node4;
        node2.right = node5;
        node3.left = node6;

        ThreeNodes threeNodes = new ThreeNodes();
        threeNodes.setRoot(node1);

        System.out.println(node5.getLeft());
        System.out.println(node5.getRight() );
        node1.infixOrder();


    }

}

// 实现线索化二叉树
class NewHeroNode {
    
    

    public int NO;
    public String name;
    public NewHeroNode left;
    public NewHeroNode right;
    public int leftType;
    public int rightType;


    public NewHeroNode(int NO, String name) {
    
    
        super();
        this.NO = NO;
        this.name = name;
    }

    // 前序遍历
    public void preOrder() {
    
    
        // 1. 先输出父节点
        System.out.println(this);
        // 2. 递归左子树
        if (this.left != null) {
    
    
            this.left.preOrder();
        }
        // 3. 递归右子树
        if (this.right != null) {
    
    
            this.right.preOrder();
        }
    }

    // 中序遍历
    public void infixOrder() {
    
    
        // 1. 递归左子树
        if (this.left != null) {
    
    
            this.left.infixOrder();
        }
        // 2. 输出父节点
        System.out.println(this);
        // 3. 向右子树遍历
        if (this.right != null) {
    
    
            this.right.infixOrder();
        }
    }

    // 后序遍历
    public void postOrder() {
    
    
        // 1. 递归左子树
        // 2. 向右子树遍历
        // 3. 输出父节点
        if (this.left != null) {
    
    
            this.left.postOrder();
        }
        if (this.right != null) {
    
    
            this.right.postOrder();
        }
        System.out.println(this);
    }

    // 前序查找
    public NewHeroNode preSearch(int value) {
    
    
        // 1. 比较当前节点是不是
        if (this.getNO() == value) {
    
    
            return this;
        }
        // 2. 判断左节点
        NewHeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.preSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 3. 判断右节点
        if (this.right != null) {
    
    
            resNode = this.right.preSearch(value);
        }
        return resNode;
    }

    // 中序查找
    public NewHeroNode infixSearch(int value) {
    
    

        NewHeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.infixSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 2. 判断当前节点
        if (value == this.NO) {
    
    
            return this;
        }
        // 3. 向右
        if (this.right != null) {
    
    
            resNode = this.right.infixSearch(value);
        }
        return resNode;
    }

    // 后序查找
    public NewHeroNode postSearch(int value) {
    
    

        // 1. 向左遍历
        NewHeroNode resNode = null;
        if (this.left != null) {
    
    
            resNode = this.left.postSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 2. 向右
        if (this.right != null) {
    
    
            resNode = this.right.postSearch(value);
        }
        if (resNode != null) {
    
    
            return resNode;
        }
        // 3. 判断当前节点
        if (value == this.NO) {
    
    

            return this;
        }
        return null;
    }

    // 删除
    public void delete(int NO) {
    
    
        // 1. 左节点
        if (this.left != null && this.left.getNO() == NO) {
    
    
            this.left = null;
            return;
        }
        // 2. 右节点
        if (this.right != null && this.right.getNO() == NO) {
    
    
            this.right = null;
            return;
        }
        // 3. 递归删除
        if (this.left != null) {
    
    
            this.left.delete(NO);
        }
        if (this.right != null) {
    
    
            this.right.delete(NO);
        }

    }
}

class ThreeNodes {
    
    
    public NewHeroNode pre = null;

    public NewHeroNode root = null;


    // node 为当前需要线索化的节点
    public void threadedNodes(NewHeroNode node) {
    
    
        if (node == null) {
    
    
            return;
        }
        // 1. 线索化左子树
        threadedNodes(node.getLeft());
        // 2. 线索化当前节点
        // 1) 先梳理当前节点的前驱节点
        if (node.getLeft() == null) {
    
    
            // 让当前节点的左指针指向前驱节点
            node.setLeft(pre);
            // 修改当前节点的左指针的类型
            node.setLeftType(1);
        }
        // 2) 处理后置节点
        if (pre != null && pre.getRight() == null) {
    
    
            pre.setRight(node);
            pre.setRightType(1);
        }
        // 每次处理完一个节点,当前节点为下一个节点的前驱节点
        pre = node;

        // 3. 线索化右子树
        threadedNodes(node.getRight());
    }

    public NewHeroNode getRoot() {
    
    
        return root;
    }

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

4)遍历

  • 代码
 // 遍历
    public void threadList() {
    
    
        // 定义一个变量存储当前节点
        NewHeroNode node = root;
        while (node != null) {
    
    
            // 循环找到 leftType == 1 的结点,前驱节点
            // 当 leftType == 1 时,说明该节点是按照线索化处理后的有效节点
            while (node.getLeftType() == 0){
    
    
                node = node.getLeft();
            }
            // 打印当前节点
            System.out.println(node);
            // 如果当前节点的右指针,指向的是后续节点,就一直输出
            while (node.getRightType() == 1){
    
    
                // 获取到当前节点的后续节点
                node = node.getRight();
                System.out.println(node);
            }
            // 替换遍历的节点
            node = node.getRight();

        }
    }

5. 堆排序

1)概念

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
  3. 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
  4. 大顶堆举例说明

在这里插入图片描述

在这里插入图片描述

对堆中的结点按层进行编号,映射到数组中就是下面这样
例如:arr[1] ≥ arr[2 * 1 + 1] && arr[1] ≥ arr[2 * 1 + 2] === 45 ≥ 20 && 45 ≥ 25

  1. 小堆顶举例
    在这里插入图片描述

2)思想

  1. 将待排序序列构建成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点
  3. 将其与末尾的元素进行交换,此时末尾就是最大值
  4. 将剩余的 n- 1个元素重新构造堆,重复上面步骤。

3)图解

  1. 构造初始堆,将给定无序序列构造成一个大顶堆,
    在这里插入图片描述

  2. 此时从最后一个非叶子结点开始(叶子结点自然不用调整,第一个非叶子结点 arr.length/2 -1 = 5/2-1 = 1,也就是下面的6结点)从左到右,从上到下进行调整。
    在这里插入图片描述

  3. 找到第三个非叶子结点4,由于9最大,1交换位置
    a

  4. 继续调整

在这里插入图片描述

  1. 将顶对元素与末尾元素进行交换,使得末尾元素最大,然后继续调整,再将堆顶元素与末尾元素交换,得到第二大元素,如此反复循环。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4)代码

   public static void main(String[] args) {
    
    
        int[] arr = {
    
    4, 6, 8, 5, 9};
        heapSort(arr);
    }

    // 堆排序的方法
    public static void heapSort(int arr[]) {
    
    
        /*adjustHeap(arr, 1, arr.length);
        System.out.println(Arrays.toString(arr));
        adjustHeap(arr, 0, arr.length);
        System.out.println(Arrays.toString(arr));*/

        // 1. 构建大 顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
    
    
            adjustHeap(arr, i, arr.length);
        }
        System.out.println(Arrays.toString(arr));
        // 2. 将堆顶元素与末尾元素进行交换, 将最大元素沉到数组末端
        int temp = 0;
        for (int j = arr.length - 1; j > 0; j--) {
    
    
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr, 0, j);
        }
        System.out.println(Arrays.toString(arr));
    }

    // 讲一个数组调整成一个大顶堆
    // 功能:完成将 以 i 对应的非叶子节点的数调整成大顶堆
    // 举例:arr = {4,6,8,5,9} i = 1,length = 5

    // arr 待调整的数组
    // i 非叶子结点在数组中的索引
    // length 对多少个元素进行调整
    public static void adjustHeap(int arr[], int i, int length) {
    
    
        int temp = arr[i]; // 取出当前元素 -->非叶子元素
        // 1. 开始调整
        // 左子节点, k 是 i的 左子节点
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
    
    
            // k+1 > length 不比了
            if (k + 1 < length && arr[k] < arr[k + 1]) {
    
     // 说明 左子节点 < 右子节点
                k++; // k指向 右子节点

            }
            // k此时为左结点 或者 右结点
            if (arr[k] > temp) {
    
     // 子节点 > 父节点
                arr[i] = arr[k]; // 父节点 变为 子节点
                i = k; // 非叶子结点 继续循环
            } else break;
        }
        // for循环结束后, 已经将 以 i为父节点的数的最大值,放在了最前面。
        arr[i] = temp;


    }

6. 赫夫曼树

1)概念

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

2)重要概念

  1. 路径和路径长度:

    在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。

    通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1

  2. 结点的权及带权路径长度:

    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。

    结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积

    例如:2 * 13 = 26;
    在这里插入图片描述

  3. 树的带权路径长度:

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NI0iyoM5-1651491300719)(https://secure2.wostatic.cn/static/syc9KrE5s3SqVDH32disos/image.png)]

3)图解

  1. 从小到大进行排序,将每一个数据,每个数据都是节点,每个节点可以看成最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树的根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小再次匹配,不断重复.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OYuaGSji-1651491300719)(https://secure2.wostatic.cn/static/kg3JJLnh9aEbBVkdrVUUok/image.png)]

4)代码

  • 代码
public class 赫夫曼树 {
    
    
    public static void main(String[] args) {
    
    
        int[] arr = {
    
    13, 7, 8, 3, 29, 6, 1};
        Node root = createHeffmanTree(arr);
        preNode(root);
    }

    // 创建赫夫曼树
    public static Node createHeffmanTree(int[] arr) {
    
    
        // 1. 遍历 arr 数组
        // 2. 将每一个构建成node
        // 3. 将每一个放入 arrayList中
        ArrayList<Node> nodes = new ArrayList<>();
        for (int value : arr) {
    
    
            nodes.add(new Node(value));
        }
        // 4. 排序 小 --> 大
        // 循环处理
        while (nodes.size() > 1) {
    
    
            Collections.sort(nodes);

            // 5. 取出权值最小的两个二叉树
            Node left = nodes.get(0);
            Node right = nodes.get(1);

            // 6. 构建新的二叉树
            Node parent = new Node(left.value + right.value);
            parent.left = left;
            parent.right = right;

            // 7. 从 arrayList中删除处理过的二叉树
            nodes.remove(left);
            nodes.remove(right);
            nodes.add(parent);
            System.out.println(nodes);
        }
        return nodes.get(0);
    }

    // 前序遍历
    public static void preNode(Node root){
    
    
        if(root == null){
    
    
            System.out.println("值为空");
        }else {
    
    
            root.preOrder();
        }
    }
}
// 创建节点类


class Node implements Comparable<Node> {
    
    
    int value; // 权
    Node left;  // 左子节点
    Node right; // 右子节点

    public Node(int value) {
    
    
        this.value = 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 +
                '}';
    }

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

7. 赫夫曼编码

1)概念

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

2)图解

  1. i like like like java do you like a java

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DSRiQoz7-1651491300720)(https://secure2.wostatic.cn/static/i92Yt13Bex8adYwZJEP5mb/image.png)]

  4. 根据赫夫曼树,给各个字符规定编码,向左的路径为 0,向右的路径为1。

    o:1000 u: 10010 d:100110 y: 100111 a : 110 k:1110 e: 1111 j:0000 v:0001 l:001 :01

  5. 按照上面的赫夫曼编码,我们的i like like like java do you like a java字符串对应的编码为(注意这里我们使用的无损压缩)
    101``01``001101111011110100110111101111010011011110111101000010000110011001111000011001111000100100100110111011011100100001100001110

  6. 长度为:133

    原来长度为 359,压缩了 62.9%

    此编码满足前缀编码,即字符的编码不能是其他字符编码的前缀,不会造成匹配的多义性,赫夫曼编码是无损的处理方式。

  7. 注意:

    这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼树编码也不一样,但是WPL是一样的,生成的长度都是一样的,都是最小的,

在这里插入图片描述

3)代码

  • Node类

class Node2 implements Comparable<Node2> {
    
    
    Byte data;
    int weight;
    Node2 left;
    Node2 right;

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

    // 前序遍历
    public void preOrder() {
    
    

        System.out.println(this);

        if (this.left != null) {
    
    
            this.left.preOrder();
        }
        if (this.right != null) {
    
    
            this.right.preOrder();
        }
    }

    @Override
    public int compareTo(Node2 o) {
    
    
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
    
    
        return "Node2{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }
}
  1. 创建节点
private static List<Node2> getNodes(byte[] bytes) {
    
    
        // 1. 创建一个 ArrayList
        ArrayList<Node2> nodes = new ArrayList<>();
        // 2. 存储灭一个byte出现的次数
        HashMap<Byte, Integer> map = new HashMap<>();
        for (byte by : bytes) {
    
    
            //说明map中没有这个字符,我们将其加入到map中
            //说明这个字符已经存在,我们让其进行总数加1
            map.merge(by, 1, Integer::sum);
        }
        System.out.println(map);

        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
    
    
            nodes.add(new Node2(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }
  1. 创建赫夫曼树
public static Node2 createHeffmanTree(List<Node2> nodes) {
    
    

        // 4. 排序 小 --> 大
        // 循环处理
        while (nodes.size() > 1) {
    
    
            Collections.sort(nodes);

            // 5. 取出权值最小的两个二叉树
            Node2 left = nodes.get(0);
            Node2 right = nodes.get(1);

            // 6. 构建新的二叉树
            Node2 parent = new Node2(null, left.weight + right.weight);
            parent.left = left;
            parent.right = right;

            // 7. 从 arrayList中删除处理过的二叉树
            nodes.remove(left);
            nodes.remove(right);
            nodes.add(parent);

        }
        return nodes.get(0);
    }
  1. 创建赫夫曼编码
    public static Map<Byte, String> getCodes(Node2 node2) {
    
    
        getCodes(node2, "", stringBuilder);
        return huffmanCodes;
    }

    private static void getCodes(Node2 node, String code, StringBuilder stringBuilder) {
    
    
        StringBuilder st = new StringBuilder(stringBuilder);
        //将code加入st
        st.append(code);
        if (node != null) {
    
    
            if (node.data == null) {
    
    //非叶子节点,叶子节点的data域是存在值的
                //递归处理
                //向左递归
                getCodes(node.left, "0", st);
                //向右递归
                getCodes(node.right, "1", st);
            } else {
    
    
                //说明找到一个叶子节点
                //就表示找到某个叶子节点的最后
                huffmanCodes.put(node.data, st.toString());
            }
        }
    }
  1. 解析赫夫曼编码,压缩
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    
    
        // 1. 将 bytes 转成 heffman编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        // 2. 遍历 bytes 数组
        for (byte b : bytes) {
    
    
            stringBuilder.append(huffmanCodes.get(b));
        }
        int len = (stringBuilder.length() + 7) / 8;
        // 创建存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i += 8) {
    
     // 8位对应一个 byte
            String strByte;
            if (i + 8 > stringBuilder.length()) {
    
    
                // 不够 8 位
                strByte = stringBuilder.substring(i);
            } else strByte = stringBuilder.substring(i, i + 8);
            // 将 strByte 转成 一个byte,放入
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }
  1. 解压数据
  private static byte[] deCode(Map<Byte, String> huffmanCodes, byte[] huffmanCodesBytes) {
    
    
        // 1. 将 huffmanCodesBytes  [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
        //    重新转成字符串(huffman编码对应的二进制字符串)
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < huffmanCodesBytes.length; i++) {
    
    
            // 判断是不是最后一个字节
            boolean flag = (i == huffmanCodesBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, huffmanCodesBytes[i]));
        }
        // 3. 把字符串按照指定的赫夫曼编码进行解码
        HashMap<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
    
    
            map.put(entry.getValue(), entry.getKey());
        }
        // 4. 存放 byte
        ArrayList<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
    
    
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
    
    
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                if (b != null) {
    
    
                    flag = false;

                } else {
    
    
                    count++;
                }
            }
            list.add(b);
            i += count;
        }
        byte[] bytes = new byte[list.size()];
        for (int i = 0; i < list.size(); i++) {
    
    
            bytes[i] = list.get(i);
        }
        return bytes;

    }
  1. 压缩文件
private static void zipFile(String srcFile, String dstFile) throws IOException {
    
    
        FileInputStream inputStream = new FileInputStream(srcFile);
        byte[] bytes = new byte[inputStream.available()];
        // 读取文件
        inputStream.read(bytes);
        // 压缩完成
        byte[] huffmanZipBytes = huffmanZip(bytes);
        // 输出文件,存放压缩文件
        OutputStream outputStream = new FileOutputStream(dstFile);
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);
        // 写入压缩数组
        oos.writeObject(huffmanZipBytes);
        // 写入赫夫曼编码
        oos.writeObject(huffmanCodes);
        // 写入完毕
        inputStream.close();
        oos.close();
        outputStream.close();
    }
  1. 解压文件
private static void unzipFile(String zipFile, String dstFile) throws IOException, ClassNotFoundException {
    
    
        FileInputStream inputStream = new FileInputStream(zipFile);
        ObjectInputStream ooin = new ObjectInputStream(inputStream);
        byte[] bytes;
        // 读取文件
        bytes = (byte[]) ooin.readObject();
        Map<Byte, String> huffmanCodes = (Map<Byte, String>) ooin.readObject();
        // 解压
        byte[] deCode = deCode(huffmanCodes, bytes);
        // 输出文件,存放压缩文件
        OutputStream outputStream = new FileOutputStream(dstFile);
        // 写入压缩数组
        outputStream.write(deCode);
        // 写入完毕
        ooin.close();
        inputStream.close();
        outputStream.close();
    }
  1. 公共方法
    // 5. 将前面的方法封装起来,便于我们的调用
    private static byte[] huffmanZip(byte[] bytes) {
    
    
        // 1. 合并
        List<Node2> nodes = getNodes(bytes);
        // 2. 生成赫夫曼树
        Node2 root = createHeffmanTree(nodes);
        // 3. 生成赫夫曼编码
        Map<Byte, String> codes = getCodes(root);
        // 4. 生成赫夫曼编码处理后的数据
        byte[] zipBytes = zip(bytes, huffmanCodes);
        return zipBytes;
    }
    
        // 将 byte 转成二进制的字符串
    private static String byteToBitString(boolean flag, byte b) {
    
    
        int temp = b;
        // 如果是正数,还存在补高位的问题
        if (flag) {
    
     // 标识是否需要高位
            temp |= 256; // temp 1 --> 000;
        }

        String string = Integer.toBinaryString(temp); // 返回的是 temp 对应的二进制的补码
        if (flag) {
    
    
            return string.substring(string.length() - 8);
        } else {
    
    
            return string;
        }

    }

    // 前缀遍历
    public static void preOrder(Node2 root) {
    
    
        root.preOrder();
    }

8. 排序二叉树(BST)

1)概念

二叉排序树:BST: (Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

特别说明: 如果有相同的值,可以将该节点放在左子节点或右子节点。

比如针对前面的数据(7,3,10,12,5,1,9),对应的二叉排序树为:

在这里插入图片描述

2)代码

  1. Node
// Node 节点
class Node3 {
    
    
    int value;
    Node3 left;
    Node3 right;

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

    // 添加节点
    public void add(Node3 node) {
    
    
        if (node == null) return;
        if (this.value > node.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();
        }
    }

    @Override
    public String toString() {
    
    
        return "Node3{" +
                "value=" + value +
                '}';
    }
}
  1. BinarySortTree
 // 二叉排序树
class BinarySortTree {
    
    
    private Node3 root;

    // 添加节点
    public void add(Node3 node) {
    
    
        if (root == null) {
    
    
            root = node;
        } else {
    
    
            root.add(node);
        }
    }

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

}

3)删除

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

    1. 需要先去找到要删除的结点 targetNode
    2. 找到targetNode 的 父结点 parent
    3. 确定 targetNode 是 parent的左子结点 还是右子结点
    4. 根据前面的情况来对应删除

    左子结点 parent.left = null

    右子结点 parent.right = null;

  • 删除只有一颗子树的节点 比如 1

    1. 需要先去找到要删除的结点 targetNode
    2. 找到targetNode 的 父结点 parent
    3. 确定targetNode 的子结点是左子结点还是右子结点
    4. targetNode 是 parent 的左子结点还是右子结点

    如果targetNode 有左子结点

    • 如果 targetNode 是 parent 的左子结点

      parent.left = targetNode.left;

    • 如果 targetNode 是 parent 的右子结点

      parent.right = targetNode.left;

    如果targetNode 有右子结点

    • 如果 targetNode 是 parent 的左子结点

      parent.left = targetNode.right;

    • 如果 targetNode 是 parent 的右子结点

      parent.right = targetNode.right

  • 删除有两颗子树的节点. (比如:7, 3,10 )

    1. 需求先去找到要删除的结点 targetNode
    2. 找到targetNode 的 父结点 parent
    3. 从targetNode 的右子树找到最小的结点
    4. 用一个临时变量,将 最小结点的值保存 temp = 11
    5. 删除该最小结点

    targetNode.value = temp

4)删除代码

  1. node

    // 查找删除的节点
    public Node3 search(int value) {
    
    

        if (value < this.value) {
    
     // 小于当前节点  向左查
            if (this.left == null) {
    
    
                return null;
            }
            return this.left.search(value);
        } else if (value > this.value) {
    
    
            if (this.right == null) {
    
    
                return null;
            }
            return this.right.search(value);
        } else {
    
    
            return this;
        }

    }

    // 查找要删除节点的父节点
    public Node3 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;
            }
        }
    }
  1. BinarySortTree
    // 查找要删除的节点
    public Node3 search(int value) {
    
    
        if (root == null) return null;
        else return root.search(value);
    }

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

    // 删除节点
    public void delNode(int value) {
    
    
        if (root == null) return;
        else {
    
    
            // 1. 找节点
            Node3 targetNode = search(value);
            if (targetNode == null) return;
            // 2. 当前二叉排序树只有一个节点,删除当前节点
            if (root.left == null && root.right == null) {
    
    
                root = null;
                return;
            }

            // 3. 查找targetNode 的父节点
            Node3 parent = searchParent(value);
            // 4. 删除的节点是叶子节点
            if (targetNode.left == null && targetNode.right == null) {
    
    
                // 5. 判断是父节点的左子节点还是右
                if (parent.left != null && parent.left.value == value) {
    
     // 左子节点
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) {
    
     // 右子节点
                    parent.right = null;
                }
            }
            // 6. 删除有两颗子树的节点
            else if (targetNode.left != null && targetNode.right != null) {
    
    
                int min = delRightThreeMin(targetNode.right);
                targetNode.value = min;

            }
            // 7. 删除只有一颗子树的节点
            else {
    
    

                // 如果要删除的节点有左子节点
                if (targetNode.left != null) {
    
    
                    if (parent == null) {
    
    
                        // 当前节点为根节点
                        root = targetNode.left;
                        return;
                    }
                    // targetNode 是 parent 的左子节点
                    if (parent.left.value == value) {
    
    
                        parent.left = targetNode.left;
                    } else {
    
     // targetNode 是parent 的右子节点
                        parent.right = targetNode.left;

                    }
                } else {
    
     // 要删除的节点有右子节点
                    if (parent.left.value == value) {
    
    
                        parent.left = targetNode.right;
                    } else {
    
    
                        // targetNode 是 parent 的右子节点
                        parent.right = targetNode.right;
                    }

                }
            }
        }
    }

    // 删除 node 为根节点的二叉排序树的最小节点
    public int delRightThreeMin(Node3 node) {
    
    
        // node 传入的节点(当作二叉排序树的根节点)
        // 返回的 以 node 为根节点的二叉排序树的最小节点的值
        Node3 targetNode = node;
        // 循环查找左节点, 就会找到最小值
        while (targetNode.left != null) {
    
    
            targetNode = targetNode.left;
        }
        // 这时 targetNode 就是最小值
        delNode(targetNode.value);
        return targetNode.value;

    }

9. 平衡二叉树(AVL)

1)BST存在的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d5UTG4Ar-1652074825558)(https://secure2.wostatic.cn/static/hfvXXSAsLwTQXhvBJnjjej/image.png)]

  1. 左子树全部为空,从形式上看,更像一个单链表
  2. 插入速度没有影响
  3. 查询速度明显变慢,不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢

2)概念

  1. 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高。
  2. 具有以下特点:
    • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树AVL替罪羊树Treap伸展树等。
  3. 举例说明.看看下面哪些AVL树,为什么?
    在这里插入图片描述

3)左旋转图解

  1. 创建一个新的节点newNode (以4这个值创建),创建一个新的节点,值等于当前根节点的值

    把新节点的左子树设置了当前节点的左子树

  2. newNode.left = left

    把新节点的右子树设置为当前节点的右子树的左子树

  3. newNode.right =right.left;

    把当前节点的值换为右子节点的值

  4. value=right.value;

    把当前节点的右子树设置成右子树的右子树

  5. right=right.right;

    把当前节点的左子树设置为新节点

  6. left=newleft;
    在这里插入图片描述

4)左旋转代码

  1. Node
// Node 节点
class Node4 {
    
    
    int value;
    Node4 left;
    Node4 right;

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

    // 添加节点
    public void add(Node4 node) {
    
    
        if (node == null) return;
        if (this.value > node.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.rightHeight() < right.leftHeight()) {

            }*/
            leftRotate();
        }
    }

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


    // 返回当前结点的高度,
    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() {
    
    
        return right == null ? 0 : right.height();
    }

    // 左旋转方法
    public void leftRotate() {
    
    
        // 1. 创建新的结点
        Node4 newNode = new Node4(this.value);
        // 2. 把新的结点的左子树设置为当前节点的左子树
        newNode.left = this.left;
        // 3. 新的结点的右子树设置为过去节点的右子树的左子树
        newNode.right = this.right.left;
        // 4. 当前结点的值换成右子结点的值
        this.value = right.value;
        // 5. 把当前结点的右子树设置成右子树的右子树
        this.right = right.right;
        // 7. 当前节点的左子结点设置为新的结点
        this.left = newNode;
    }


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

5)右旋转图解

进行右旋转

就是降低左子树的高度,这里是将9这个节点,通过右旋转,到右子树

  1. 创建一个新的节点newNode(以10这个值创建),创建―个新的节点,值等于当前根节点的值

    把新节点的右子树设置了当前节点的右子树

  2. newNode.right =right

    把新节点的左子树设置为当前节点的吃子树的右子树

  3. newNode.left=left.right;

    把当前节点的值换为左子节点的值

  4. valde=left.value,

    把当前节点的左子树设置成左子树的吃子树

  5. left=left.left;

    把当前节点的右子树设置为新节点

  6. right=newLeft;
    在这里插入图片描述

6)右旋转代码

Node


    // 右旋转
    public void rightRotate() {
    
    
        Node4 newNode = new Node4(this.value);
        newNode.right = this.right;
        newNode.left = this.left.right;
        this.value = this.left.value;
        this.left = this.left.left;
        this.right = newNode;
    }
    
    
    // 添加节点
    public void add(Node4 node) {
    
    
        if (node == null) return;
        if (this.value > node.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.rightHeight() < right.leftHeight()) {

            }*/
//            leftRotate();
        }
        if (leftHeight() - rightHeight() > 1) {
    
    
            rightRotate();
        }
    }

7)双旋转图解

问题图:
在这里插入图片描述

解决

  1. 当符号右旋转的条件
  2. 如果它的左子树的右子树的高度,大于它的右子树的高度
  3. 先对当前节点的左节点进行左旋转
  4. 再对当前节点进行右旋转的操作就行

8)双旋转代码

Node

    // 添加节点
    public void add(Node4 node) {
    
    
        if (node == null) return;
        if (this.value > node.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();
            return;
        }
        if (leftHeight() - rightHeight() > 1) {
    
    
            // 当前节点 的 左子树 的 右子树的高度 > 左子树的高度
            if (left != null && left.rightHeight() > left.leftHeight()) {
    
    
                // 先对当前节点的左节点进行左旋转
                left.leftRotate();
            }
            rightRotate();
        }
    }

9)测试用数组

        int[] array = {
    
    4, 3, 6, 5, 7, 8};
        int[] array = {
    
    10, 12, 8, 9, 7, 6};
        int[] array = {
    
    10, 11, 7, 6, 8, 9};

10. 多路查找树

1)二叉树的问题

  1. 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿),就存在如下回题:

  2. 问题1∶

    在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中)。节点海量构建才叉树时,速度有影响

  3. 问题2:

    节点海量,也会造成二叉树的高度很大,会降低操作速度.
    在这里插入图片描述

2)多叉树概念

  1. 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树( multiway tree)
  2. 后面我们讲解的2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
  3. 举例说明(下面2-3树就是—颗多叉树)
    在这里插入图片描述

3)B树概念

B树概念—> 链接

B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。

  1. 如图B树通过重新组织节点,降低了树的高度
  2. 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次i/o就可以完全载入
  3. 将树的度M(所有节点里子节点数量最多的数量)设置为1024,在600亿个元素中最多只需要4次i/o操作就可以读取到想要的元素,B树()广泛应用于文件存储系统以及数据库系统中
    在这里插入图片描述

4)2-3树概念

  1. 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  2. 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
  3. 有三个子节点的节点叫三节点,三节占要么没有子节占,要么有三个子节点.
  4. 2-3树是由二节点和三节点构成的树。

5)2-3 树应用

将数列{16,24,12,32,14,26,34,10,8,28,38,20}构建成2-3树
在这里插入图片描述

插入规则

  1. 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  2. 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
  3. 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
  4. 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面3个条件。
  5. 对于三节点的子树的值大小仍然遵守(BST二叉排序树)的规则

11. B树

1)概念

B-tree树就是B树,B即Balanced,平衡的意思。B-tree就是B树

前面已经介绍了2-3树和2-3-4树,他们就是B树(英语: B-tree也写成B-树),这里我们再做一个说明,我们在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如图:

  1. B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4

  2. B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;

    重复,直到所对应的儿子指针为空,或已经是叶子结点

  3. 关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据.

  4. 索有可能在非叶子结点结束

  5. 其搜索性能等价于在关键字全集内做一次二分查找
    在这里插入图片描述

12. B+树

B+树详细概念

1)概念

B+树是B树的变体,也是一种多路搜索树。
在这里插入图片描述

  1. B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找

  2. 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。

  3. 不可能在非叶子结点命中

  4. 非叶子结点相当于是叶子结点的索引(稀疏索引)叶子结点相当于是存储(关键字)数据的数据层

  5. 更适合文件索引系统

  6. B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然.

12. B*树

1)概念

B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
在这里插入图片描述

  1. B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2.
  2. 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高

猜你喜欢

转载自blog.csdn.net/m0_56186460/article/details/124545347
今日推荐