DSA_树结构的实际应用(java数据结构与算法)

堆排序

堆排序基本介绍

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。

  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。

  3. 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

  4. 大顶堆举例说明

image-20210711175519221

  1. 小顶堆举例说明

image-20210711175542884

  1. 一般升序采用大顶堆,降序采用小顶堆

堆排序基本思想

堆排序的基本思想是:

  1. 将待排序序列构造成一个大顶堆

  2. 此时,整个序列的最大值就是堆顶的根节点。

  3. 将其与末尾元素进行交换,此时末尾就为最大值。

  4. 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

堆排序步骤图解说明

要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。

步骤一 :构造初始堆。

将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。 原始的数组 [4, 6, 8, 5, 9]

  1. .假设给定无序序列结构如下

image-20210711180021331

  1. 此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从右至左,从下至上进行调整。

image-20210711180251826

  1. 找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。

image-20210711180322094

  1. 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。

image-20210711180344771

此时,我们就将一个无序序列构造成了一个大顶堆。

步骤二 :交换

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

  1. 将堆顶元素 9 和末尾元素 4 进行交换

image-20210711181027217

  1. 重新调整结构,使其继续满足堆定义

image-20210711181049022

  1. 再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.

image-20210711181111044

  1. 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

image-20210711181130512

基本思路
  1. 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

堆排序代码实现

package cn.chasing.DataStructure.tree;

import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/**
 * @author 柴柴快乐每一天
 * @create 2021-07-11  6:14 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class HeapSort {
    @Test
    public void test1() {
        int arr[] = {4, 6, 8, 5, 9};
        System.out.println("排序前");
        Date data1 = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date1Str = simpleDateFormat.format(data1);
        System.out.println("排序前的时间是=" + date1Str);

        heapSort(arr);

        Date data2 = new Date();
        String date2Str = simpleDateFormat.format(data2);
        System.out.println("排序前的时间是=" + date2Str);

        System.out.println("排序后=" + Arrays.toString(arr));
    }

    @Test
    public void test2() {
        // 创建要给 80000 个的随机的数组
        int[] arr = new int[8000000];
        for (int i = 0; i < 8000000; i++) {
            arr[i] = (int) (Math.random() * 8000000);
        }
        System.out.println("排序前");
        Date data1 = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date1Str = simpleDateFormat.format(data1);
        System.out.println("排序前的时间是=" + date1Str);

        heapSort(arr);

        Date data2 = new Date();
        String date2Str = simpleDateFormat.format(data2);
        System.out.println("排序前的时间是=" + date2Str);
    }

    // 编写一个堆排序的方法
    public static void heapSort(int[] arr) {
        int temp = 0;
        System.out.println("堆排序");

        // 将无序序列构建成一个堆
        // 从最后一个非叶子节点开始,相当于从右至左,从下至上调整子树,再加上是顺序化二叉树,所以是i--
        for (int i = arr.length/2-1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length);
        }

        // 将堆订元素与末尾元素交换,将最大元素沉到数组末端
        // 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换
        for (int j = arr.length-1; j > 0; j--) {
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr, 0, j);
        }
    }

    /**
     * 将一个数组(二叉树),调整成大顶堆
     * 功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
     * 举例 int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
     * 如果我们再次调用 adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5,4}
     * @param arr   待调整的数组
     * @param i     表示非叶子结点在数组中的索引
     * @param length    表示对多少个元素继续调整,length逐渐减少
     */
    public static void adjustHeap(int[] arr, int i, int length) {
        // 先取出当前元素的值,保存在临时变量
        int temp = arr[i];

        // k = i*2+1 是 i节点的左子节点
        for (int k = i*2+1; k < length; k = k*2+1) {
            // 左子节点的值小于右,则指针k指向右
            if (k+1 < length && arr[k] < arr[k+1]) {
                k++;
            }

            if (arr[k] > arr[i]) {
                // 如果子节点大于父节点,把较大的赋值给当前节点
                arr[i] = arr[k];
                // 继续循环比较
                i = k;
            } else {
                break;
            }
        }

        // 当for循环结束,已经将以i为父节点的树的最大值,放在了此子树的最顶部
        // 实现了顶部元素与最大的交换,使最大的到最顶部
        arr[i] = temp;
    }
}

