🌲 树形结构:从二叉树到B+树,MySQL索引的秘密!

4 阅读8分钟

"树就像家族族谱,从祖先到后代,层层分明!" 👨‍👩‍👧‍👦


🌳 什么是树?家族族谱的故事

👨‍👩‍👧‍👦 生活中的树

      爷爷(根节点)
      /  \
   爸爸  叔叔
   /  \    \
  我  弟弟  堂弟

术语:
- 根节点:爷爷(最上面的节点)
- 父节点:爸爸是我的父节点
- 子节点:我和弟弟是爸爸的子节点
- 兄弟节点:我和弟弟是兄弟
- 叶子节点:我、弟弟、堂弟(没有孩子的节点)
- 高度:从根到叶子的最长路径(这里是3层)

📊 二叉树(Binary Tree)

基本概念

二叉树:每个节点最多有2个子节点

        1
       / \
      2   3
     / \   \
    4   5   6

节点定义

class TreeNode {
    int val;
    TreeNode left;   // 左孩子
    TreeNode right;  // 右孩子
    
    TreeNode(int val) {
        this.val = val;
    }
}

二叉树的类型

1. 满二叉树(Full Binary Tree)

每层都是满的!

        1
       / \
      2   3
     / \ / \
    4 5 6  7
    
节点数:2^h - 1(h是高度)

2. 完全二叉树(Complete Binary Tree)

除了最后一层,其他层都是满的,
最后一层的节点靠左排列

        1
       / \
      2   3
     / \ /
    4 5 6
    
用途:堆(Heap)就是完全二叉树!

3. 二叉搜索树(BST - Binary Search Tree)⭐⭐⭐

性质:
- 左子树的所有节点 < 根节点
- 右子树的所有节点 > 根节点
- 左右子树也是BST

        5
       / \
      3   7
     / \ / \
    2  4 6  8
    
查找、插入、删除:平均O(log n)
最坏情况:退化成链表O(n)

BST操作

// 查找
TreeNode search(TreeNode root, int val) {
    if (root == null || root.val == val) {
        return root;
    }
    
    if (val < root.val) {
        return search(root.left, val);   // 去左子树找
    } else {
        return search(root.right, val);  // 去右子树找
    }
}

// 插入
TreeNode insert(TreeNode root, int val) {
    if (root == null) {
        return new TreeNode(val);
    }
    
    if (val < root.val) {
        root.left = insert(root.left, val);
    } else {
        root.right = insert(root.right, val);
    }
    
    return root;
}

⚖️ AVL树(平衡二叉搜索树)

为什么需要AVL树?

BST的问题

插入顺序:1, 2, 3, 4, 5

BST退化成链表:
  1
   \
    2
     \
      3
       \
        4
         \
          5
          
查找效率:O(n) 😱

AVL树的解决方案自动平衡

AVL树会自动旋转,保持平衡:

        3
       / \
      2   4
     /     \
    1       5
    
查找效率:O(log n) ✅

AVL树的平衡条件

平衡因子 = 左子树高度 - 右子树高度

每个节点的平衡因子 ∈ {-1, 0, 1}

如果超出范围,需要旋转!

四种旋转

1. LL(左左)- 右旋
2. RR(右右)- 左旋
3. LR(左右)- 先左旋后右旋
4. RL(右左)- 先右旋后左旋

例子:LL情况

不平衡:        右旋后:
    3              2
   /              / \
  2              1   3
 /
1

平衡因子:3的平衡因子=2,超标!
解决:右旋

🔴⚫ 红黑树(Red-Black Tree)⭐⭐⭐⭐⭐

为什么Java用红黑树而不是AVL树?

特性AVL树红黑树
平衡性😊 严格平衡😐 近似平衡
查找速度⚡ 略快⚡ 略慢
插入删除🐌 慢(频繁旋转)⚡ 快
应用查询多插入删除多

结论:红黑树在插入删除操作更多的场景下更优!

红黑树的5条性质

1. 每个节点不是红色就是黑色 🔴⚫
2. 根节点是黑色 ⚫
3. 叶子节点(NIL)是黑色 ⚫
4. 红色节点的子节点必须是黑色(不能有连续的红色)🔴
5. 从任意节点到其叶子的所有路径,包含相同数量的黑色节点

