B 树和 B + 树:数据结构的深度解析

目录

B 树和 B + 树:数据结构的深度解析

一、引言

二、B 树和 B + 树的基本概念

(一)从二叉树到 B 树

(二)B + 树与 B 树的区别

三、B 树和 B + 树的应用场景

四、为何使用 B 树或 B + 树作为索引结构

五、B 树和 B + 树的特点总结

(一)B 树特点

(二)B + 树特点

六、学习建议

二叉树节点定义示例

二叉查找树插入节点示例方法

平衡二叉树(AVL 树)节点定义及左旋右旋示例方法(简化示意,实际更复杂)

B 树节点定义示例(简化示意,未包含完整 B 树操作逻辑)

B + 树节点定义示例(简化示意,未包含完整 B + 树操作逻辑)


一、引言

在数据结构与算法领域,B 树和 B + 树是常常被提及的重要概念。不少小伙伴对它们存在疑惑,甚至质疑其在实际工作中的用处,以及为何面试中经常出现相关问题。今天,我们就来深入剖析 B 树和 B + 树,解答这些疑问,并分享一些相关的学习资源。

二、B 树和 B + 树的基本概念

(一)从二叉树到 B 树

  1. 二叉树:是一种每个节点最多支持两个分支的树结构,相比单向链表多了一个分支。二叉查找树在二叉树的基础上增加了规则,即左子树所有节点值小于根节点,右子树所有节点值大于根节点。但二叉查找树可能出现斜树问题,导致时间复杂度增加。
  2. 平衡二叉树(AVL 树):具有二叉查找树的所有特点,同时规定左右两个子树的高度差绝对值不能超过 1。它通过左旋和右旋的方式来维持平衡。
  3. B 树:是一种多路平衡查找树,满足平衡二叉树的规则,但可以有多个子树。子树的数量取决于关键字的数量,比如根节点有两个关键字,那么它能拥有的子树数量是关键字数量加 1。在存储同样数据量的情况下,平衡二叉树的高度要大于 B 树。

以下是根据上述视频内容涉及的部分概念,编写的一些相关 Java 示例代码,用于辅助理解其中提到的数据结构相关内容:

二叉树节点定义示例

class BinaryTreeNode {
    int value;
    BinaryTreeNode left;
    BinaryTreeNode right;

    public BinaryTreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

二叉查找树插入节点示例方法

class BinarySearchTree {
    private BinaryTreeNode root;

    public void insert(int value) {
        root = insertRecursive(root, value);
    }

    private BinaryTreeNode insertRecursive(BinaryTreeNode current, int value) {
        if (current == null) {
            return new BinaryTreeNode(value);
        }

        if (value < current.value) {
            current.left = insertRecursive(current.left, value);
        } else if (value > current.value) {
            current.right = insertRecursive(current.right, value);
        }

        return current;
    }
}

平衡二叉树(AVL 树)节点定义及左旋右旋示例方法(简化示意,实际更复杂)

class AVLTreeNode {
    int value;
    AVLTreeNode left;
    AVLTreeNode right;
    int height;

    public AVLTreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
        this.height = 1;
    }
}

class AVLTree {
    private AVLTreeNode root;

    // 获取节点高度
    private int height(AVLTreeNode node) {
        if (node == null) {
            return 0;
        }
        return node.height;
    }

    // 获取平衡因子
    private int getBalanceFactor(AVLTreeNode node) {
        if (node == null) {
            return 0;
        }
        return height(node.left) - height(node.right);
    }

    // 右旋操作(简化示意)
    private AVLTreeNode rightRotate(AVLTreeNode y) {
        AVLTreeNode x = y.left;
        AVLTreeNode T2 = x.right;

        x.right = y;
        y.left = T2;

        y.height = Math.max(height(y.left), height(y.right)) + 1;
        x.height = Math.max(height(x.left), height(x.right)) + 1;

        return x;
    }

    // 左旋操作(简化示意)
    private AVLTreeNode leftRotate(AVLTreeNode x) {
        AVLTreeNode y = x.right;
        AVLTreeNode T2 = y.left;

        y.left = x;
        x.right = T2;

        x.height = Math.max(height(x.left), height(x.right)) + 1;
        y.height = Math.max(height(y.left), height(y.right)) + 1;

        return y;
    }

    // 插入节点并保持平衡(简化示意,实际需更多处理)
    public void insert(int value) {
        root = insertRecursive(root, value);
    }

    private AVLTreeNode insertRecursive(AVLTreeNode current, int value) {
        if (current == null) {
            return new AVLTreeNode(value);
        }

        if (value < current.value) {
            current.left = insertRecursive(current.left, value);
        } else if (value > current.value) {
            current.right = insertRecursive(current.right, value);
        } else {
            // 不允许插入重复值,这里可根据实际需求处理
            return current;
        }

        current.height = 1 + Math.max(height(current.left), height(current.right));

        int balanceFactor = getBalanceFactor(current);

        // 左左情况
        if (balanceFactor > 1 && value < current.left.value) {
            return rightRotate(current);
        }

        // 右右情况
        if (balanceFactor < -1 && value > current.right.value) {
            return leftRotate(current);
        }

        // 左右情况
        if (balanceFactor > 1 && value > current.left.value) {
            current.left = leftRotate(current.left);
            return rightRotate(current);
        }

        // 右左情况
        if (balanceFactor < -1 && value < current.right.value) {
            current.right = rightRotate(current.right);
            return leftRotate(current);
        }

        return current;
    }
}