赫夫曼树

基本介绍

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

赫夫曼树几个重要概念和举例说明

  1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
  2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  3. 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  4. WPL 最小的就是赫夫曼树

image-20210818114539903

赫夫曼树创建思路图解

给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.

构成赫夫曼树的步骤

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

image-20210818154209710

赫夫曼树的代码实现

package cn.chasing.DataStructure.huffmanTree;

import org.junit.Test;

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

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-18  4:52 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class HuffmanTree {

    @Test
    public void test() {
        int[] arr = { 13, 7, 8, 3, 29, 6, 1 };
        Node root = createHuffmanTree(arr);

        //测试一把
        preOrder(root);
    }


    // 前序遍历方法
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("Empty Tree!");
        }
    }

    public static Node createHuffmanTree(int[] arr) {
        List<Node> nodes = new ArrayList<>();

        for (int i : arr) {
            nodes.add(new Node(i));
        }

        Node leftNode = null;
        Node rightNode = null;
        Node parent = null;
        while (nodes.size() > 1) {
            // 排序后取出权值最小的两个树
            Collections.sort(nodes);
            leftNode = nodes.get(0);
            rightNode = nodes.get(1);

            parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;

            // 生成的新树加入排序
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }



}

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

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

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

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

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

    public int getValue() {
        return value;
    }

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

    public Node getLeft() {
        return left;
    }

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

    public Node getRight() {
        return right;
    }

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

赫夫曼编码

基本介绍

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

原理剖析

定长编码

image-20210818221623772

变长编码

image-20210818221644187

赫夫曼编码

传输的字符串

  • i like like like java do you like a java
  • d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
  • 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
步骤:

构成赫夫曼树的步骤:

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

image-20210818221931137

  1. 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 向右的路径为 1 , 编码如下:

o: 1000

u: 10010

d: 100110

y: 100111

i: 101

a : 110

k: 1110

e: 1111

j: 0000

v: 0001

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

    1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

    通过赫夫曼编码处理 长度为 133

  2. 长度为 : 133

    原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%

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

注意事项

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

image-20210818222315809

数据压缩

创建赫夫曼树

将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理,形式如

“1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110”

根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树。

结点数据
class Node implements Comparable<Node>{
    Byte data;
    Integer weight;
    Node left;
    Node right;

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

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

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

    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}
创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {

    Node leftNode = null;
    Node rightNode = null;
    Node parentNode = null;

    while (nodes.size() > 1) {
        Collections.sort(nodes);
        leftNode = nodes.get(0);
        rightNode = nodes.get(1);

        parentNode = new Node(null, leftNode.weight + rightNode.weight);
        parentNode.left = leftNode;
        parentNode.right = rightNode;

        nodes.remove(leftNode);
        nodes.remove(rightNode);
        nodes.add(parentNode);
    }
    return nodes.get(0);
}

生成赫夫曼编码表

我们已经生成了赫夫曼树, 下面我们继续完成任务

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

  • =01
  • a=100
  • d=11000
  • u=11001
  • e=1110
  • v=11011
  • i=101
  • y=11010
  • j=0010
  • k=1111
  • l=000
  • o=0011
/**
 * 传入字符串转换的字节数组,得到Node的List集合,用于creatHuffmanTree创建赫夫曼树
 * @param bytes 接收字节数组
 * @return 得到List<Node>,用于creatHuffmanTree的创建
 */
private List<Node> getNodes(Byte[] bytes) {
    List<Node> nodes = new ArrayList<>();

    // 用于统计每个字符出现的次数,作为Node节点的权值
    Map<Byte, Integer> counts = new HashMap<>();
    Integer count = null;
    for (Byte aByte : bytes) {
        count = counts.get(aByte);
        if (count == null) {
            // 第一次遍历时,map里还没有这个字符
            counts.put(aByte, 1);
        } else {
            counts.put(aByte, count+1);
        }
    }

    // 把每一个键值对转换为Node节点,加入到Nodes内
    for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
        nodes.add(new Node(entry.getKey(), entry.getValue()));
    }
    return nodes;
}

