数据结构==B-树==

一、B 树基础概念(铺垫层)

知识点梳理

  1. 名称定义 :B 树(B-tree)是多叉平衡查找树,正确名称为 "B 树",非 "B 阶树"("阶" 是 B 树的属性,如 m 阶 B 树)。
  2. 树型对比
    • 二叉树体系:二叉搜索树(BST)、AVL 树(绝对平衡二叉树)、红黑树(相对平衡二叉树),均为二叉结构
    • B 树:多叉结构,平衡特性体现在 "所有叶子节点在同一层"。
  3. 适用场景 :哈希、二叉树等结构仅适用于内存数据处理;当数据量过大(超出内存容量)需存储在磁盘(外存)时,B 树为核心解决方案。

重点标注【重点】

  1. B 树的核心定位:多叉 + 平衡 + 查找(兼具有序性和外存适配性)。
  2. 二叉树与 B 树的本质区别:子节点数量 (二叉 vs 多叉)、应用场景(内存 vs 外存)。

难点解析【难点】

为何二叉树不适合外存数据处理? 二叉树的树高随数据量增长呈O(log₂n) 趋势(如 100 万条数据,树高约 20),而外存操作的核心成本是磁盘 IO(每次 IO 仅能读取一个节点),二叉树的高树高会导致大量 IO 次数,性能急剧下降。

内容扩张(补充对比与原理)

  1. 内存 vs 外存性能差异

    • 内存访问速度:纳秒级(10⁻⁹s);
    • 磁盘 IO 访问速度:毫秒级(10⁻³s),相差约 100 万倍。因此,外存操作的核心目标是减少 IO 次数,而非单纯减少计算次数。
  2. 常见树结构适用场景对比表

    树结构 核心特性 适用场景
    二叉搜索树 有序,无平衡保证 少量内存数据、单次查询
    AVL 树 绝对平衡,旋转次数多 少量内存数据、频繁查询
    红黑树 相对平衡,旋转次数少 大量内存数据(如 Java TreeMap)
    B 树 多叉平衡,树高低 外存数据处理(如数据库索引)

二、磁盘 IO 与 B 树的优势(核心价值层)

知识点梳理

  1. 磁盘 IO 慢的物理原因 :磁盘是机械结构,由盘片、磁头、扇区组成,读取数据时需经历磁头寻道(找扇区)→盘片旋转(定位数据),这两个步骤耗时占比超 99%。
  2. B 树的优势原理:B 树为 "矮胖" 结构(多叉导致树高极低),可大幅减少磁盘 IO 次数(每次 IO 读取一个 B 树节点,节点可存储多个关键字)。

重点标注【重点】

  1. 磁盘 IO 的性能瓶颈:磁头寻道 + 盘片旋转(而非数据传输)。
  2. B 树 "矮胖" 结构的核心意义:降低树高 = 减少 IO 次数

难点解析【难点】

如何量化 B 树的 IO 次数优势?m 阶 B 树二叉搜索树为例,假设存储 n=100 万条数据:

  • 二叉搜索树(平衡时):树高 h=log₂10⁶≈20 → IO 次数≈20 次;
  • m=100 阶 B 树:根据 B 树树高公式(见下文),树高 h≈3 → IO 次数≈3 次。结论:B 树的 IO 次数呈数量级下降。

内容扩张(公式与计算)

  1. m 阶 B 树的树高公式(推导:基于 B 树性质的最小节点数):设 m 阶 B 树的树高为 h(根节点为第 1 层),则最小总关键字数为:Nmin=1+2×(⌈m/2⌉)h−2×(⌈m/2⌉−1)简化(取⌈m/2⌉=k):n≥1+2(k−1)(kh−2) → 树高h≤logk(2(k−1)n−1)+2。
  2. 实例计算:m=100(k=50),n=10⁶:h≤log50(2×49106−1)+2≈log50(10204)+2≈3,验证上述结论。

三、B 树的核心性质(规则核心层)

