二叉树与B树以及多路查找树学习

69 阅读6分钟

二叉树与B树

二叉树是一种数据结构,它的每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树可以用来实现排序、查找、遍历等操作。

B树是一种平衡的多路查找树,它的每个节点可以有多个子节点,通常与磁盘或其他外部存储器相关联。B树的目的是减少磁盘的I/O操作,提高查找效率。

二叉树和B树的区别

  1. 二叉树是一种典型的普通树,而B树是一种中序遍历结果有序的多路平衡树。
  2. 二叉树中的节点最多可以有两个子节点,而B树中的节点可以有多个子节点,取决于磁盘块的大小。
  3. 二叉树的高度为 log_ {2} N ,其中 N 是节点个数。而B树的高度为 log_ {M} N ,其中 M 是B树的阶,也就是一个节点可以最多包含关键字的个数。
  4. 二叉树中节点的插入和删除操作相对简单,而B树中节点的插入和删除操作相对复杂,需要进行分裂、合并、旋转等操作来保持平衡。

B树代码

// B树的节点类
class BTreeNode {
    int[] keys; // 存储关键字的数组
    int t; // 每个节点最少有t-1个关键字,最多有2t-1个关键字
    BTreeNode[] children; // 存储子节点的数组
    int n; // 当前节点的关键字个数
    boolean leaf; // 是否是叶子节点

    // 构造函数,初始化节点
    public BTreeNode(int t, boolean leaf) {
        this.t = t;
        this.leaf = leaf;
        this.keys = new int[2 * t - 1]; // 分配空间给关键字数组
        this.children = new BTreeNode[2 * t]; // 分配空间给子节点数组
        this.n = 0; // 初始化关键字个数为0
    }

    // 打印节点及其子树的内容,用于调试
    public void print() {
        System.out.print("[");
        for (int i = 0; i < n; i++) {
            if (!leaf) {
                children[i].print(); // 递归打印子节点
            }
            System.out.print(" " + keys[i] + " ");
        }
        if (!leaf) {
            children[n].print(); // 递归打印最后一个子节点
        }
        System.out.print("]");
    }

    // 在不满的节点中插入一个关键字,假设该节点不是叶子节点
    public void insertNonFull(int k) {
        int i = n - 1; // 从右向左找到插入位置
        if (leaf) { // 如果是叶子节点,直接插入并排序
            while (i >= 0 && keys[i] > k) {
                keys[i + 1] = keys[i]; // 将大于k的关键字右移一位
                i--;
            }
            keys[i + 1] = k; // 将k插入到合适的位置
            n++; // 增加关键字个数
        } else { // 如果不是叶子节点,需要找到合适的子节点进行插入
            while (i >= 0 && keys[i] > k) {
                i--; // 找到第一个小于等于k的关键字
            }
            i++; // i是要插入的子节点的索引
            if (children[i].n == 2 * t - 1) { // 如果该子节点已满,需要分裂
                splitChild(i); // 分裂该子节点,将中间的关键字上移至当前节点
                if (keys[i] < k) { // 判断k应该插入到哪个分裂后的子节点中
                    i++;
                }
            }
            children[i].insertNonFull(k); // 递归地在合适的子节点中插入k
        }
    }

    // 分裂一个满的子节点,将中间的关键字上移至当前节点中,假设当前节点不满
    public void splitChild(int i) {
        BTreeNode y = children[i]; // y是要分裂的子节点
        BTreeNode z = new BTreeNode(y.t, y.leaf); // z是分裂后的新节点,和y有相同的t和leaf属性
        z.n = t - 1; // z有t-1个关键字

        for (int j = 0; j < t - 1; j++) { // 将y的后t-1个关键字复制到z中
            z.keys[j] = y.keys[j + t];
        }
        if (!y.leaf) { // 如果y不是叶子节点,还需要将y的后t个子节点复制到z中          
            for (int j = 0; j < t; j++) { // 将y的后t个子节点复制到z中
                z.children[j] = y.children[j + t];
            }
        }
        y.n = t - 1; // y现在只有t-1个关键字

        for (int j = n; j >= i + 1; j--) { // 将当前节点的子节点数组右移一位,为z腾出空间
            children[j + 1] = children[j];
        }
        children[i + 1] = z; // 将z作为当前节点的子节点

        for (int j = n - 1; j >= i; j--) { // 将当前节点的关键字数组右移一位,为y的中间关键字腾出空间
            keys[j + 1] = keys[j];
        }
        keys[i] = y.keys[t - 1]; // 将y的中间关键字上移至当前节点
        n++; // 增加当前节点的关键字个数
    }