/**
 * 将传入的Node节点的所有叶子节点的赫夫曼编码得到,存入赫夫曼编码表
 * @param node 传入的节点
 * @param pathCode  路径:左子节点0,右子节点1
 * @param huffmanCodes 将赫夫曼编码表存放在 Map<Byte,String> 形式
 * @param stringBuilder 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
 */
private static void getCodes(Node node, String pathCode,Map<Byte, String> huffmanCodes, StringBuilder stringBuilder) {
    StringBuilder stringTemp = new StringBuilder(stringBuilder);
    stringTemp.append(pathCode);
    if (node != null) {
        // 等于null不用再拼接字符串了,已经递归出树的范围了
        if (node.data == null) {
            // 说明是一个非叶子节点
            // 向左递归
            getCodes(node.left, "0", huffmanCodes, stringTemp);
            //向右递归
            getCodes(node.right, "1", huffmanCodes, stringTemp);
        } else {
            // 叶子节点,将叶子节点的路径添加进赫夫曼编码表
            huffmanCodes.put(node.data, stringTemp.toString());
        }
    }
}

生成赫夫曼编码后的数据

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

/**
 * 将字符串对应的原始byte数组,通过赫夫曼编码表,返回一个编码后并压缩的byte数组
 * @param bytes 原始的字符串对应的byte[]
 * @param huffmanCodes getCodes生成的huffmanCodes编码表,01序列
 * @return 返回赫夫曼编码处理后的byte[],8位01序列对应一个byte,字节数据用的补码表示
 */
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    // 利用huffmanCodes表,将byte[]转化为编码后的01序列字符串
    StringBuilder huffmanString = new StringBuilder();
    for (Byte aByte : bytes) {
        huffmanString.append(huffmanCodes.get(aByte));
    }
    // 统计huffmanString 生成byte[]后,该byte[]的长度。8位为一个byte
    int len = (huffmanString.length() + 7) / 8;

    // 创建压缩后的byte数组
    byte[] huffmanCodeBytes = new byte[len];
    // 记录是第几个byte
    int index = 0;
    for (int i = 0; i < huffmanString.length(); i+=8) {
        String strByte;
        if (i+8 > huffmanString.length()) {
            // 说明到了最后一个字节了,且01序列不够8位
            strByte = huffmanString.substring(i);
        } else {
            strByte = huffmanString.substring(i, i+8);
        }
        // 将strByte转化二进制Integer,再强转为byte,放入到huffmanCodeBytes
        huffmanCodeBytes[index++] = (byte) Integer.parseInt(strByte, 2);
    }
    return huffmanCodeBytes;
}

/**
 * 重载,顺便将相关的方法封装
 * @param bytes 原始的字符串对应的字节数组
 * @return 返回赫夫曼编码处理后的byte[],8位01序列对应一个byte,字节数据用的补码表示
 */
private static byte[] zip(byte[] bytes) {
    // 根据原始字符串的byte[]得到带权值的node的集合
    List<Node> nodes = getNodes(bytes);
    // 通过node集合创建赫夫曼树
    Node huffmanTree = createHuffmanTree(nodes);
    // 根据赫夫曼树得到赫夫曼编码表
    Map<Byte, String> huffmanCode = getCodes(huffmanTree);

    // 根据原始字符串的byte[]和赫夫曼编码表进行压缩
    byte[] huffmanCodeBytes = zip(bytes, huffmanCode);
    return huffmanCodeBytes;
}

数据解压

使用赫夫曼编码解码

使用赫夫曼编码来解码数据,具体要求是

  1. 前面我们得到了赫夫曼编码和对应的编码 byte[]

    即:[-88, -65, -56, -65, -56, -65, -55, 77 , -57, 6, -24, -14, -117, -4, -60, -90, 28]

  2. 现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java"

  3. 思路:解码过程,就是编码的一个逆向操作。

/**
 * 将一个8位byte转成一个二进制的字符串,因为是补码表示,所以可能需要补高位
 * @param flag 标志是否需要补高位,true需要补高位,false不要
 * @param b 传入的byte
 * @return 该b的对应二进制的字符串,按补码返回的
 */
