STM32输入捕获除了测频率,还能怎么玩?一个实例讲透PWM输入模式
2026/5/6 16:22:44
为何二叉树不适合外存数据处理?二叉树的树高随数据量增长呈O(log₂n)趋势(如 100 万条数据,树高约 20),而外存操作的核心成本是磁盘 IO(每次 IO 仅能读取一个节点),二叉树的高树高会导致大量 IO 次数,性能急剧下降。
| 树结构 | 核心特性 | 适用场景 |
|---|---|---|
| 二叉搜索树 | 有序,无平衡保证 | 少量内存数据、单次查询 |
| AVL 树 | 绝对平衡,旋转次数多 | 少量内存数据、频繁查询 |
| 红黑树 | 相对平衡,旋转次数少 | 大量内存数据(如 Java TreeMap) |
| B 树 | 多叉平衡,树高低 | 外存数据处理(如数据库索引) |
如何量化 B 树的 IO 次数优势?以m 阶 B 树和二叉搜索树为例,假设存储 n=100 万条数据:
为何非根节点的关键字数下限是⌈m/2⌉−1?该下限是为了保证 B 树的平衡特性和最坏情况下的查找效率:
以 **m=3 阶 B 树(2-3 树)和m=4 阶 B 树(2-3-4 树)** 为例,直观理解性质:
以m=3 阶 B 树为例,演示插入序列1,3,5,7,9的完整过程:
usedSize。usedSize == m,触发分裂逻辑。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(); } }BTreeNode:keys和children数组的大小与 m 阶强关联(keys为 m-1,children为 m),是多叉树的核心设计,需严格匹配。search:通过循环遍历节点内关键字,定位子节点或目标关键字,返回的SearchResult是插入逻辑的关键前提。split:mid = MAX_KEY / 2)是拆分节点的核心;insert:叶子节点的关键字移位插入是保持有序性的关键,插入后的分裂检查是维持 B 树性质的核心步骤。