实现B-树

一、概述

1.历史

B树(B-Tree)结构是一种高效存储和查询数据的方法,它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database Systems》中的,题目为"Organization and Maintenance of Large Ordered Indexes"。

这篇论文提出了一种能够高效地维护大型有序索引的方法,这种方法的主要思想是将每个节点扩展成多个子节点,以减少查找所需的次数。B树结构非常适合应用于磁盘等大型存储器的高效操作,被广泛应用于关系数据库和文件系统中。

B树结构有很多变种和升级版,例如B+树,B*树和SB树等。这些变种和升级版本都基于B树的核心思想,通过调整B树的参数和结构,提高了B树在不同场景下的性能表现。

总的来说,B树结构是一个非常重要的数据结构,为高效存储和查询大量数据提供了可靠的方法。它的历史可以追溯到上个世纪70年代,而且在今天仍然被广泛应用于各种场景。

2.B-树的优势

B树和AVL树、红黑树相比,B树更适合磁盘的增删改查,而AVL和红黑树更适合内存的增删改查。

假设存储100万的数据:

  • 使用AVL来存储,树高为: l o g 2 1000000 ≈ 20 log_21000000≈20 log2100000020 (20次的磁盘IO很慢,但是20次的内存操作很快)
  • 使用B-树存储,最小度数为500,树高为:3

B树优势:

  • 磁盘存储比内存存储慢很多,尤其是访问磁盘的延迟相对较高。每次访问磁盘都需要消耗更多的时间,而B树的设计可以最大化地减少对磁盘的访问次数。
  • 磁盘访问一般是按块读取的,而B树的节点通常设计为与磁盘块大小一致。由于B树是多路的,单次磁盘访问通常会加载多个数据项,而不是像AVL树和红黑树那样每次只读取一个节点。
  • 在磁盘中存储B树时,操作系统通常会将树的部分结构加载到内存中以便快速查询,避免了频繁的磁盘访问。
  • 在数据库和文件系统中,数据通常是大规模的,存储在外部存储介质上。B树特别适合大规模数据的增删改查,因为它减少了不必要的磁盘访问,能够高效地执行复杂的数据操作。

二、特性

1.度和阶

  • 度(degree):节点的孩子数
  • 阶(order):所有节点孩子最大值

2.特性

  • 每个节点具有

    • 属性 n,表示节点中 key 的个数
    • 属性 leaf,表示节点是否是叶子节点
    • 节点 key 可以有多个,以升序存储
  • 每个非叶子节点中的孩子数是 n + 1、叶子节点没有孩子

  • 最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:

最小度数t键数量范围
21 ~ 3
32 ~ 5
43 ~ 7
n(n-1) ~ (2n-1)

其中,当节点中键数量达到其最大值时,即 3、5、7 … 2n-1,需要分裂

  • 叶子节点的深度都相同

三、实现

1.定义节点类

static class Node {// 关键字int[] keys;// 关键字数量int keyNum;// 孩子节点Node[] children;// 是否是叶子节点boolean leafFlag = true;// 最小度数:最少孩子数(决定树的高度,度数越大,高度越小)int t;// ≥2public Node(int t) {this.t = t;// 最多的孩子数(约定)this.children = new Node[2 * t];this.keys = new int[2 * t -1];}
}
1.1 节点类相关方法

查找key:查找目标22,在当前节点的关键字数组中依次查找,找到了返回;没找到则从孩子节点找:

  • 当前节点是叶子节点:目标不存在
  • 非叶子结点:当key循环到25,大于目标22,此时从索引4对应的孩子key数组中继续查找,依次递归,直到找到为止。
    在这里插入图片描述

根据key获取节点

/*** 根据key获取节点* @param key* @return*/
Node get(int key) {// 先从当前key数组中找int i = 0;while (i < keyNum) {if (keys[i] == key) {// 在当前的keys关键字数组中找到了return this;}if (keys[i] > key) {// 当数组比当前key大还未找到时,退出循环break;}i++;}// 如果是叶子节点,没有孩子了,说明key不存在if (leafFlag) {return null;} else {// 非叶子节点,退出时i的值就是对应范围的孩子节点数组的索引,从对应的这个孩子数组中继续找return children[i].get(key);}
}

向指定索引插入key