private static String byteToBitString(boolean flag, byte b) {
    // 将b转成int
    int temp = b;

    // 如果是正数,还需要补高位,负数或上256还是原值
    if (flag) {
        // 1 0000 0000
        temp |= 256;
    }
    // 返回的是temp对应的二进制的补码
    String str = Integer.toBinaryString(temp);

    if (flag || str.length() > 8) {
        // 因为补高位导致数值变了,多了一位
        return str.substring(str.length()-8);
    } else {
        return str;
    }
}

/**
 * 完成对数据的解码
 * @param huffmanCodes 赫夫曼编码表map
 * @param huffmanBytes 赫夫曼编码后得到的字节数组
 * @return 原来的字符串对应的byte数组
 */
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    // 先得到huffmanBytes对应的二进制字符串,形如101010001011...
    StringBuilder bitString = new StringBuilder();
    // 将byte数组转化为二进制字符串
    for (int i = 0; i < huffmanBytes.length; i++) {
        byte b = huffmanBytes[i];
        // 判断是不是最后一个字节
        boolean flag = (i == huffmanBytes.length-1);
        bitString.append(byteToBitString(!flag, b));
    }
    // 把字符串按照赫夫曼编码表解码。把赫夫曼编码表进行调换,要进行方向查询
    Map<String , Byte> decodeMap = new HashMap<>();
    for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
        decodeMap.put(entry.getValue(), entry.getKey());
    }

    // 创建一个集合存放byte
    List<Byte> list = new ArrayList<>();
    // i 可以理解成索引,及第几个字符。扫描bitString
    for (int i = 0; i < bitString.length();) {
        // count表示扫描到第几位
        int count = 1;
        boolean flag = true;
        Byte b = null;

        while (flag) {
            // i 不动,count移动,直到匹配到一个字符
            String key = bitString.substring(i, i+count);
            b = decodeMap.get(key);
            if (b == null) {
                // 未找到
                count++;
            } else {
                // 匹配到一个字符
                flag = false;
            }
        }
        list.add(b);
        i += count;
    }
    // 当for循环结束,list就存放了所有字符,把list中的数据放到byte[]然后返回
    byte[] result = new byte[list.size()];
    for (int i = 0; i < list.size(); i++) {
        result[i] = list.get(i);
    }
    return result;
}

文件压缩

我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压, 具体要求:

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

public static void zipFile(String srcFile, String dstFile) {
    // 创建输出流
    OutputStream os = null;
    ObjectOutputStream oos = null;
    // 创建文件的输入流
    FileInputStream is = null;

    try {
        is = new FileInputStream(srcFile);
        // 创建一个和文件一样大小的byte[]
        byte[] b = new byte[is.available()];
        // 读取文件
        is.read(b);
        // 直接对源文件压缩
        byte[] huffmanBytes = zip(b);
        // 创建文件的输出流
        os = new FileOutputStream(dstFile);
        // 创建一个和文件输出流关联的ObjectOutputStream
        oos = new ObjectOutputStream(os);
        // 把赫夫曼编码后的字节数组写入压缩文件
        oos.writeObject(huffmanBytes);
        // 把赫夫曼编码表写入输出流
        oos.writeObject(huff);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    } finally {
        try {
            is.close();
            oos.close();
            os.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[] huffmanBytes = (byte[]) ois.readObject();
        Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
        // 解码
        byte[] bytes = decode(huffmanCodes, huffmanBytes);
        os = new FileOutputStream(dstFile);
        os.write(bytes);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            os.close();
            ois.close();
            is.close();
        } catch (Exception e2) {
            // TODO: handle exception
            System.out.println(e2.getMessage());
        }
    }
}

解压文件有错误,赫夫曼编码有误,导致编码表有误。

二叉排序树

先看一个需求

给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加

解决方案分析

使用数组

  • 数组未排序

    • 优点:直接在数组尾添加,速度快。
    • 缺点:查找速度慢.
  • 数组排序

    • 优点:可以使用二分查找,查找速度快
    • 缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。

使用链式存储-链表

  • 添加数据速度比数组快,不需要数据整体移动。
  • 不管链表是否有序,查找速度都慢

