【UCB CS 61B SP24】Lecture 17 - Data Structures 3: B-Trees学习笔记

本文以 2-3-4 树详细讲解了 B 树的概念,逐步分析其操作,并用 Java 实现了标准的 B 树。

1. 2-3 & 2-3-4 Trees

上一节课中讲到的二叉搜索树当数据是随机顺序插入的时候能够使得树变得比较茂密,如下图右侧所示,时间复杂度也就近似 O ( l o g n ) O(log n) O(logn)。但是当数据按顺序插入时,二叉搜索树就退化为了链表(每个节点都向同一侧倾斜),如下图左侧所示,这样时间复杂度就退化为 O ( n ) O(n) O(n)。有什么更优化的数据结构呢?

在这里插入图片描述

B 树(B-Trees)是一种自平衡的树数据结构,适用于在磁盘等存储设备上高效管理大量数据。它通过保持平衡来确保查找、插入、删除操作的时间复杂度为 O ( l o g n ) O(log n) O(logn)。B 树广泛应用于数据库和文件系统。

1.1 插入

假设我们现在有一颗还算完美的二叉搜索树,如下图右上角所示,接下来如果我们需要插入 {17, 18, 19, ...} 怎么办?我们可以通过在叶节点中“过度填充”来避免产生新的叶节点,也就是把插入的元素都塞到 16 节点中:

在这里插入图片描述

但是如果一个节点过于充斥,如下图所示,那么我们可能就得遍历节点中的所有元素才能找到我们想要的,这样效率同样会下降:

在这里插入图片描述

我们的解决方法是设定一个限制 L L L,表示一个节点中最多有几个键值,假设我们令 L = 3 L = 3 L=3,那么当节点中的键值已经到 {16, 17, 18, 19} 时就已经超过限制了,这时我们需要选择一个键值提升到父节点中

我们会将中间键(记为 node[mid])提升到父节点,在我们这个例子中键值数量为偶数,那么就选择中间偏左的键值 17,如下图所示:

在这里插入图片描述

仔细观察又会发现这样有个问题,那就是这时 16{15, 17} 的右侧了,这就不是个合法的搜索树了。因此再提完中间键后我们需要将中间键的左右两部分分裂开变成两个节点,假设 node 表示提升键值前的原节点 {16, 17, 18, 19},那么分裂操作就是将 node[0 ~ mid - 1]node[mid + 1, node.size - 1] 分裂开。

因此我们会将 16{18, 19} 分裂开,如下图所示,这样小于 15 的键值在左侧子节点(可以表示为 {15, 17}.children[0]),在 15 ~ 17 之间的键值在左侧第二个子节点(可以表示为 {15, 17}.children[1]),大于 17 的键值在右侧子节点(可以表示为 {15, 17}.children[2]):

在这里插入图片描述

假设我们继续插入 {20, 21},如下图所示,当插入 21 时,节点又爆满了,将中间靠左的键值 19 提升到父节点中,接着原节点分裂开:

在这里插入图片描述

我们继续插入 {25, 26},流程如下图所示,可以看到当非叶子节点分裂时,还需要同步处理子节点的引用,即分裂非叶子节点 node[0 ~ mid - 1]node[mid + 1, node.size - 1] 时,还需要顺带分裂 node.children[0, mid]node.children[mid + 1, node.size - 1](注意左半部分需要将 mid 包含进去才正确,可以结合图片理解):

在这里插入图片描述

如果我们一直添加到根节点都塞满了怎么办?那么就同样将根节点中的中间键往上提,这时候就成为了新的根节点,树的高度在这时候才加了一层,即树的高度只有在分裂根时才会增加,此时树还是保持着完美的平衡:

在这里插入图片描述

我们此前设定的限制 L = 3 L = 3 L=3 就最后就形成了这棵 2-3-4 树,当 L = 2 L = 2 L=2 时我们称其为 2-3 树,这两种就是相对最常见的 B 树。