红黑树示例

        ⚫7
       /   \
     🔴3   🔴18
     / \    / \
   ⚫1 ⚫5 ⚫10⚫22

红黑树的应用

✅ Java的TreeMap、TreeSet
✅ C++ STL的map、set
✅ Linux内核的进程调度
✅ HashMap的链表转红黑树(JDK 8+)

📚 B树(B-Tree)

为什么需要B树?

问题:二叉树在磁盘存储场景下效率低!

磁盘读取特点:
- 按块读取(每次读4KB或更多)
- 读取慢(机械硬盘)

二叉树问题:
- 每个节点只存1个数据,浪费!
- 树高太高,读取次数多

解决方案:B树 - 多叉树,每个节点存多个数据!

B树的特点

一个5B树:

                [50, 100]
               /    |     \
        [20,30]  [60,70]  [110,120]
        /  |  \   /  |  \   /   |   \
      ...  ... ... ... ... ... ... ... ...

特点:
- 每个节点可以有多个key(不只2个)
- 每个节点可以有多个子节点
- 所有叶子节点在同一层
- 节点内的key有序

B树的定义

m阶B树:
1. 每个节点最多有m个子节点
2. 除根外,每个节点至少有⌈m/2⌉个子节点
3. 根节点至少有2个子节点(除非是叶子)
4. 所有叶子在同一层
5. 有k个子节点的节点包含k-1个key

🏆 B+树(B+ Tree)⭐⭐⭐⭐⭐⭐

MySQL为什么用B+树做索引?

B树 vs B+树

特性B树B+树
数据存储所有节点都存数据只有叶子节点存数据
叶子链表叶子节点形成链表
范围查询🐌 慢⚡ 超快
顺序访问🐌 慢⚡ 超快

B+树结构

B+树(MySQL InnoDB索引):

                   [50, 100]
                  /    |     \
           [20,30]   [60,70]  [110,120]
              ↓         ↓         ↓
      [10,20,30][50,60,70][100,110,120] ⇄ ...
        ↓  ↓  ↓      ↓  ↓  ↓       ↓   ↓   ↓
       数据 数据 数据   数据 数据 数据    数据 数据 数据

特点:
1. 非叶子节点只存key,不存数据(索引)
2. 所有数据都在叶子节点
3. 叶子节点形成有序链表(⇄)

为什么B+树适合MySQL?

1. 范围查询超快!⚡

查询 age BETWEEN 20 AND 30B树:需要遍历整棵树 😱
B+树:找到20,然后沿着链表一直到30[10,20,30][40,50,60][70,80,90]
     ↑____________↑
   从20开始,到30结束

2. 磁盘IO次数少!💾

B+树的非叶子节点不存数据,只存key:
→ 每个节点能存更多key
→ 树的高度更低
→ 磁盘IO次数更少

例子:
B树:高度4,需要4次IO
B+树:高度3,需要3次IO ✅

3. 全表扫描高效!📊

B+树叶子节点形成链表:
遍历链表即可,不需要遍历整棵树!

[10][20][30][40][50] ...

MySQL InnoDB的B+树

一个节点 = 一页 = 16KB

假设:
- key: 8字节
- 指针: 6字节
- 一个节点可存: 16KB / 14B ≈ 1170个key

3层B+树能存多少数据?
第1层:1个节点
第2层:1170个节点
第3层:1170 × 1170 = 1,368,900个叶子节点

如果每个叶子节点存16条数据:
1,368,900 × 16 = 21,902,400条数据

结论:3层B+树能存2000万+数据!😱
查找只需3次IO!⚡

代码示例:B+树节点

class BPlusTreeNode {
    boolean isLeaf;           // 是否是叶子节点
    int[] keys;               // key数组
    BPlusTreeNode[] children; // 子节点指针(非叶子节点)
    Object[] data;            // 数据(叶子节点)
    BPlusTreeNode next;       // 下一个叶子节点(叶子节点的链表)
    