使用二叉排序树

二叉排序树介绍

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

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

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

image-20210820224722197

二叉排序树创建和遍历

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

image-20210820224757220

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

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

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

    /**
     * 添加节点,用以递归地创建二叉排序树
     * @param node 传入的用于添加节点
     */
    public void add(Node node) {
        if (node == null) {
            return;
        }

        if (node.value < this.value) {
            // 说明添加的节点的值小于当前子树的根节点,放左边去
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            // 说明添加的节点的值大于等于当前子树的根节点,放右边去
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }
    }

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

}

class BinarySortTree {
    private Node root;

    /**
     * 添加节点的方法
     * @param node 需要添加的节点
     */
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

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


    public Node getRoot() {
        return root;
    }

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

二叉排序树的删除

二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

  1. 删除叶子节点 (比如:2, 5, 9, 12)
  2. 删除只有一颗子树的节点 (比如:1)
  3. 删除有两颗子树的节点. (比如:7, 3,10 )

操作的思路分析

image-20210820231542589

第一种情况:

删除叶子节点 (比如: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 的左子结点还是右子结点

  5. 如果 targetNode 有左子结点

    • 如果 targetNode 是 parent 的左子结点

      parent.left = targetNode.left;

    • 如果 targetNode 是 parent 的右子结点

      parent.right = targetNode.left;

  6. 如果 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. 删除该最小结点
  6. targetNode.value = temp

二叉排序树删除结点的代码实现:

package cn.chasing.DataStructure.binarySortTree;

import org.junit.Test;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-20  10:51 下午
 * 『Stay hungry, stay foolish. 』
 */
public class BinarySortTreeDemo {
    @Test()
    public void test1() {
        // 二叉搜索树的创建和遍历
        int[] data = {7, 3, 10};
        BinarySortTree binarySortTree = new BinarySortTree();
        for (int i = 0; i < data.length; i++) {
            binarySortTree.add(new Node(data[i]));
        }
        System.out.println("中序遍历二叉树:");
//        binarySortTree.infixOrder();
//
//        binarySortTree.delNode(12);
//
//
//        binarySortTree.delNode(5);
//        binarySortTree.delNode(10);
//        binarySortTree.delNode(2);
//        binarySortTree.delNode(3);
//
//        binarySortTree.delNode(9);
//        binarySortTree.delNode(1);
        binarySortTree.delNode(7);


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


        System.out.println("删除结点后");
        binarySortTree.infixOrder();

    }
}

class BinarySortTree {
    private Node root;

    /**
     * 添加节点的方法
     * @param node 需要添加的节点
     */
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

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

    /**
     * 找到要删除的节点
     * @param value 要删除的节点的值
     * @return 返回要删除的节点
     */
    public Node search(int value) {
        if (root == null) {
            return null;
        }
        return root.search(value);
    }

    /**
     * 查找要删除的节点的父节点,协助删除该节点
     * @param value 要删除的节点的值
     * @return 要删除的节点的父节点
     */
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        }
        return root.searchParent(value);
    }

    /**
     * 返回以node为根节点的二叉排序树的最小节点的值,并删除该节点
     * @param node 传入的节点(当做二叉排序树的根节点)
     * @return 返回以node为根节点的二叉排序树的最小节点的值
     */
    public int delRightTreeMin(Node node) {
        Node target = node;
        while (target.left != null) {
            target = target.left;
        }
        // 此时target指向了最小节点,返回最小值,删除最小节点
        delNode(target.value);
        return target.value;
    }