知识点梳理(m 阶 B 树,m 为树的阶数:每个节点最多有 m 个子节点)

  1. 根节点规则
    • 关键字数量:最少 1 个,最多 m-1 个;
    • 子节点数量:最少 2 个,最多 m 个。
  2. 非根节点规则
    • 关键字数量:最少⌈m/2⌉−1个,最多 m-1 个;
    • 子节点数量:最少⌈m/2⌉个,最多 m 个(子节点数 = 关键字数 + 1)。
  3. 关键字关系:节点内关键字按升序排列,且第 i 个关键字的左子树所有关键字 <该关键字,右子树所有关键字> 该关键字(与二叉搜索树一致)。
  4. 叶子节点规则 :所有叶子节点在同一层(平衡的核心体现),且叶子节点无子女(或视为空节点)。

重点标注【重点】

  1. 子节点数与关键字数的关系:子节点数 = 关键字数 + 1(B 树的核心关联规则)。
  2. 关键字的有序性和叶子节点的同层性(B 树平衡的两大保证)。
  3. 各节点的关键字 / 子节点数范围(需熟记,为插入分裂打基础)。

难点解析【难点】

为何非根节点的关键字数下限是⌈m/2⌉−1? 该下限是为了保证 B 树的平衡特性和最坏情况下的查找效率

  • 若下限过低(如 1 个),可能导致节点分布不均(部分节点关键字多,部分极少),破坏 "矮胖" 结构;
  • ⌈m/2⌉−1是 "最小平衡阈值",确保节点分裂 / 合并后仍能维持平衡,且树高始终保持 O (log_m n) 级别。

内容扩张(实例验证)

以 **m=3 阶 B 树(2-3 树)m=4 阶 B 树(2-3-4 树)** 为例,直观理解性质:

  1. m=3(k=2,⌈3/2⌉=2)
    • 非根节点关键字数:1~2 个(⌈3/2⌉−1=1,m-1=2);
    • 子节点数:2~3 个;
    • 实例:插入 1,2,3 → 根节点关键字满(2 个),插入 4 触发分裂,中间元素 2 上移为新根,1 和 3/4 为子节点,叶子节点同层。
  2. m=4(k=2,⌈4/2⌉=2)
    • 非根节点关键字数:1~3 个;
    • 子节点数:2~4 个;
    • 平衡特性:无论插入多少数据,叶子节点始终在同一层。

四、B 树的插入与分裂机制(操作核心层)

知识点梳理

  1. 插入前置步骤
    • 查找元素是否存在(存在则不插入,去重);
    • 若不存在,定位到叶子节点(插入只能在叶子节点,核心规则)。
  2. 插入过程 :在叶子节点中对关键字进行插入排序(保持有序性)。
  3. 分裂触发条件:节点关键字数达到 m-1(满),再插入一个关键字时触发分裂。
  4. 分裂步骤
    • 取节点的中间关键字(位置:⌊m/2⌋或⌈m/2⌉);
    • 中间关键字上移到父节点;
    • 原节点拆分为左、右两个新节点(中间关键字左侧为左节点,右侧为右节点)。
  5. 树高变化 :仅当根节点分裂时,树高 + 1(普通节点分裂为横向扩展,树高不变)。

重点标注【重点】

  1. 插入的核心规则:只能插在叶子节点(区别于二叉树的插入位置)。
  2. 分裂的触发条件:关键字数 = m(原节点满 m-1,插入后为 m)。
  3. 分裂的核心步骤:中间关键字上移 + 原节点拆分

难点解析【难点】

  1. 分裂的递归处理:若中间关键字上移后,父节点也满了(关键字数 = m-1),则父节点需继续分裂,直到根节点(根节点分裂后树高 + 1)。
  2. 中间关键字的选择:m 为奇数时,中间关键字唯一;m 为偶数时,通常取⌈m/2⌉位置的关键字(如 m=4,取第 2 个关键字)。