1.2 删除

B 树的删除与 BST 一样是比较复杂的,有多种情况需要讨论。

(1)如果要删除的节点为内部节点(无论节点中有几个键值),那么思想与 BST 类似,找到前驱(左子树最大键)或后继(右子树最小键)替换要删除的键值,然后递归删除叶子节点中的键:

在这里插入图片描述

在这种情况中我们找到了 18,最后将其删去,这样看起来很简单,因为如果我们从具有多个键值的叶子节点中删除某个值只需要简单将其删去即可。

(2)如果我们的叶子节点只有一个键,我们就不能简单地完全删除节点,因为根据 B 树的性质(先见第二小节),每个拥有 k k k 个键值的节点(除叶子)都有 k + 1 k + 1 k+1 个子节点,因此我们将留下一个必须填充的空节点:

在这里插入图片描述

如何填充空节点是比较复杂的,同样也有多种情况要讨论:

Case 1:空节点的相邻兄弟节点有多个键值(非常难的情况),如下图所示,我们用哪个键来填充呢?

在这里插入图片描述

解决思路为:

  • X 先把父节点的键值拿过来,然后父节点再从 X 的兄弟节点中拿一个键值过来;
  • 如果 X 不是叶节点,再将其兄弟节点的一个子树拿过来(维持 B 树性质)。

在这里插入图片描述

结合例子看看,我们要删除 17,首先在右子树找到了后继键值 19,将其与 17 交换,然后删除 17,删除后留下了一个空节点,填充时从父节点拿来 21,父节点再从另一个兄弟节点拿来 22,由于空节点为叶节点,因此不进一步拿兄弟节点的子树:

在这里插入图片描述

Case 2:空节点右侧的所有兄弟节点都只有一个键值,但是父节点有多个键值(同样很困难),如下图所示:

在这里插入图片描述

解决思路为:

  • X 和最右侧的兄弟节点把父节点的键值拿来,中间子节点的键值提到父节点中;
  • 传递中间子节点的子树,以便每个节点都有正确的子节点

在这里插入图片描述

结合例子看看,我们要删除 3,首先在右子树中找到了后继键值 4,将其与 3 交换,然后删除 4,删除后留下了一个空节点,填充时右侧兄弟节点都只有一个键值,因此和最右边的兄弟节点 9 一起分别将父节点的键值拿来,然后将中间兄弟节点的键值提到父节点中,已经是叶节点了因此不用再调整子树了:

在这里插入图片描述

Case 3:父节点和所有兄弟节点都只有一个键值,这种简单点,解决思路就是将一个兄弟节点和父节点合并成一个节点,替换到 X 上,然后将空节点上移一层,如果空节点最终作为了根节点,那么直接删除空节点即可:

在这里插入图片描述

结合例子看看,我们要删除 6,首先在右子树中找到了后继键值 7,将其与 6 交换,然后删除 7,删除后留下了一个空节点,填充时右侧兄弟节点与父节点都只有一个键值,因此合并兄弟节点和父节点变为 {8, 9},然后将空节点上移一层,此时空节点并不是根节点,回到了第一种情况(兄弟节点有多个键值),也就是先把父节点键值 7 拿来,然后父节点从有多个键值的子节点那把 4 拿来,空节点不是叶节点,最后再把兄弟节点的子树 5 拿来当自己的子树:

在这里插入图片描述

2. 实现通用B树

通过上面演示的 B 树我们能发现其具有以下特性,我们此处以 m m m 阶 B 树为例进行概括:

  • 节点容量:
    • 根节点:至少有1个键,最多 m − 1 m - 1 m1 个键。
    • 内部节点:至少 ⌈ m / 2 ⌉ − 1 \lceil m / 2\rceil - 1 m/21个键,最多 m − 1 m - 1 m1 个键。
  • 子节点数量:每个拥有 k k k 个键值的节点(除叶子节点)都有 k + 1 k + 1 k+1 个子节点。
  • 平衡性:所有叶子节点位于同一层(相同深度),树是完全平衡的,无论怎么添加键值时间复杂度都为 O ( l o g n ) O(log n) O(logn)
  • 有序性:节点内的键按升序排列,子树遵循二叉搜索树性质。