    /**
     * 删除节点(分3种情况讨论)
     * @param value 要删除节点的值
     */
    public void delNode(int value) {
        if (root == null) {
            return;
        }
        // 找到要删除的节点
        Node target = search(value);
        // 如果没有找到,就直接返回
        if (target == null) {
            return;
        }
        // 0. 如果发现这个二叉排序树只有一个节点。要删除的节点是根节点(根节点下还有元素,找不到根节点父元素而已),和该树只有根节点是不同的(直接删除根节点)
        if (root.left == null && root.right == null) {
            root = null;
            return;
        }

        // 找到target的父节点辅助删除(考虑根节点没有父节点)
        Node parent = searchParent(value);
        // 1. 如果要删除的是叶子节点(与0情况重合,不用考虑没父节点)
        if (target.left == null && target.right == null) {
            // 判断target是父节点的左节点还是右节点
            if (parent.left != null && parent.left.value == value) {
                parent.left = null;
            } else if (parent.right != null && parent.right.value == value) {
                parent.right = null;
            }
        } else if (target.left != null && target.right != null) {
            // 2. 如果要删除的节点有两个子树(向右子树的左子树不断递归,找到最小值,因为并不是直接删除该节点,而是赋值,所以不需要父节点)
            int treeMin = delRightTreeMin(target.right);
            target.value = treeMin;
        } else {
            // 3. 如果要删除的节点只有一个子树(要考虑没有父节点的情况了)
            // 3.1 如果要删除的节点有左子节点
            if (target.left != null) {
                if (parent != null) {
                    // 如果target是parent的左子节点
                    if (parent.left!= null && parent.left.value == value) {
                        parent.left = target.left;
                    } else {
                        parent.right = target.left;
                    }
                } else {
                    root = target.left;
                }
            } else {
                // 3.2 如果要删除的节点有右子节点
                if (parent != null) {
                    // 如果target是parent的左子节点,左子节点不一定存在
                    if (parent.left!= null && parent.left.value == value) {
                        parent.left = target.right;
                    } else {
                        parent.right = target.right;
                    }
                } else {
                    root = target.right;
                }
            }
        }
    }


    public Node getRoot() {
        return root;
    }

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

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

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

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

    /**
     * 添加节点,用以递归地创建二叉排序树
     * @param node 传入的用于添加节点
     */
    public void add(Node node) {
        if (node == null) {
            return;
        }

        if (node.value < this.value) {
            // 说明添加的节点的值小于当前子树的根节点,放左边去
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            // 说明添加的节点的值大于等于当前子树的根节点,放右边去
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }
    }

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

    /**
     * 查找要删除的节点
     * @param value 希望删除的节点的值
     * @return 如果找到就返回该节点,否则就返回null
     */
    public Node search(int value) {
        if (value == this.value) {
            // 相等即就是该节点
            return this;
        } else if (value < this.value) {
            // 可能在左边,或者根本不存在
            if (this.left == null) {
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null) {
                return null;
            }
            return this.right.search(value);
        }
    }