内容扩张(实例演示)

m=3 阶 B 树 为例,演示插入序列1,3,5,7,9的完整过程:

  1. 插入 1:根节点为 1(关键字数 = 1,符合规则)。
  2. 插入 3:根节点为 1,3(关键字数 = 2,达到 m-1=2,未分裂)。
  3. 插入 5:根节点关键字数 = 3(超出 m-1=2),触发分裂:
    • 中间关键字 3 上移为新根;
    • 原节点拆分为 1(左)、5(右);
    • 树结构:根 3,子节点 15(叶子节点同层)。
  4. 插入 7:定位到叶子节点 5,插入后为 5,7(关键字数 = 2,未分裂)。
  5. 插入 9:定位到叶子节点 5,7,插入后为 5,7,9(触发分裂):
    • 中间关键字 7 上移到父节点 3,父节点变为 3,7
    • 原节点拆分为 5(左)、9(右);
    • 最终树结构:根 3,7,子节点 159(叶子节点仍在同一层)。
  6. 若继续插入 11:定位到 9,插入后为 9,11,无分裂;插入 13:9,11,13 触发分裂,中间关键字 11 上移到父节点 3,7,父节点变为 3,7,11(触发根节点分裂),中间关键字 7 上移为新根,树高 + 1。

五、B 树的代码实现(工程落地层)

知识点梳理(会议提及思路)

  1. 节点定义 :包含关键字数组、孩子节点数组、父节点引用、记录关键字数量的usedSize
  2. 查找方法:返回自定义结果类(包含节点和下标)(找到则返回节点和关键字下标,未找到则返回父节点和 - 1)。
  3. 插入方法
    • 树为空:创建根节点,插入元素;
    • 树非空:调用查找方法,判断元素是否存在,不存在则在叶子节点插入,插入后检查是否需要分裂。
  4. 分裂方法:处理节点拆分、中间关键字上移、父节点更新。

重点标注【重点】

  1. 节点数据结构设计:需同时存储关键字和子节点(多叉树的核心),且数组大小为 m(关键字数组大小 m-1,子节点数组大小 m)。
  2. 查找方法的返回值设计:用自定义类返回父节点和下标,是插入的关键前提(定位插入位置)。
  3. 插入后的分裂检查 :插入后需立即判断usedSize == m,触发分裂逻辑。

难点解析【难点】

  1. 分裂的代码实现
    • 拆分原节点的关键字和子节点到左、右新节点;
    • 中间关键字插入父节点后,若父节点满,需递归分裂
    • 关键字和子节点的移位操作(保持有序性)。
  2. 多阶 B 树的通用性设计:代码需支持任意 m 阶,而非固定阶数。

内容扩张(Java 代码实现:核心部分)

java

运行

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

/**
 * m阶B树(此处取m=3,可修改常量支持任意阶)
 * 核心重点:节点结构设计、查找逻辑、插入流程
 * 核心难点:分裂的递归实现、节点拆分与关键字移位
 */
public class BTree {
    // 定义B树的阶数(m阶:每个节点最多有m个子节点)
    private static final int m = 3;
    // 每个节点最多存储的关键字数量:m-1
    private static final int MAX_KEY = m - 1;
    // 非根节点最少存储的关键字数量:⌈m/2⌉ - 1(m=3时为1)
    private static final int MIN_KEY = (m + 1) / 2 - 1;

    // B树节点类【重点:节点结构设计】
    static class BTreeNode {
        int[] keys;          // 关键字数组,最多存储MAX_KEY个
        BTreeNode[] children;// 孩子节点数组,最多存储m个
        BTreeNode parent;    // 父节点引用
        int usedSize;        // 当前关键字数量
        boolean isLeaf;      // 是否为叶子节点

        // 构造函数
        public BTreeNode() {
            this.keys = new int[MAX_KEY];
            this.children = new BTreeNode[m];
            this.parent = null;
            this.usedSize = 0;
            this.isLeaf = true; // 初始为叶子节点
        }
    }