    // 在B树中查找一个关键字,返回一个包含该关键字的节点和索引的对象,如果不存在则返回null
    public SearchResult search(int k) {
        int i = 0; // 从左向右找到第一个大于等于k的关键字
        while (i < n && keys[i] < k) {
            i++;
        }
        if (i < n && keys[i] == k) { // 如果找到了k,返回结果
            return new SearchResult(this, i);
        }
        if (leaf) { // 如果是叶子节点,说明k不存在,返回null
            return null;
        } else { // 如果不是叶子节点,递归地在合适的子节点中查找k
            return children[i].search(k);
        }
    }
}

// B树的类
class BTree {
    BTreeNode root; // 根节点
    int t; // 每个节点最少有t-1个关键字,最多有2t-1个关键字

    // 构造函数,初始化B树
    public BTree(int t) {
        this.t = t;
        this.root = null;
    }

    // 打印B树的内容,用于调试
    public void print() {
        if (root != null) {
            root.print();
        }
    }

    // 在B树中插入一个关键字,如果已存在则不插入
    public void insert(int k) {
        if (root == null) { // 如果根节点为空,创建一个新的根节点并插入k
            root = new BTreeNode(t, true);
            root.keys[0] = k;
            root.n = 1;
        } else { // 如果根节点不为空,先检查是否已满
            if (root.n == 2 * t - 1) { // 如果根节点已满,需要分裂并增加高度
                BTreeNode s = new BTreeNode(t, false); // s是新的根节点
                s.children[0] = root; // s的第一个子节点是原来的根节点
                s.splitChild(0); // 分裂原来的根节点,将中间的关键字上移至s中
                int i = 0; // 判断k应该插入到哪个分裂后的子节点中
                if (s.keys[0] < k) {
                    i++;
                }
                s.children[i].insertNonFull(k); // 在合适的子节点中插入k
                root = s; // 更新根节点为s
            } else { // 如果根节点不满,直接在根节点中插入k
                root.insertNonFull(k);
            }
        }
    }
    
    // 在B树中查找一个关键字,返回一个包含该关键字的节点和索引的对象,如果不存在则返回null
    public SearchResult search(int k) {
        if (root == null) { // 如果根节点为空,说明B树为空,返回null
            return null;
        } else { // 如果根节点不为空,递归地在根节点中查找k
            return root.search(k);
        }
    }
}

// 查找结果的类,包含一个节点和一个索引
class SearchResult {
    BTreeNode node; // 包含关键字的节点
    int index; // 关键字在节点中的索引

    // 构造函数,初始化查找结果
    public SearchResult(BTreeNode node, int index) {
        this.node = node;
        this.index = index;
    }
}

多路查找树

B 树、B+树和 B*树

多路查找树是一种特殊的查找树,它的每个节点可以有多个元素和多个子节点,从而提高了查找效率。多路查找树有不同的类型,如2-3树、2-3-4树、B树、B+树和B*树,它们的区别主要在于节点的元素个数和子节点个数,以及元素在节点中的分布方式

概念:

  1. m叉树:每个非叶节点有最多m棵子树的树,其中m称为树的度。
  2. 2-3树:一种特殊的m叉树,每个节点最多有两个子节点或三个子节点,且所有叶节点在同一层上。
  3. 2-3-4树:一种特殊的m叉树,每个节点最多有两个子节点或三个子节点或四个子节点,且所有叶节点在同一层上。
  4. B树:一种平衡的m叉树,用于磁盘或其他随机访问的存储设备上的数据结构。B树通常具有大的度数和大的高度。
  5. B+树:B树的一种变体,它将数据存储在叶子节点中,而非内部节点中,以提高数据访问的效率。
  6. B*树:B+树的一种变体,它将内部节点占据的空间比例增加到了2/3,以降低B+树的高度。
  7. 2-3-5树:一种非常高效的多路查找树,它将3节点拆分为两个子节点,以减少查找和插入操作的平均路径长度。

总体来说,多路查找树是一类高效的数据结构,可以用于存储和查询大量数据。其中,B树和其变体常常被用于数据库和文件系统等存储设备上。而2-3-5树则被认为是一种非常高效的多路查找树。