总结一下 B 树的操作:

(1)查找

从根节点开始,逐层向下比较键值:

  • 若找到目标键,返回 true
  • 否则,根据键的大小选择对应的子节点递归查找。
  • 到达叶子节点仍未找到,返回 false

(2)插入

  1. 寻找插入位置:递归找到对应的叶子节点。
  2. 插入键:将键插入叶子节点。
  3. 分裂处理
    • 若节点键数超过 m − 1 m - 1 m1,则分裂:
      • 中间键提升到父节点;
      • 原节点分裂为两个子节点。
    • 递归检查父节点是否需要分裂,直到根节点。

(3)删除

  1. 定位键:找到待删除键的位置。
  2. 处理内部节点键:若键在内部节点,用前驱(左子树最大键)或后继(右子树最小键)替换,转为删除叶子节点中的键。
  3. 删除叶子键:直接删除。
  4. 处理下溢
    • 借键:若兄弟节点有富余键,从兄弟借一个键并调整父节点;
    • 合并:若兄弟节点无富余,合并当前节点与兄弟,并递归调整父节点。

Java 实现 m m m 阶 B 树代码如下,可以简单参考一下,不一定要完全看明白:

package CS61B.Lecture17;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 标准 m 阶 B 树实现(支持插入、查找、删除)* 特性:* 1. 每个节点最多包含 m - 1 个键* 2. 根节点最少包含 1 个键,非根节点最少包含 ⌈m / 2⌉ - 1 个键* 3. 所有叶子节点位于同一层*/
public class BTree {private final int m;  // B 树的阶private Node root;public BTree(int m) {this.m = m;this.root = new Node(true);}/** 节点 */private static class Node {List<Integer> keys = new ArrayList<>();  // 存储键值(始终保持有序)List<Node> children = new ArrayList<>();  // 子节点引用(非叶子节点使用)boolean isLeaf;  // 是否为叶子节点Node(boolean isLeaf) {this.isLeaf = isLeaf;}}/** 核心操作:查找 */public boolean contains(int key) {return search(root, key) != null;}/** 递归查找实现 */private Integer search(Node node, int key) {// 找到当前节点中第一个不小于 key 的键的位置int i = 0;while (i < node.keys.size() && key > node.keys.get(i)) i++;if (i < node.keys.size() && key == node.keys.get(i)) {  // 在当前节点找到目标键return key;} else if (node.isLeaf) {return null;} else {  // 未找到但当前节点非叶子节点return search(node.children.get(i), key);  // 递归查找子节点}}/** 核心操作:插入 */public void insert(int key) {insert(root, key);// 根节点分裂处理if (root.keys.size() == m) {Node newRoot = new Node(false);newRoot.children.add(root);splitChild(newRoot, 0);root = newRoot;}}/** 递归插入实现 */private void insert(Node node, int key) {int i = node.keys.size() - 1;if (node.isLeaf) {// 叶子节点:直接插入while (i >= 0 && key < node.keys.get(i)) i--;  // 找到小于等于 key 的最大值位置node.keys.add(i + 1, key);  // 在其右侧插入 key} else {// 内部节点:找到子节点位置while (i >= 0 && key < node.keys.get(i)) i--;i++;  // 调整到正确的子节点索引,因为 node[i] 小于等于 key,node.children[i] 是小于 node[i] 的子树// 子节点已满时先分裂if (node.children.get(i).keys.size() == m - 1) {splitChild(node, i);if (key > node.keys.get(i)) i++;  // 分裂后可能需要调整目标子节点索引}insert(node.children.get(i), key);}}/** 分裂子节点(核心辅助方法) */private void splitChild(Node parent, int childIndex) {Node child = parent.children.get(childIndex);Node sibling = new Node(child.isLeaf);  // 与原节点在同一层int mid = m - 1 >> 1;  // 中间键索引// 将右半部分键移动到新节点sibling.keys.addAll(child.keys.subList(mid + 1, child.keys.size()));child.keys.subList(mid + 1, child.keys.size()).clear();  // 清除原节点右半部分// 非叶子节点:处理子节点引用if (!child.isLeaf) {sibling.children.addAll(child.children.subList(mid + 1, child.children.size()));child.children.subList(mid + 1, child.children.size()).clear();}// 将中间键提升到父节点,新节点在原节点的右边parent.keys.add(childIndex, child.keys.remove(mid));parent.children.add(childIndex + 1, sibling);}/** 核心操作:删除 */public void delete(int key) {delete(root, key);/*根节点为空时降低树高度,选择其第一个子节点作为新的根节点当根节点被删除到空时,唯一可能的场景是:根节点原本只有一个键,且该键被删除根节点此时仅剩一个子节点(因为如果根节点有多个子节点,它必须至少保留一个键来分隔子节点)*/if (root.keys.isEmpty() && !root.isLeaf) {root = root.children.get(0);}}/** 递归删除实现 */private void delete(Node node, int key) {int i = 0;while (i < node.keys.size() && key > node.keys.get(i)) i++;  // 找到大于等于 key 的最小值// Case 1: 当前节点包含目标键if (i < node.keys.size() && key == node.keys.get(i)) {if (node.isLeaf) {  // 如果为叶子节点的键则直接删除node.keys.remove(i);} else {  // 如果是内部节点则用前驱/后继替换后递归删除handleInternalKey(node, i);}}// Case 2: 目标键可能在子节点中else if (!node.isLeaf) {Node child = node.children.get(i);// 子节点键不足时先调整if (child.keys.size() < (m + 1) / 2) {// 尝试从左兄弟借键if (i > 0 && node.children.get(i - 1).keys.size() >= (m + 1) / 2) {borrowFromLeftSibling(node, i);}// 尝试从右兄弟借键else if (i < node.children.size() - 1 && node.children.get(i + 1).keys.size() >= (m + 1) / 2) {borrowFromRightSibling(node, i);}// 需要合并节点else {if (i < node.children.size() - 1) {mergeChildren(node, i);} else {mergeChildren(node, i - 1);i--;  // 合并后索引调整}}}delete(node.children.get(i), key);}}/** 处理内部节点键的删除,选择前驱后继时需要注意非根节点最少包含 ⌈m / 2⌉ - 1 个键的性质 */private void handleInternalKey(Node node, int index) {Node leftChild = node.children.get(index);Node rightChild = node.children.get(index + 1);// Case 1: 左子节点的键足够多,用前驱替换if (leftChild.keys.size() >= (m + 1) / 2) {int predecessor = getPredecessor(leftChild);node.keys.set(index, predecessor);delete(leftChild, predecessor);}// Case 2: 右子节点的键足够多,用后继替换else if (rightChild.keys.size() >= (m + 1) / 2) {int successor = getSuccessor(rightChild);node.keys.set(index, successor);delete(rightChild, successor);}// Case 3: 否则合并 leftChild 与 rightChild 两个子节点后递归删除else {int keyToDelete = node.keys.get(index);  // 合并后 node.keys.get(index) 可能已变更,需要提前保存mergeChildren(node, index);delete(leftChild, keyToDelete);  // 删除已下移到子节点的原键}}/** 获取左子树的最大键(前驱) */private int getPredecessor(Node node) {while (!node.isLeaf) {node = node.children.get(node.children.size() - 1);}return node.keys.get(node.keys.size() - 1);}/** 获取右子树的最小键(后继) */private int getSuccessor(Node node) {while (!node.isLeaf) {node = node.children.get(0);}return node.keys.get(0);}/** 从左兄弟借键 */private void borrowFromLeftSibling(Node parent, int childIndex) {Node child = parent.children.get(childIndex);Node leftSibling = parent.children.get(childIndex - 1);// 父节点键下移,左兄弟键上移child.keys.add(0, parent.keys.get(childIndex - 1));parent.keys.set(childIndex - 1, leftSibling.keys.remove(leftSibling.keys.size() - 1));// 移动子节点引用(非叶子节点)if (!child.isLeaf) {child.children.add(0, leftSibling.children.remove(leftSibling.children.size() - 1));}}/** 从右兄弟借键 */private void borrowFromRightSibling(Node parent, int childIndex) {Node child = parent.children.get(childIndex);Node rightSibling = parent.children.get(childIndex + 1);// 父节点键下移,右兄弟键上移child.keys.add(parent.keys.get(childIndex));parent.keys.set(childIndex, rightSibling.keys.remove(0));// 移动子节点引用(非叶子节点)if (!child.isLeaf) {child.children.add(rightSibling.children.remove(0));}}/** 合并 childIndex 与 childIndex + 1 两个位置的子节点 */private void mergeChildren(Node parent, int childIndex) {Node left = parent.children.get(childIndex);Node right = parent.children.get(childIndex + 1);// 提取父节点的键并下移int parentKey = parent.keys.get(childIndex);left.keys.add(parentKey);parent.keys.remove(childIndex);// 合并右子节点的键和子节点left.keys.addAll(right.keys);left.children.addAll(right.children);parent.children.remove(childIndex + 1);// 若父节点是根且无键,降低树高度if (parent == root && parent.keys.isEmpty()) {root = left;}}/** 打印 B 树结构 */public void printTree() {printTree(root, 0);}/** 递归打印 B 树结构 */private void printTree(Node node, int level) {StringBuilder indent = new StringBuilder();for (int i = 0; i < level; i++) {indent.append("│   "); // 每层缩进 4 个字符}// 打印当前节点键值System.out.print(indent);if (level > 0) {System.out.print("├── ");}System.out.print("[" + String.join(", ", node.keys.stream().map(Object::toString).toArray(String[]::new)) + "]");if (node.isLeaf) {System.out.print(" (Leaf)");}System.out.println();// 递归打印子节点for (int i = 0; i < node.children.size(); i++) {Node child = node.children.get(i);printTree(child, level + 1);}}/** 递归打印 B 树结构(添加箭头符号的增强版) */private void printTreeEnhancement(Node node, int level) {// 生成缩进前缀StringBuilder prefix = new StringBuilder();for (int i = 0; i < level; i++) {prefix.append(i == level - 1 ? "│   " : "    ");}// 打印当前节点System.out.print(prefix);if (level > 0) {System.out.print("└── ");}System.out.print("[" + String.join(", ", Arrays.toString(node.keys.stream().map(Object::toString).toArray(String[]::new)) + "]"));if (node.isLeaf) System.out.print(" (Leaf)");System.out.println();// 递归子节点for (int i = 0; i < node.children.size(); i++) {Node child = node.children.get(i);printTree(child, level + 1);}}/** 测试 */public static void main(String[] args) {BTree tree = new BTree(4);// 插入测试数据int[] keys = {10, 20, 30, 40, 50, 60, 70, 80, 90};for (int key : keys) tree.insert(key);// 验证存在性System.out.println("Contains 30: " + tree.contains(30));  // trueSystem.out.println("Contains 10: " + tree.contains(10));  // truetree.printTree();// 删除内部节点键tree.delete(30);System.out.println("Contains 30 after deletion: " + tree.contains(30));  // falsetree.printTree();// 边界测试:删除后树结构调整tree.delete(10);tree.delete(20);tree.delete(40);System.out.println("Contains 50: " + tree.contains(50));  // trueSystem.out.println("Contains 10 after deletion: " + tree.contains(10));  // false}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/71098.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【手撕算法】支持向量机(SVM)从入门到实战:数学推导与核技巧揭秘

摘要 支持向量机&#xff08;SVM&#xff09;是机器学习中的经典算法&#xff01;本文将深入解析最大间隔分类原理&#xff0c;手撕对偶问题推导过程&#xff0c;并实战实现非线性分类与图像识别。文中附《统计学习公式手册》及SVM调参指南&#xff0c;助力你掌握这一核心算法…

西门子S7-1200比较指令

西门子S7-1200 PLC比较指令学习笔记 一、比较指令的作用 核心功能&#xff1a;用于比较两个数值的大小或相等性&#xff0c;结果为布尔值&#xff08;True/False&#xff09;。典型应用&#xff1a; 触发条件控制&#xff08;如温度超过阈值启动报警&#xff09;数据筛选&…

SDF,占用场,辐射场简要笔记

符号距离函数&#xff08;Signed Distance Function&#xff0c;SDF&#xff09;的数学公式用于描述空间中任意点到某个几何形状边界的最短距离&#xff0c;并通过符号区分点在边界内外。具体定义如下&#xff1a; 假设 Ω \Omega Ω 是一个几何形状的边界&#xff0c;对于空…

solidwork智能尺寸怎么对称尺寸

以构造轴为中心线就能画智能尺寸的对称尺寸。先点击边再点击构造线

如何从零开始理解LLM训练理论?预训练范式、模型推理与扩容技巧全解析

Part 1&#xff1a;预训练——AI的九年义务教育 &#x1f4da; 想象你往峨眉山猴子面前扔了1000本《五年高考三年模拟》-我那时候还在做的题&#xff08;海量互联网数据&#xff09;&#xff0c;突然有一天它开口唱起《我在东北玩泥巴》&#xff0c;这有意思的过程就是LLM的预…

工程化与框架系列(13)--虚拟DOM实现

虚拟DOM实现 &#x1f333; 虚拟DOM&#xff08;Virtual DOM&#xff09;是现代前端框架的核心技术之一&#xff0c;它通过在内存中维护UI的虚拟表示来提高渲染性能。本文将深入探讨虚拟DOM的实现原理和关键技术。 虚拟DOM概述 &#x1f31f; &#x1f4a1; 小知识&#xff1…

设计模式--spring中用到的设计模式

一、单例模式&#xff08;Singleton Pattern&#xff09; 定义&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点 Spring中的应用&#xff1a;Spring默认将Bean配置为单例模式 案例&#xff1a; Component public class MySingletonBean {// Spring 默认将其…

深入浅出:Spring AI 集成 DeepSeek 构建智能应用

Spring AI 作为 Java 生态中备受瞩目的 AI 应用开发框架&#xff0c;凭借其简洁的 API 设计和强大的功能&#xff0c;为开发者提供了构建智能应用的强大工具。与此同时&#xff0c;DeepSeek 作为领先的 AI 模型服务提供商&#xff0c;在自然语言处理、计算机视觉等领域展现了卓…

CSS浮动详解

1. 浮动的简介 浮动是用来实现文字环绕图片效果的 2.元素浮动后会有哪些影响 对兄弟元素的影响&#xff1a; 后面的兄弟元素&#xff0c;会占据浮动元素之前的位置&#xff0c;在浮动元素的下面&#xff1b;对前面的兄弟 无影响。 对父元素的影响&#xff1a; 不能撑起父元…

python数据类型等基础语法

目录 字面量 注释 变量 查数据类型 类型转换 算数运算符 字符串定义的三种方式 字符串占位 数据输入 字面量 被写在代码中固定的值 六种数据类型: 1 字符串 String 如"egg" 2 数字 Number: 整数int 浮点数float 复数complex :如43j 布尔…

Android 图片压缩详解

在 Android 开发中,图片压缩是一个重要的优化手段,旨在提升用户体验、减少网络传输量以及降低存储空间占用。以下是几种主流的图片压缩方法,结合原理、使用场景和优缺点进行详细解析。 效果演示 直接先给大家对比几种图片压缩的效果 质量压缩 质量压缩:根据传递进去的质…

Flutter状态管理框架GetX最新版详解与实践指南

一、GetX框架概述 GetX是Flutter生态中轻量级、高性能的全能开发框架&#xff0c;集成了状态管理、路由导航、依赖注入等核心功能&#xff0c;同时提供国际化、主题切换等实用工具。其优势在于代码简洁性&#xff08;减少模板代码约70%&#xff09;和高性能&#xff08;基于观…

【linux】详谈 环境变量

目录 一、基本概念 二、常见的环境变量 取消环境变量 三、获取环境变量 通过代码获取环境变量 环境变量的特性 1. getenv函数:获取指定的环境变量 2. environ获取环境变量 四、本地变量 五、定义环境变量的方法 临时定义&#xff08;仅对当前会话有效&#xff09; 永…

LangChain教程 - RAG - PDF问答

系列文章索引 LangChain教程 - 系列文章 在现代自然语言处理&#xff08;NLP&#xff09;中&#xff0c;基于文档内容的问答系统变得愈发重要&#xff0c;尤其是当我们需要从大量文档中提取信息时。通过结合文档检索和生成模型&#xff08;如RAG&#xff0c;Retrieval-Augment…

大白话前端性能优化方法的分类与具体实现

大白话前端性能优化方法的分类与具体实现 一、资源加载优化 1. 压缩与合并文件 大白话解释&#xff1a; 咱们的网页代码里&#xff0c;就像一个房间堆满了东西&#xff0c;有很多没用的“杂物”&#xff0c;比如代码里的空格、注释啥的。压缩文件就是把这些“杂物”清理掉&a…

MySQL并发知识(面试高频)

mysql并发事务解决 不同隔离级别下&#xff0c;mysql解决并发事务的方式不同。主要由锁机制和MVCC(多版本并发控制)机制来解决并发事务问题。 1. mysql中的锁有哪些&#xff1f; 表级锁&#xff1a; 场景&#xff1a;表级锁适用于需要对整个表进行操作的情况&#xff0c;例如…

【Kubernets】K8S内部nginx访问Service资源原理说明

文章目录 原理概述**一、核心概念****二、Nginx 访问 Service 的流程****1. Service 的作用****2. Endpoint 的作用****3. Nginx Pod 发起请求****(1) DNS 解析****(2) 流量到达 kube-proxy****(3) 后端 Pod 处理请求** **三、不同代理模式的工作原理****1. iptables 模式****2…

HTML:自闭合标签简单介绍

1. 什么是自结束标签&#xff1f; 定义&#xff1a;自结束标签&#xff08;Self-closing Tag&#xff09;是指 不需要单独结束标签 的 HTML 标签&#xff0c;它们通过自身的语法结构闭合。语法形式&#xff1a; 在 HTML5 中&#xff1a;直接写作 <tag>&#xff0c;例如 …

《几何原本》公理

《几何原本》公理 等于同量的量彼此相等 即若 a b , b c ab,bc ab,bc 则 a c ac ac 等量加等量&#xff0c;其和仍相等 即若 a b ab ab 则 a c b c acbc acbc 等量减等量&#xff0c;其差仍相等 即若 a b ab ab 则 a − c b − c a-cb-c a−cb−c 彼此能够&a…

学习路程十一 langchain核心组件 Memory

前序 在最开始我们就通过实验知道LLM 本身是没有记忆的&#xff0c;每一次LLM的API调用都是一个全新的会话。但在某些应用程序中&#xff0c;如&#xff1a;聊天机器人&#xff0c;让LLM记住以前的历史交互是非常重要&#xff0c;无论是在短期的还是长期的。langchain中的“Me…