    // 查找结果类:替代C++的pair,存储节点和下标
    // 找到关键字:node为目标节点,index为关键字下标
    // 未找到关键字:node为父节点,index为-1
    static class SearchResult {
        BTreeNode node;
        int index;

        public SearchResult(BTreeNode node, int index) {
            this.node = node;
            this.index = index;
        }
    }

    private BTreeNode root; // B树的根节点

    public BTree() {
        this.root = null;
    }

    /**
     * 查找关键字
     * 核心重点:循环遍历节点内关键字,定位子节点或目标关键字
     * @param key 要查找的关键字
     * @return 查找结果(节点+下标)
     */
    private SearchResult search(int key) {
        BTreeNode curr = root;
        BTreeNode parent = null;

        while (curr != null) {
            int i = 0;
            // 找到第一个大于等于key的关键字下标
            while (i < curr.usedSize && curr.keys[i] < key) {
                parent = curr;
                i++;
            }
            // 找到关键字:返回当前节点和下标
            if (i < curr.usedSize && curr.keys[i] == key) {
                return new SearchResult(curr, i);
            }
            // 未找到:继续遍历子节点
            parent = curr;
            curr = curr.children[i];
        }
        // 未找到:返回父节点和-1
        return new SearchResult(parent, -1);
    }

    /**
     * 分裂节点:处理满节点的拆分【核心难点:递归分裂、节点拆分】
     * @param node 要分裂的节点(此时节点的usedSize == MAX_KEY + 1,即关键字数超上限)
     */
    private void split(BTreeNode node) {
        // 1. 创建右节点(左节点复用原节点)
        BTreeNode rightNode = new BTreeNode();
        BTreeNode parent = node.parent;
        // 中间关键字的下标(m=3时,mid=1;m为偶数时可调整为⌈m/2⌉)
        int mid = MAX_KEY / 2;
        int midKey = node.keys[mid];

        // 2. 拆分原节点的关键字到右节点(中间关键字右侧的关键字移到右节点)
        int j = 0;
        for (int i = mid + 1; i < node.usedSize; i++) {
            rightNode.keys[j] = node.keys[i];
            rightNode.usedSize++;
            node.usedSize--;
            j++;
        }

        // 3. 拆分原节点的孩子节点到右节点(非叶子节点时)
        if (!node.isLeaf) {
            j = 0;
            for (int i = mid + 1; i < m; i++) {
                rightNode.children[j] = node.children[i];
                if (rightNode.children[j] != null) {
                    rightNode.children[j].parent = rightNode;
                }
                node.children[i] = null; // 原节点该位置置空
                j++;
            }
        }
        rightNode.isLeaf = node.isLeaf;
        rightNode.parent = parent;

        // 4. 中间关键字插入父节点
        if (parent == null) {
            // 父节点为空(根节点分裂):创建新根节点【难点:根节点分裂处理】
            BTreeNode newRoot = new BTreeNode();
            newRoot.keys[0] = midKey;
            newRoot.usedSize = 1;
            newRoot.isLeaf = false; // 新根不是叶子节点
            newRoot.children[0] = node;
            newRoot.children[1] = rightNode;
            node.parent = newRoot;
            rightNode.parent = newRoot;
            this.root = newRoot; // 更新根节点
        } else {
            // 父节点非空:插入中间关键字到父节点【难点:父节点插入后递归分裂】
            // 找到插入位置
            int i = 0;
            while (i < parent.usedSize && parent.keys[i] < midKey) {
                i++;
            }

            // 关键字后移,腾出插入位置(保持有序性)
            for (int k = parent.usedSize; k > i; k--) {
                parent.keys[k] = parent.keys[k - 1];
            }

            // 孩子节点后移,腾出位置
            for (int k = parent.usedSize + 1; k > i + 1; k--) {
                parent.children[k] = parent.children[k - 1];
            }

            // 插入关键字和右孩子节点
            parent.keys[i] = midKey;
            parent.children[i + 1] = rightNode;
            parent.usedSize++;

            // 检查父节点是否满,满则递归分裂
            if (parent.usedSize > MAX_KEY) {
                split(parent);
            }
        }
    }