B 树节点定义示例(简化示意,未包含完整 B 树操作逻辑)

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

class BTreeNode {
    // 存储关键字
    List keys = new ArrayList<>();
    // 存储子节点
    List children = new ArrayList<>();
    boolean isLeaf;

    public BTreeNode(boolean isLeaf) {
        this.isLeaf = isLeaf;
    }

    // 添加关键字
    public void addKey(int key) {
        keys.add(key);
    }

    // 添加子节点
    public void addChild(BTreeNode child) {
        children.add(child);
    }
}

B + 树节点定义示例(简化示意,未包含完整 B + 树操作逻辑)

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

class BPlusTreeNode {
    // 存储关键字
    List keys = new ArrayList<>();
    // 存储指向子节点的指针(非叶子节点)或数据(叶子节点)
    List pointers = new ArrayList<>();
    boolean isLeaf;

    public BPlusTreeNode(boolean isLeaf) {
        this.isLeaf = isLeaf;
    }

    // 添加关键字
    public void addKey(int key) {
        keys.add(key);
    }

    // 添加指针或数据
    public void addPointer(Object pointer) {
        pointers.add(pointer);
    }
}
 
  

请注意,上述代码只是对视频中提及的数据结构概念进行简单的 Java 代码实现示例,用于辅助理解相关结构特点及操作逻辑的大致思路。真正完整且高效的实现这些数据结构会涉及到更多复杂的细节处理,比如节点的分裂、合并,数据的插入、删除以及平衡维护等操作,在实际应用场景中还需要根据具体需求进一步优化和完善。

(二)B + 树与 B 树的区别

B + 树是在 B 树基础上的增强。二者最大的区别有两点:

  1. 数据存储位置:B 树的数据存储在每个节点上,而 B + 树的数据只存储在叶子节点上,并且通过链表(双向链表)的方式把叶子节点中的数据进行连接。以下是一个简单的 Python 代码示例,用于创建一个简单的树结构(这里只是示意,与 B 树和 B + 树不完全相同,但可以帮助理解节点和数据存储概念):
# 定义树节点类
class TreeNode:
    def __init__(self, data=None):
        self.data = data
        self.children = []

# 创建一个简单的树(类似B树的节点存储数据方式)
root = TreeNode(10)
node2 = TreeNode(5)
node3 = TreeNode(15)
root.children = [node2, node3]

  1. 子树数量与关键字关系:B + 树的子树数量等于关键字数量,而 B 树的子树数量是关键字数量加 1。

三、B 树和 B + 树的应用场景

B 树和 B + 树一般应用在文件系统或者数据库系统中,主要目的是减少磁盘 I/O 带来的性能损耗。以 MySQL 的 InnoDB 为例,当执行select语句查询一条数据时,InnoDB 需要从磁盘读取数据,这个过程涉及磁盘 I/O 和磁盘随机 I/O。磁盘 I/O 的性能很低,特别是随机磁盘 I/O,因为磁盘 I/O 需要经过系统传逻辑地址、磁盘控制电路翻译物理地址、磁头寻道和旋转等步骤,耗时较长。所以 InnoDB 对存储在磁盘块上的数据建立索引,将索引数据以及索引列对应的磁盘地址以 B + 树的方式存储。这样在查找目标数据时,根据索引从 B + 树中查找,由于 B + 树的分支比较多,只需较少次数的磁盘 I/O 就能找到对应的数据。

四、为何使用 B 树或 B + 树作为索引结构

原因是 AVL 树(平衡二叉树)的高度比 B + 树的高度要高,而树的高度意味着磁盘 I/O 的数量。为了减少磁盘 I/O 的次数,文件系统或者数据库才会采用 B 树或者 B + 树。

五、B 树和 B + 树的特点总结

(一)B 树特点

  1. 在 B 树中每个节点都包含索引及数据,所以树的层高相对较高。
  2. 在进行数据查找时,由于数据存储在每个节点,在极端情况下要遍历整个树才能找到目标数据,所以查询的时间稳定性比较差。
  3. B 树的所有叶子节点都在同一层,意味着从根节点到每个叶子节点的距离相等,这样可以保证无论访问哪个叶子节点,时间复杂度都是 O (log n)。

(二)B + 树特点

  1. B + 树的非叶子节点只存储索引,所有数据存储在叶子节点上,这样可以更好地缩小树的层高,提高查询效率。
  2. B + 树的所有叶子节点通过一个双向指针连接,使得范围查找的效率更高。

六、学习建议

刷面试题虽能在短期内提升应试能力,但可能存在准备的问题没考,没准备的问题却被问到的情况。所以我们要深度梳理技术体系,再结合面试文档整体梳理,确保能通过面试。对于 Java 技术的系统化梳理,可以选择酷跑科技的架构涨薪班课程提升解决问题的能力。如果对课程内容有疑惑,可以在评论区留言咨询。希望大家通过对 B 树和 B + 树的学习,能更好地理解数据结构在实际应用中的重要性,提升自己的知识储备。

你可能感兴趣的:(java,数据结构)