Files
interview/questions/08-算法与数据结构/B+树原理.md
yasinshaw 0e46a367c4 refactor: rename files to Chinese and organize by category
Organized 50 interview questions into 12 categories:
- 01-分布式系统 (9 files): 分布式事务, 分布式锁, 一致性哈希, CAP理论, etc.
- 02-数据库 (2 files): MySQL索引优化, MyBatis核心原理
- 03-缓存 (5 files): Redis数据结构, 缓存问题, LRU算法, etc.
- 04-消息队列 (1 file): RocketMQ/Kafka
- 05-并发编程 (4 files): 线程池, 设计模式, 限流策略, etc.
- 06-JVM (1 file): JVM和垃圾回收
- 07-系统设计 (8 files): 秒杀系统, 短链接, IM, Feed流, etc.
- 08-算法与数据结构 (4 files): B+树, 红黑树, 跳表, 时间轮
- 09-网络与安全 (3 files): TCP/IP, 加密安全, 性能优化
- 10-中间件 (4 files): Spring Boot, Nacos, Dubbo, Nginx
- 11-运维 (4 files): Kubernetes, CI/CD, Docker, 可观测性
- 12-面试技巧 (1 file): 面试技巧和职业规划

All files renamed to Chinese for better accessibility and
organized into categorized folders for easier navigation.

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-01 00:10:53 +08:00

9.2 KiB
Raw Blame History

B+ 树 (B+ Tree)

数据结构原理

什么是 B+ 树?

B+ 树是一种自平衡的树数据结构,是 B 树的变体,专门为磁盘存储和索引设计。它在数据库系统和文件系统中广泛应用,特别适合磁盘等外部存储设备。

B+ 树的特点

  1. 多路搜索树:每个节点可以有多个子节点
  2. 有序结构:所有键值按顺序存储
  3. 平衡性:从根到任何叶子的路径长度相同
  4. 叶节点链表:所有叶子节点通过指针连接成有序链表

B+ 树与 B 树的区别

特性 B+ 树 B 树
叶子节点 包含所有键值和指针 只包含键值
非叶子节点 只包含键值和指针,不包含数据 包含键值和指针,部分包含数据
查找效率 查找路径相同,但范围查询更高效 查找路径稍长
插入/删除 叶子节点统一操作,分布更均匀 数据分散在各级节点
范围查询 直接遍历叶子链表,高效 需要中序遍历整个树

图解说明

B+ 树结构示例m=3
                  [10, 20, 30]
                /    |    |    \
        [5, 8, 10]  [15,18,20]  [25,28,30]  []
            |        |         |          |
    [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]

关键概念

  • m:每个节点的最大子节点数
  • Key:用于索引的值
  • 指针Pointer:指向子节点或数据的地址
  • 叶子节点:存储实际数据的节点
  • 内部节点:用于索引的中间节点

Java 代码实现

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

class BPlusTreeNode {
    List<Integer> keys;
    List<BPlusTreeNode> children;
    BPlusTreeNode next;  // 叶子节点间的指针
    boolean isLeaf;

    public BPlusTreeNode(boolean isLeaf) {
        this.keys = new ArrayList<>();
        this.children = new ArrayList<>();
        this.isLeaf = isLeaf;
        this.next = null;
    }
}

public class BPlusTree {
    private BPlusTreeNode root;
    private int order; // B+树的阶

    public BPlusTree(int order) {
        this.root = new BPlusTreeNode(true);
        this.order = order;
    }

    // 插入操作
    public void insert(int key) {
        if (root.keys.size() == order - 1) {
            BPlusTreeNode newRoot = new BPlusTreeNode(false);
            newRoot.children.add(root);
            splitChild(newRoot, 0, root);
            root = newRoot;
        }
        insertNonFull(root, key);
    }

    private void insertNonFull(BPlusTreeNode node, int key) {
        if (node.isLeaf) {
            int i = 0;
            while (i < node.keys.size() && node.keys.get(i) < key) {
                i++;
            }
            node.keys.add(i, key);
        } else {
            int i = 0;
            while (i < node.keys.size() && node.keys.get(i) < key) {
                i++;
            }
            if (node.children.get(i).keys.size() == order - 1) {
                splitChild(node, i, node.children.get(i));
                if (node.keys.get(i) < key) {
                    i++;
                }
            }
            insertNonFull(node.children.get(i), key);
        }
    }

    private void splitChild(BPlusTreeNode parent, int index, BPlusTreeNode fullNode) {
        BPlusTreeNode newNode = new BPlusTreeNode(fullNode.isLeaf);

        // 移动后半部分键
        int mid = fullNode.keys.size() / 2;
        for (int i = mid + (fullNode.isLeaf ? 0 : 1); i < fullNode.keys.size(); i++) {
            newNode.keys.add(fullNode.keys.get(i));
        }
        fullNode.keys.subList(mid + (fullNode.isLeaf ? 0 : 1), fullNode.keys.size()).clear();

        // 移动子节点(如果是内部节点)
        if (!fullNode.isLeaf) {
            for (int i = mid + 1; i < fullNode.children.size(); i++) {
                newNode.children.add(fullNode.children.get(i));
            }
            fullNode.children.subList(mid + 1, fullNode.children.size()).clear();
        }

        // 如果是叶子节点,维护链表
        if (fullNode.isLeaf) {
            newNode.next = fullNode.next;
            fullNode.next = newNode;
        }

        // 插入到父节点
        parent.children.add(index + 1, newNode);
        parent.keys.add(index, fullNode.keys.get(mid));
    }

