数据结构==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],子节点 [1]、[5](叶子节点同层)。
  4. 插入 7:定位到叶子节点 [5],插入后为 [5,7](关键字数 = 2,未分裂)。
  5. 插入 9:定位到叶子节点 [5,7],插入后为 [5,7,9](触发分裂):
    • 中间关键字 7 上移到父节点 [3],父节点变为 [3,7];
    • 原节点拆分为 [5](左)、[9](右);
    • 最终树结构:根 [3,7],子节点 [1]、[5]、[9](叶子节点仍在同一层)。
  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 树性质的核心步骤。
相关推荐
带鱼吃猫3 小时前
数据结构:单链表 / 双链表的结构、接口实现与顺序表对比
数据结构·链表
for_ever_love__3 小时前
二插堆的基本原理以及简单实现
数据结构
月明长歌4 小时前
【码道初阶】【LeetCode 150】逆波兰表达式求值:为什么栈是它的最佳拍档?
java·数据结构·算法·leetcode·后缀表达式
C雨后彩虹4 小时前
最大数字问题
java·数据结构·算法·华为·面试
Han.miracle4 小时前
数据结构与算法--006 和为s的两个数字(easy)
java·数据结构·算法·和为s的两个数字
AuroraWanderll4 小时前
C++类和对象--访问限定符与封装-类的实例化与对象模型-this指针(二)
c语言·开发语言·数据结构·c++·算法
Dylan的码园4 小时前
链表与LinkedList
java·数据结构·链表
Han.miracle5 小时前
优选算法-005 有效三角形的个数(medium)
数据结构·算法·有效的三角形个数
yuuki2332335 小时前
【C++】类和对象下
数据结构·c++·算法