    /**
     * 插入关键字
     * 核心重点:叶子节点插入、插入后分裂检查
     * @param key 要插入的关键字
     */
    public void insert(int key) {
        // 情况1:树为空,创建根节点
        if (root == null) {
            root = new BTreeNode();
            root.keys[0] = key;
            root.usedSize = 1;
            return;
        }

        // 情况2:树非空,先查找关键字是否存在
        SearchResult result = search(key);
        if (result.index != -1) {
            // 关键字已存在,不插入
            System.out.println("关键字" + key + "已存在,不插入");
            return;
        }

        // 情况3:关键字不存在,定位到叶子节点插入
        BTreeNode leaf = result.node;
        int i = 0;
        // 找到插入位置
        while (i < leaf.usedSize && leaf.keys[i] < key) {
            i++;
        }

        // 关键字后移,腾出插入位置
        for (int k = leaf.usedSize; k > i; k--) {
            leaf.keys[k] = leaf.keys[k - 1];
        }

        // 插入关键字
        leaf.keys[i] = key;
        leaf.usedSize++;

        // 检查是否需要分裂(关键字数超过上限)
        if (leaf.usedSize > MAX_KEY) {
            split(leaf);
        }
    }

    /**
     * 层序遍历打印B树(辅助调试)
     */
    public void print() {
        if (root == null) {
            System.out.println("B树为空");
            return;
        }

        Queue<BTreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int levelSize = queue.size();
            for (int i = 0; i < levelSize; i++) {
                BTreeNode curr = queue.poll();
                if (curr == null) {
                    continue;
                }
                // 打印当前节点的关键字
                for (int j = 0; j < curr.usedSize; j++) {
                    System.out.print(curr.keys[j] + " ");
                }
                System.out.print("| ");
                // 将孩子节点加入队列
                for (int j = 0; j < m; j++) {
                    if (curr.children[j] != null) {
                        queue.offer(curr.children[j]);
                    }
                }
            }
            System.out.println(); // 换行表示下一层
        }
    }

    // 测试代码
    public static void main(String[] args) {
        BTree bTree = new BTree();
        // 插入测试序列
        int[] keys = {1, 3, 5, 7, 9, 11, 13};
        for (int key : keys) {
            bTree.insert(key);
        }
        // 打印B树
        System.out.println("B树层序遍历结果:");
        bTree.print();
    }
}
代码重难点标注
  1. 节点类BTreeNodekeyschildren数组的大小与 m 阶强关联(keys为 m-1,children为 m),是多叉树的核心设计,需严格匹配。
  2. 查找方法search :通过循环遍历节点内关键字,定位子节点或目标关键字,返回的SearchResult是插入逻辑的关键前提。
  3. 分裂方法split
    • 中间关键字的选择(mid = MAX_KEY / 2)是拆分节点的核心;
    • 根节点分裂时的新根创建是树高增长的唯一场景;
    • 父节点插入后的递归分裂是处理节点满的关键难点,确保 B 树始终满足平衡特性。
  4. 插入方法insert:叶子节点的关键字移位插入是保持有序性的关键,插入后的分裂检查是维持 B 树性质的核心步骤。
相关推荐
闪电悠米4 小时前
力扣hot100-41.缺失的第一个正数-原地哈希详解
数据结构·算法·哈希算法
WL学习笔记5 小时前
堆(数据结构)
数据结构·
wabs6669 小时前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl1010 小时前
图的最短路径Dijkstra
数据结构
zander25811 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态11 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面12 小时前
栈在递归中的应用
数据结构·算法
皓月斯语12 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king12 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79420 小时前
设置交集大小至少为2
数据结构·算法·leetcode