    // 查找操作
    public boolean search(int key) {
        return search(root, key);
    }

    private boolean search(BPlusTreeNode node, int key) {
        int i = 0;
        while (i < node.keys.size() && node.keys.get(i) < key) {
            i++;
        }

        if (i < node.keys.size() && node.keys.get(i) == key) {
            return true;
        }

        if (node.isLeaf) {
            return false;
        } else {
            return search(node.children.get(i), key);
        }
    }

    // 范围查询
    public List<Integer> rangeSearch(int start, int end) {
        List<Integer> result = new ArrayList<>();
        rangeSearch(root, start, end, result);
        return result;
    }

    private void rangeSearch(BPlusTreeNode node, int start, int end, List<Integer> result) {
        if (node.isLeaf) {
            for (int key : node.keys) {
                if (key >= start && key <= end) {
                    result.add(key);
                }
            }
        } else {
            int i = 0;
            while (i < node.keys.size() && node.keys.get(i) < start) {
                i++;
            }
            rangeSearch(node.children.get(i), start, end, result);
        }
    }

    // 删除操作
    public void delete(int key) {
        delete(root, key);
    }

    private void delete(BPlusTreeNode node, int key) {
        // 实现删除逻辑(简化版)
        // 实际实现需要处理合并、重新平衡等复杂逻辑
    }
}

时间复杂度分析

操作时间复杂度

操作 时间复杂度 说明
查找 O(log n) 树高为 O(log n),每层需要比较 O(m) 次
插入 O(log n) 查找插入位置 + 可能的分裂操作
删除 O(log n) 查找删除位置 + 可能的合并操作
范围查询 O(log n + k) k 是结果集大小

空间复杂度

  • O(n) - 需要存储 n 个元素
  • 每个节点存储约 m/2 到 m 个元素

实际应用场景

1. 数据库索引

  • MySQL InnoDB:聚簇索引使用 B+ 树
  • PostgreSQL:标准索引使用 B+ 树
  • 优点:范围查询高效,磁盘 I/O 次数少

2. 文件系统

  • NTFS:主文件表使用 B+ 树结构
  • ext4:目录索引使用 B+ 树
  • 优点:文件查找效率高,支持大文件系统

3. 内存数据库

  • Redis有序集合Sorted Set使用类似 B+ 树的结构
  • LevelDB:底层存储引擎使用 B+ 树变种
  • 优点:内存访问速度更快,但结构保持高效

4. 文件搜索

  • 全文搜索引擎:倒排索引使用 B+ 树
  • 文件系统搜索:快速定位文件位置
  • 优点:前缀查询和范围查询高效

与其他数据结构的对比

数据结构 查找时间 插入时间 删除时间 适用场景
B+ 树 O(log n) O(log n) O(log n) 磁盘存储、数据库索引
AVL 树 O(log n) O(log n) O(log n) 内存存储、需要平衡
红黑树 O(log n) O(log n) O(log n) 内存存储、平衡性较好
哈希表 O(1) O(1) O(1) 精确查找、内存存储
二叉搜索树 O(log n) ~ O(n) O(log n) ~ O(n) O(log n) ~ O(n) 排序数据、简单应用

选择 B+ 树的原因

  1. 磁盘友好:减少磁盘 I/O 次数
  2. 范围查询高效:叶子节点链表支持快速范围扫描
  3. 局部性原理:每次读取整个页面,充分利用磁盘预读
  4. 稳定性能:即使树不平衡,性能下降缓慢
  5. 批量操作:适合批量插入和删除

不同场景的最佳选择

  • 内存数据AVL 树或红黑树
  • 磁盘数据B+ 树
  • 精确查找:哈希表
  • 范围查询B+ 树
  • 频繁插入删除B+ 树或红黑树

常见面试问题

Q1: 为什么数据库索引使用 B+ 树而不是 B 树?

  1. 范围查询更高效B+ 树的叶子节点形成链表,范围查询只需遍历叶子节点
  2. 磁盘利用率高:非叶子节点不存储数据,可以存储更多索引键
  3. 查找稳定:无论查找什么数据,都到达叶子节点,树高相同
  4. 预读效率高:每次读取一个完整的页面

Q2: B+ 树的阶数如何选择?

阶数取决于:

  • 磁盘块大小通常等于操作系统页面大小4KB
  • 键值大小:整数 vs 字符串
  • 指针大小64位系统 8 字节指针
  • 缓存大小:内存缓存页面数量

Q3: B+ 树在什么情况下性能下降?

  1. 树太高:查找路径变长
  2. 叶子节点过多:范围查询变慢
  3. 内存不足:频繁磁盘 I/O
  4. 数据分布不均:导致树不平衡

Q4: 如何优化 B+ 树的性能?

  1. 选择合适的阶数:平衡磁盘 I/O 和内存使用
  2. 使用缓存:缓存热点数据
  3. 预加载:提前加载可能访问的节点
  4. 批量操作:合并多次插入/删除操作
  5. 数据分片:大表分片减少树的大小