/*** 向keys数组中指定的索引位置插入key* @param key* @param index*/
void insertKey(int key,int index) {/*** [0,1,2,3]* src:源数组* srcPos:起始索引* dest:目标数组* destPos: 目标索引* length:拷贝的长度*/System.arraycopy(keys, index, keys, index + 1, keyNum - index);keys[index] = key;keyNum++;
}

向指定索引插入child

/*** 向children指定索引插入child** @param child* @param index*/
void insertChild(Node child, int index) {System.arraycopy(children, index, children, index + 1, keyNum - index);children[index] = child;
}

2.定义树

public class BTree {// 根节点private Node root;// 树中节点最小度数int t;// 最小key数量 在创建树的时候就指定好final int MIN_KEY_NUM;// 最大key数量final int MAX_KEY_NUM;public BTree() {// 默认度数设置为2this(2);}public BTree(int t) {this.t = t;root = new Node(t);MIN_KEY_NUM = t - 1;MAX_KEY_NUM = 2 * t - 1;}
}    

判断key在树中是否存在

/*** 判断key在树中是否存在* @param key* @return*/
public boolean contains(int key) {return root.get(key) != null;
}

3.新增key:

  • 1.查找插入位置:从根节点开始,沿着树向下查找,直到找到一个叶子节点,这个叶子节点包含的键值范围覆盖了要插入的键值。
  • 2.插入键值:在找到的叶子节点中插入新的键值。如果叶子节点中的键值数量没有超过B树的阶数(即每个节点最多可以包含的键值数量),则插入操作完成。
  • 3.分裂节点:如果叶子节点中的键值数量超过了B树的阶数,那么这个节点需要分裂。

如果度为3,最大key数量为:2*3-1=5,当插入了8后,此时达到了最大数量5,需要分裂:
叶子节点分裂

分裂逻辑:
分裂节点数据一分为三:

  • 左侧数据:本身左侧的数据留在该节点
  • 中间数据:中间索引2(度-1)的数据6移动到父节点的索引1(被分裂节点的索引)处
  • 右侧数据:从索引3(度)开始的数据,移动到新节点,新节点的索引值为分裂节点的index+1

如果分裂的节点是非叶子节点:
需要多一步操作:右侧数据需要和孩子一起连带到新节点去:
非叶子节点分裂
分裂的是根节点:
需要再创建多一个节点来当做根节点,此根节点为父亲,存入中间的数据。
其他步骤同上。
根节点分裂
分裂方法:

/*** 节点分裂* 左侧数据:本身左侧的数据留在该节点* 中间数据:中间索引2(度-1)的数据6移动到父节点的索引1(被分裂节点的索引)处* 右侧数据:从索引3(度)开始的数据,移动到新节点,新节点的索引值为分裂节点的index+1* @param node 要分裂的节点* @param index 分裂节点的索引* @param parent 要分裂节点的父节点**/
public void split(Node node, int index, Node parent) {// 没有父节点,当前node为根节点if (parent == null) {// 创建出新的根来存储中间数据Node newRoot = new Node(t);newRoot.leafFlag = false;newRoot.insertChild(node, 0);// 更新根节点为新创建的newRootthis.root = newRoot;parent = newRoot;}// 1.处理右侧数据:创建新节点存储右侧数据Node newNode = new Node(t);// 新创建的节点跟原本分裂节点同级newNode.leafFlag = node.leafFlag;// 新创建节点的数据从 原本节点【度】位置索引开始拷贝 拷贝长度:t-1System.arraycopy(node.keys, t, newNode.keys, 0, t - 1);// 如果node不是叶子节点,还需要把node的一部分孩子也同时拷贝到新节点的孩子中if (!node.leafFlag) {System.arraycopy(node.children, t, newNode.children, 0, t);}// 更新新节点的keyNumnewNode.keyNum = t - 1;// 更新原本节点的keyNumnode.keyNum = t - 1;// 2.处理中间数据:【度-1】索引处的数据 移动到父节点【分裂节点的索引】索引处// 要插入父节点的数据:int midKey = node.keys[t - 1];parent.insertKey(midKey, index);// 3. 新创建的节点作为父亲的孩子parent.insertChild(newNode, index + 1);// parent的keyNum在对应的方法中已经更新了
}

新增key:

/*** 新增key** @param key*/
public void put(int key) {doPut(root, key, 0, null);
}/*** 执行新增key* 1.查找插入位置:从根节点开始,沿着树向下查找,直到找到一个叶子节点,这个叶子节点包含的键值范围覆盖了要插入的键值。* 2.插入键值:在找到的叶子节点中插入新的键值。如果叶子节点中的键值数量没有超过B树的阶数(即每个节点最多可以包含的键值数量),则插入操作完成。* 3.分裂节点:如果叶子节点中的键值数量超过了B树的阶数,那么这个节点需要分裂。* @param node 待插入元素的节点* @param key 插入的key* @param nodeIndex  待插入元素节点的索引* @param nodeParent 待插入节点的父节点*/
public void doPut(Node node, int key, int nodeIndex, Node nodeParent) {// 查找插入位置int index = 0;while (index < node.keyNum) {if (node.keys[index] == key ) {// 找到了 做更新操作 (因为没有维护value,所以就不用处理了)return;}if (node.keys[index] > key) {// 没找到该key, 退出循环,index的值就是要插入的位置break;}index++;}// 如果是叶子节点,直接插入if (node.leafFlag) {node.insertKey(key, index);} else {// 非叶子节点,继续从孩子中找到插入位置 父亲的这个待插入的index正好就是元素要插入的第x个孩子的位置doPut(node.children[index], key , index, node);}// 处理节点分裂逻辑 : keyNum数量达到上限,节点分裂if (node.keyNum == MAX_KEY_NUM) {split(node, nodeIndex, nodeParent);}
}

4.删除key

情况一:删除的是叶子节点的key

节点是叶子节点,找到了直接删除,没找到返回。

情况二:删除的是非叶子节点的key

没有找到key,继续在孩子中找。
找到了,把要删除的key和替换为后继key,删掉后继key。

平衡树:该key被删除后,key数目<key下限(t-1),树不平衡,需要调整
  • 如果左边兄弟节点的key是富裕的,可以直接找他借:右旋,把父亲一个节点的旋转下来(在父亲中找到失衡节点的前驱节点),把兄弟的一个节点旋转上去(旋转上去的是兄弟中最大的key)。
    在这里插入图片描述
  • 如果右边兄弟节点的key是富裕的,可以直接找他借:左旋,把父亲的旋转下来,把兄弟的旋转上去。在这里插入图片描述
  • 当没有兄弟是富裕时,没办法借,采用向左合并:父亲和失衡节点都合并到左侧的节点中。
    在这里插入图片描述

右旋详细流程
旋转
处理孩子:
处理孩子

向左合并详细流程
在这里插入图片描述
根节点调整的情况
在这里插入图片描述

调整平衡代码:

/*** 树的平衡* @param node 失衡节点* @param index 失衡节点索引* @param parent 失衡节点父节点*/
public void balance(Node node, int index, Node parent) {if (node == root) {// 如果是根节点 当调整到根节点只剩下一个key时,要替换根节点 (根节点不能为null,要保证右孩子才替换)if (root.keyNum == 0 && root.children[0] != null) {root = root.children[0];}return;}// 拿到该节点的左右兄弟,判断节点是不是富裕的,如果富裕,则找兄弟借Node leftBrother = parent.childLeftBrother(index);Node rightBrother = parent.childRightBrother(index);// 左边的兄弟富裕:右旋if (leftBrother != null && leftBrother.keyNum > MIN_KEY_NUM) {// 1.要旋转下来的key:父节点中【失衡节点索引-1】的key:parent.keys[index-1];插入到失衡节点索引0位置// (这里父亲节点旋转走的不用删除,因为等会左侧的兄弟旋转上来会覆盖掉)node.insertKey(parent.keys[index - 1], 0);// 2.0 如果左侧节点不是叶子节点,有孩子,当旋转一个时,只需要留下原本孩子数-1 ,把最大的孩子过继给失衡节点的最小索引处(先处理后事)if (!leftBrother.leafFlag) {node.insertChild(leftBrother.removeRightMostChild(), 0);}// 2.1 要旋转上去的key:左侧兄弟最大的索引key,删除掉,插入到父节点中【失衡节点索引-1】位置(此位置就是刚才在父节点旋转走的key的位置)// 这里要直接覆盖,不能调插入方法,因为这个是当初旋转下去的key。parent.keys[index - 1] = leftBrother.removeRightMostKey();return;}// 右边的兄弟富裕:左旋if (rightBrother != null && rightBrother.keyNum > MIN_KEY_NUM) {// 1.要旋转下来的key:父节点中【失衡节点索引】的key:parent.keys[index];插入到失衡节点索引最大位置keyNum位置// (这里父亲节点旋转走的不用删除,因为等会右侧的兄弟旋转上来会覆盖掉)node.insertKey(parent.keys[index], node.keyNum);// 2.0 如果右侧节点不是叶子节点,有孩子,当旋转一个时,只需要留下原本孩子数-1 ,把最小的孩子过继给失衡节点的最大索引处(孩子节点的索引比父亲要多1)if (!rightBrother.leafFlag) {node.insertChild(rightBrother.removeLeftMostChild(), node.keyNum + 1);}// 2.1 要旋转上去的key:右侧兄弟最小的索引key,删除掉,插入到父节点中【失衡节点索引-1】位置(此位置就是刚才在父节点旋转走的key的位置)// 这里要直接覆盖,不能调插入方法,因为这个是当初旋转下去的key。parent.keys[index] = rightBrother.removeLeftMostKey();return;}// 左右兄弟都不够,往左合并if (leftBrother != null) {// 向左兄弟合并// 1.把失衡节点从父亲中移除parent.removeChild(index);// 2.插入父节点的key到左兄弟 将父节点中【失衡节点索引-1】的key移动到左侧leftBrother.insertKey(parent.removeKey(index - 1), leftBrother.keyNum);// 3.插入失衡节点的key及其孩子到左兄弟node.moveToTarget(leftBrother);} else {// 右兄弟向自己合并// 1.把右兄弟从父亲中移除parent.removeChild(index + 1);// 2.把父亲的【失衡节点索引】 处的key移动到自己这里node.insertKey(parent.removeKey(index), node.keyNum);// 3.把右兄弟完整移动到自己这里rightBrother.moveToTarget(node);}
}

删除key:

/*** 删除指定key* @param node 查找待删除key的起点* @param parent 待删除key的父亲* @param nodeIndex 待删除的key的索引* @param key 待删除的key*/
public void doRemove(Node node, Node parent, int nodeIndex, int key) {// 找到被删除的keyint index = 0;// 循环查找待删除的keywhile (index < node.keyNum) {if (node.keys[index] >= key) {//找到了或者没找到break;}index++;}// 如果找到了 index就是要删除的key索引;// 如果没找到,index就是要在children的index索引位置继续找// 一、是叶子节点if (node.leafFlag) {// 1.1 没找到if (!found(node, key, index)) {return;}// 1.2 找到了else {// 删除当前节点index处的keynode.removeKey(index);}}// 二、不是叶子节点else {// 1.1 没找到if (!found(node, key, index)) {// 继续在孩子中找 查找的孩子的索引就是当前indexdoRemove(node.children[index], node, index, key);}// 1.2 找到了else {// 找到后继节点,把后继节点复制给当前的key,然后删除后继节点。// 在索引+1的孩子里开始,一直往左找,直到节点是叶子节点为止,就找到了后继节点Node deletedSuccessor = node.children[index + 1];while (!deletedSuccessor.leafFlag) {// 更新为最左侧的孩子deletedSuccessor = deletedSuccessor.children[0];}// 1.2.1 当找到叶子节点之后,最左侧的key就是后继keyint deletedSuccessorKey = deletedSuccessor.keys[0];// 1.2.2 把后继key赋值给待删除的keynode.keys[index] = deletedSuccessorKey;// 1.2.3 删除后继key 再调用该方法,走到情况一,删除掉该后继key: 起点为索引+1的孩子处,删除掉后继keydoRemove(node.children[index + 1], node, index + 1, deletedSuccessorKey);}}// 树的平衡:if (node.keyNum < MIN_KEY_NUM) {balance(node, nodeIndex, parent);}
}

节点相关方法:

        /*** 移除指定索引处的key* @param index* @return*/int removeKey(int index) {int deleted = keys[index];System.arraycopy(keys, index + 1, keys, index, --keyNum - index);return deleted;}/*** 移除最左索引处的key* @return*/int removeLeftMostKey(){return removeKey(0);}/*** 移除最右边索引处的key* @return*/int removeRightMostKey() {return removeKey(keyNum - 1);}/*** 移除指定索引处的child* @param index* @return*/Node removeChild(int index) {Node deleted = children[index];System.arraycopy(children, index + 1, children, index, keyNum - index);children[keyNum] = null;return deleted;}/*** 移除最左边的child* @return*/Node removeLeftMostChild() {return removeChild(0);}/*** 移除最右边的child* @return*/Node removeRightMostChild() {return removeChild(keyNum);}/*** 获取指定children处左边的兄弟* @param index* @return*/Node childLeftBrother(int index) {return index > 0 ? children[index - 1] : null;}/*** 获取指定children处右边的兄弟* @param index* @return*/Node childRightBrother(int index) {return index == keyNum ? null : children[index + 1];}/*** 复制当前节点到目标节点(key和child)* @param target*/void moveToTarget(Node target) {int start = target.keyNum;// 当前节点不是叶子节点 说明有孩子if (!leafFlag) {// 复制当前节点的孩子到目标节点的孩子中for (int i = 0; i <= keyNum; i++) {target.children[start + i] = children[i];}}// 复制key到目标节点的keys中for (int i = 0; i < keyNum; i++) {target.keys[target.keyNum++] = keys[i];}}

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

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

相关文章

新一代搜索引擎,是 ES 的15倍?

Manticore Search介绍 Manticore Search 是一个使用 C 开发的高性能搜索引擎&#xff0c;创建于 2017 年&#xff0c;其前身是 Sphinx Search 。Manticore Search 充分利用了 Sphinx&#xff0c;显着改进了它的功能&#xff0c;修复了数百个错误&#xff0c;几乎完全重写了代码…

iperf 测 TCP 和 UDP 网络吞吐量

注&#xff1a;本文为 “iperf 测网络吞吐量” 相关文章合辑。 未整理去重。 使用 iperf3 监测网络吞吐量 Tom 王 2019-12-21 22:23:52 一 iperf3 介绍 (1.1) iperf3 是一个网络带宽测试工具&#xff0c;iperf3 可以擦拭 TCP 和 UDP 带宽质量。iperf3 可以测量最大 TCP 带宽…

神经网络的数据流动过程(张量的转换和输出)

文章目录 1、文本从输入到输出&#xff0c;经历了什么&#xff1f;2、数据流动过程是张量&#xff0c;如何知道张量表达的文本内容&#xff1f;3、词转为张量、张量转为词是唯一的吗&#xff1f;为什么&#xff1f;4、如何保证词张量的质量和合理性5、总结 &#x1f343;作者介…

MediaPipe与YOLO已训练模型实现可视化人脸和手势关键点检测

项目首页 - ZiTai_YOLOV11:基于前沿的 MediaPipe 技术与先进的 YOLOv11 预测试模型&#xff0c;精心打造一款强大的实时检测应用。该应用无缝连接摄像头&#xff0c;精准捕捉画面&#xff0c;能即时实现人脸检测、手势识别以及骨骼关键点检测&#xff0c;将检测结果实时、直观地…

JAVA篇12 —— 泛型的使用

​ 欢迎来到我的主页&#xff1a;【Echo-Nie】 本篇文章收录于专栏【JAVA学习】 如果这篇文章对你有帮助&#xff0c;希望点赞收藏加关注啦~ 1 泛型介绍 先对集合进行说明&#xff0c;不能对加入到集合中的元素类型进行约束&#xff08;不安全&#xff09;。遍历的时候需要…

JavaScript 数据类型

基本概念 什么是数据类型 JavaScript是一种 灵活的动态类型语言 &#xff0c;其数据类型构成了程序的基础构建块。它主要包括两类数据类型&#xff1a; 原始数据类型 &#xff1a;包括String、Number、Boolean、Undefined、Null和Symbol。 复杂数据类型 &#xff1a;以Object…

被裁与人生的意义--春节随想

还有两个月就要被迫离开工作了十多年的公司了&#xff0c;不过有幸安安稳稳的过了一个春节&#xff0c;很知足! 我是最后一批要离开的&#xff0c;一百多号同事都没“活到”蛇年。看着一批批仁人志士被“秋后斩首”&#xff0c;马上轮到我们十来个&#xff0c;个中滋味很难言清…

Redis代金卷(优惠卷)秒杀案例-多应用版

Redis代金卷(优惠卷)秒杀案例-单应用版-CSDN博客 上面这种方案,在多应用时候会出现问题,原因是你通过用户ID加锁 但是在多应用情况下,会出现两个应用的用户都有机会进去 让多个JVM使用同一把锁 这样就需要使用分布式锁 每个JVM都会有一个锁监视器,多个JVM就会有多个锁监视器…

绘制决策树尝试3

目录 代码解读AI 随机状态 种子 定义决策树回归模型 tree的decision regressor fit 还可用来预测 export 效果图 我的X只有一个特征 为何这么多分支 &#xff1f;&#xff1f;&#xff1f; 这是CART回归 CART回归 为什么说代码是CART回归&#xff1f; 不是所有的决…

为大模型提供webui界面的利器:Open WebUI 完全本地离线部署deepseek r1

为大模型提供webui界面的利器&#xff1a;Open WebUI Open WebUI的官网&#xff1a;&#x1f3e1; Home | Open WebUI 开源代码&#xff1a;WeTab 新标签页 Open WebUI是一个可扩展、功能丰富、用户友好的自托管AI平台&#xff0c;旨在完全离线运行。它支持各种LLM运行程序&am…

langchain 实现多智能体多轮对话

这里写目录标题 工具定义模型选择graph节点函数定义graph 运行 工具定义 import random from typing import Annotated, Literalfrom langchain_core.tools import tool from langchain_core.tools.base import InjectedToolCallId from langgraph.prebuilt import InjectedSt…

【Block总结】CPCA,通道优先卷积注意力|即插即用

论文信息 标题: Channel Prior Convolutional Attention for Medical Image Segmentation 论文链接: arxiv.org 代码链接: GitHub 创新点 本文提出了一种新的通道优先卷积注意力&#xff08;CPCA&#xff09;机制&#xff0c;旨在解决医学图像分割中存在的低对比度和显著…

Python从零构建macOS状态栏应用(仿ollama)并集成AI同款流式聊天 API 服务(含打包为独立应用)

在本教程中,我们将一步步构建一个 macOS 状态栏应用程序,并集成一个 Flask 服务器,提供流式响应的 API 服务。 如果你手中正好持有一台 MacBook Pro,又怀揣着搭建 AI 聊天服务的想法,却不知从何处迈出第一步,那么这篇文章绝对是你的及时雨。 最终,我们将实现以下功能: …

强化学习、深度学习、深度强化学习的区别是什么?

前言 深度强化学习就是 深度学习 和 强化学习 的结合体。它让计算机程序&#xff08;也就是智能体&#xff09;在特定环境中不断尝试&#xff0c;从错误中学习&#xff0c;最终找到最优的行动策略。 深度学习是AlphaGo从棋谱里学习&#xff0c;强化学些Alphazero 学习规则&am…

string类(详解)

为什么学习string类&#xff1f; 1.1 C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP的思想&#xf…

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …

一些计算机零碎知识随写(25年2月)

今天复习 MySQL 的时候&#xff0c;我突然冒出一个想法&#xff1a;能不能远程连接 MySQL 呢&#xff1f;虽说心里清楚理论上可行&#xff0c;但一直没实际操作过。 于是&#xff0c;起床后我立马打开服务器&#xff0c;准备启动 MySQL。结果&#xff0c;这一启动就发现问题了&…

ESP32-c3实现获取土壤湿度(ADC模拟量)

1硬件实物图 2引脚定义 3使用说明 4实例代码 // 定义土壤湿度传感器连接的模拟输入引脚 const int soilMoisturePin 2; // 假设连接到GPIO2void setup() {// 初始化串口通信Serial.begin(115200); }void loop() {// 读取土壤湿度传感器的模拟值int sensorValue analogRead…

Java 大视界 -- Java 大数据在量子通信安全中的应用探索(69)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

86.(2)攻防世界 WEB PHP2

之前做过&#xff0c;回顾一遍&#xff0c;详解见下面这篇博客 29.攻防世界PHP2-CSDN博客 既然是代码审计题目&#xff0c;打开后又不显示代码&#xff0c;肯定在文件里 <?php // 首先检查通过 GET 请求传递的名为 "id" 的参数值是否严格等于字符串 "admi…