    BPlusTreeNode(boolean isLeaf) {
        this.isLeaf = isLeaf;
        this.keys = new int[MAX_KEYS];
        
        if (isLeaf) {
            this.data = new Object[MAX_KEYS];
        } else {
            this.children = new BPlusTreeNode[MAX_KEYS + 1];
        }
    }
}

🎯 二叉树的遍历

1. 前序遍历(Pre-order)

顺序:根 → 左 → 右

        1
       / \
      2   3
     / \
    4   5

遍历顺序:1 → 2 → 4 → 5 → 3
void preorder(TreeNode root) {
    if (root == null) return;
    System.out.print(root.val + " ");  // 根
    preorder(root.left);               // 左
    preorder(root.right);              // 右
}

2. 中序遍历(In-order)⭐

顺序:左 → 根 → 右

        5
       / \
      3   7
     / \ / \
    2  4 6  8

遍历顺序:2 → 3 → 4 → 5 → 6 → 7 → 8

BST的中序遍历是有序的!✅
void inorder(TreeNode root) {
    if (root == null) return;
    inorder(root.left);                // 左
    System.out.print(root.val + " ");  // 根
    inorder(root.right);               // 右
}

3. 后序遍历(Post-order)

顺序:左 → 右 → 根

        1
       / \
      2   3
     / \
    4   5

遍历顺序:4 → 5 → 2 → 3 → 1
void postorder(TreeNode root) {
    if (root == null) return;
    postorder(root.left);              // 左
    postorder(root.right);             // 右
    System.out.print(root.val + " ");  // 根
}

4. 层序遍历(Level-order)⭐

一层一层遍历:

        1
       / \
      2   3
     / \
    4   5

遍历顺序:1 → 2 → 3 → 4 → 5
void levelOrder(TreeNode root) {
    if (root == null) return;
    
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        System.out.print(node.val + " ");
        
        if (node.left != null) queue.offer(node.left);
        if (node.right != null) queue.offer(node.right);
    }
}

🏆 经典面试题

1. 验证二叉搜索树(LeetCode 98)

public boolean isValidBST(TreeNode root) {
    return isValidBST(root, null, null);
}

private boolean isValidBST(TreeNode node, Integer lower, Integer upper) {
    if (node == null) return true;
    
    int val = node.val;
    if (lower != null && val <= lower) return false;
    if (upper != null && val >= upper) return false;
    
    return isValidBST(node.left, lower, val) && 
           isValidBST(node.right, val, upper);
}

2. 二叉树的最大深度(LeetCode 104)

public int maxDepth(TreeNode root) {
    if (root == null) return 0;
    
    int leftDepth = maxDepth(root.left);
    int rightDepth = maxDepth(root.right);
    
    return Math.max(leftDepth, rightDepth) + 1;
}

3. 二叉树的最近公共祖先(LeetCode 236)

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) {
        return root;
    }
    
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    
    if (left != null && right != null) {
        return root;  // 一个在左,一个在右,root就是LCA
    }
    
    return left != null ? left : right;
}

📊 树形结构总结

树类型特点时间复杂度应用场景
二叉树每个节点≤2个子节点-基础
BST左<根<右O(log n)~O(n)查找
AVL树严格平衡O(log n)查找密集
红黑树近似平衡O(log n)TreeMap
B树多叉树O(log n)数据库
B+树叶子存数据+链表O(log n)MySQL索引

📝 总结

🎓 记忆口诀

树像家族谱,层层有分明。
二叉树最基础,BST能查找。
AVL严格平衡,红黑树更灵活。
HashMap用红黑树,TreeMap也用它。
B树是多叉树,磁盘存储佳。
B+树MySQL用,范围查询快。
叶子节点成链表,顺序遍历妙!

恭喜你!🎉 你已经掌握了从二叉树到B+树的核心知识!

这些是Java高级工程师面试的重点中的重点!💪


📌 重点记忆:

  • 红黑树:HashMap、TreeMap
  • B+树:MySQL InnoDB索引
  • BST中序遍历有序

🤔 思考题:为什么MySQL不用红黑树而用B+树?

(答案:B+树磁盘IO少、范围查询快、叶子链表顺序访问快!)