    /**
     * 找到要删除的节点的父节点,通过父节点双指针的方法才能删除一个节点
     * @param value 要删除的节点的值
     * @return 返回的是要删除的节点的父节点,没有则返回null
     */
    public Node searchParent(int value) {
        // 如果当前节点就是要删除的节点的父节点,直接返回
        boolean flagLeft = this.left != null && this.left.value == value;
        boolean flagRight = this.right != null && this.right.value == value;
        if (flagLeft || flagRight) {
            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. 一定要讨论父节点的情况,看父节点存不存在
  2. 要使用parent.left.value时,先判断parent.left存在不
  3. 讨论只有一个根节点的情况

平衡二叉树(AVL 树)

案例(二叉排序树可能的问题)

给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.

左边 BST 存在的问题分析:

  1. 左子树全部为空,从形式上看,更像一个单链表.
  2. 插入速度没有影响
  3. 查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
  4. 解决方案-平衡二叉树(AVL)

基本介绍

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

image-20210821154921872

单旋转(左旋转)

  1. 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
  2. 思路分析(示意图):实际上就是把右子树节点提上来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ae7OP9rx-1633318787396)(https://chasing1874.oss-cn-chengdu.aliyuncs.com/image-20210821155443373.png)]

  1. 代码实现
/**
 * 左旋转
 */
public void leftRotate() {
    Node newNode = new Node(this.value);
    newNode.left = this.left;
    newNode.right = this.right.left;

    this.value = this.right.value;
    this.left = newNode;
    this.right = this.right.right;
}

单旋转(右旋转)

  1. 要求:

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

  2. 思路分析(示意图) :就是把左子树提上来

image-20210821171026455

  1. 代码实现
/**
 * 右旋转
 */
public void rightRotate() {
    Node newNode = new Node(this.value);
    newNode.left = this.left.right;
    newNode.right = this.right;

    this.value = this.left.value;
    this.left = this.left.left;
    this.right = newNode;
}

双旋转

前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列

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

int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树

问题分析

image-20210821171205915

解决思路分析

  1. 当符号右旋转的条件时

  2. 如果它的左子树的右子树高度大于它的左子树的高度

  3. 先对当前这个结点的左节点进行左旋转

  4. 在对当前结点进行右旋转的操作即可

即要使长子树被放置在外侧

代码实现

/**
 * 添加节点,用以递归的创建二叉排序树
 * @param node 传入的用于添加的节点
 */
public void add(Node node) {
    if (node == null) {
        return;
    }
    if (node.value < this.value) {
        if (this.left == null) {
            this.left = node;
        } else {
            this.left.add(node);
        }
    } else {
        if (this.right == null) {
            this.right = node;
        } else {
            this.right.add(node);
        }
    }

    // 右子树比左子树长,左旋
    if (this.rightHeight() - this.leftHeight() > 1) {
        // 如果右子树的左子树比其右子树长,则先让右子树右旋(即让长子树在外侧)
        if (this.right != null && this.right.leftHeight() > this.right.rightHeight()) {
            this.right.rightRotate();
            // 然后在对当前节点左旋
        }
        this.leftRotate();
        // 及时返回,以免重复旋转
        return;
    }

    if (this.leftHeight() - this.rightHeight() > 1) {
        if (this.left != null && this.left.leftHeight() < this.left.rightHeight()) {
            this.left.leftRotate();
        }
        this.rightRotate();
    }
}

完整代码

package cn.chasing.DataStructure.avlTree;

import org.junit.Test;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-21  10:21 上午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class AvlTreeDemo {

    @Test
    public void test() {
        int[] arr = { 10, 11, 7, 6, 8, 9 };
        //创建一个 AVLTree对象
        AvlTree avlTree = new AvlTree();
        //添加结点
        for(int i=0; i < arr.length; i++) {
            avlTree.add(new Node(arr[i]));
        }

        //遍历
        System.out.println("中序遍历");
        avlTree.infixOrder();

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


class AvlTree {
    private Node root;

    public Node getRoot() {
        return root;
    }

    /**
     * 添加节点的方法
     * @param node 需要添加的节点
     */
    public void add(Node node) {
        if (this.root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

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

    /**
     * 找到要删除的节点
     * @param value 要删除的节点的值
     * @return 返回要删除的节点
     */
    public Node search(int value) {
        if (this.root == null) {
            return null;
        }
        return this.root.search(value);
    }

    /**
     * 查找要删除的节点的父节点,协助删除该节点
     * @param value 要删除的节点的值
     * @return 要删除的节点的父节点
     */
    public Node searchParent(int value) {
        if (this.root == null) {
            return null;
        }
        return this.root.searchParent(value);
    }

    /**
     * 返回以node为根节点的二叉排序树的最小节点的值,并删除该节点
     * @param node 传入的节点(当做二叉排序树的根节点)
     * @return 返回以node为根节点的二叉排序树的最小节点的值
     */
    public int delRightTreeMin(Node node) {
        Node target = node;
        while (target.left != null) {
            target = target.left;
        }
        delNode(target.value);
        return target.value;
    }

    /**
     * 删除节点(分3种情况讨论)
     * @param value 要删除节点的值
     */
    public void delNode(int value) {
        if (this.root == null) {
            return;
        }
        Node target = this.search(value);
        if (target == null) {
            return;
        }
        if (this.root.left == null && root.right == null) {
            this.root = null;
            return;
        }
        Node parent = this.searchParent(value);
        if (target.left == null && target.right == null) {
            if (parent.left != null && parent.left.value == value) {
                parent.left = null;
            } else if (parent.right != null && parent.right.value == value) {
                parent.right = null;
            }
        } else if (target.left != null && target.right != null) {
            target.value = delRightTreeMin(target.right);
        } else {
            if (target.left != null) {
                if (parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = target.left;
                    } else {
                        parent.right = target.left;
                    }
                } else {
                    root = target.left;
                }
            } else {
                if (parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = target.right;
                    } else {
                        parent.right = target.right;
                    }
                } else {
                    root = target.right;
                }
            }
        }
    }
}

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

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

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

    /**
     * 添加节点,用以递归的创建二叉排序树
     * @param node 传入的用于添加的节点
     */
    public void add(Node node) {
        if (node == null) {
            return;
        }
        if (node.value < this.value) {
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }

        // 右子树比左子树长,左旋
        if (this.rightHeight() - this.leftHeight() > 1) {
            // 如果右子树的左子树比其右子树长,则先让右子树右旋(即让长子树在外侧)
            if (this.right != null && this.right.leftHeight() > this.right.rightHeight()) {
                this.right.rightRotate();
                // 然后在对当前节点左旋
            }
            this.leftRotate();
            return;
        }

        if (this.leftHeight() - this.rightHeight() > 1) {
            if (this.left != null && this.left.leftHeight() < this.left.rightHeight()) {
                this.left.leftRotate();
            }
            this.rightRotate();
        }
    }

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

    /**
     * 查找要删除的节点
     * @param value 希望删除的节点的值
     * @return 找到则返回该节点,未找到则返回null
     */
    public Node search(int value) {
        if (value == this.value) {
            return this;
        } else if (value < this.value) {
            if (this.left == null) {
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null) {
                return null;
            }
            return this.right.search(value);
        }
    }

    /**
     * 找到要删除的节点的父节点,通过父节点双指针的方法删除节点
     * @param value 要删除的节点的值
     * @return 要删除的节点的父节点,没有父节点就返回null
     */
    public Node searchParent(int value) {
        boolean flagLeft = this.left != null && this.left.value == value;
        boolean flagRight = this.right != null && this.right.value == value;
        if (flagLeft || flagRight) {
            return this;
        }
        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);
        }
        return null;
    }

    /**
     * 左旋转
     */
    public void leftRotate() {
        Node newNode = new Node(this.value);
        newNode.left = this.left;
        newNode.right = this.right.left;

        this.value = this.right.value;
        this.left = newNode;
        this.right = this.right.right;
    }

    /**
     * 右旋转
     */
    public void rightRotate() {
        Node newNode = new Node(this.value);
        newNode.left = this.left.right;
        newNode.right = this.right;

        this.value = this.left.value;
        this.left = this.left.left;
        this.right = newNode;
    }

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

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

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

二叉树与 B 树

二叉树的问题分析

二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树

image-20210822172023636

  1. 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如 1 亿), 就存在如下问题:
  2. 问题 1:在构建二叉树时,需要多次进行 i/o 操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
  3. 问题 2:节点海量,也会造成二叉树的高度很大,会降低操作速度.

多叉树

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

image-20210822172502137

B 树的基本介绍

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

image-20210822172608536

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

2-3 树

2-3 树是最简单的 B 树结构, 具有如下特点:

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

2-3 树应用案例

将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。(演示一下构建 2-3 树的过程.)

image-20210822172805382

插入规则:

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

其它说明

除了 23 树,还有 234 树等,概念和 23 树类似,也是一种 B 树。 如图:

image-20210822172906522

B 树、B+树和 B*树

B 树的介绍

B-tree 树即 B 树,B 即 Balanced,平衡的意思。有人把 B-tree 翻译成 B-树,容易让人产生误解。会以为 B-树是一种树,而 B 树又是另一种树。实际上,B-tree 就是指的 B 树。

B 树的介绍

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

image-20210822173011124

对上图的说明:

  1. B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
  2. B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  3. 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
  4. 搜索有可能在非叶子结点结束
  5. 其搜索性能等价于在关键字全集内做一次二分查找

B+树的介绍

B+树是 B 树的变体,也是一种多路搜索树。

image-20210822173220229

对上图的说明:

  1. B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
  2. 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
  3. 不可能在非叶子结点命中
  4. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  5. 更适合文件索引系统
  6. B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然.

B*树的介绍

B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针

image-20210822173408132

B*树的说明:

  1. B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的1/2。
  2. 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值