当前位置: 首页 > news >正文

数据结构与算法 - 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树的名称是由其发明者Rudolf Bayer提出的。Bayer和McCreight从未解释B代表什么,人们提出了许多可能的解释,比如Boeing、balance、between、broad、bushy和Bayer等。但McCreight表示,越是思考B-trees中的B代表什么,就越能更好地理解B-trees。

3. 特性

一棵B-树具有以下性质

特性1:每个节点x具有

  • 属性n,表示节点x中key的个数
  • 属性leaf,表示节点是否是叶子节点
  • 节点key可以有多个,以升序存储

特性2:每个非叶子节点中的孩子数是n + 1、叶子节点没有孩子

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

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

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

特性4:叶子节点的深度都相同

问题1:B-树为什么有最小度数的限制?

答:B树种有最小度数的限制是为了保证B树的平衡特性。

在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。

最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树种,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)

4. B-树与2-3树、2-3-4树的关系

可以这样总计它们之间的关系:

  • 2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点
  • 2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点
  • B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。

二、实现

1. 定义节点

static class Node {boolean leaf = true;int keyNumber;int t;int[] keys;Node[] children;    public Node(int t) {this.t = t;this.keys = new int[2 * t - 1];this.children = new Node[2 * t];}@Overridepublic String toString() {return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));}
}
  • leaf表示是否是叶子节点
  • keyNumber为keys中有效key数目
  • t为最小度数,它决定了节点中key的最小、最大数目,分别是t - 1 和 2t - 1
  • keys存储此节点的key
  • children存储此节点的child
  • toString只是为了方便调试和测试,非必须
  • 实际keys应当改为entries以便同时保存key和value,刚开始简化实现

2. 多路查找

为上面节点添加get方法

        /*** 多路查找* @param key* @return*/public Node get(int key) {int i = 0;while(i < keyNumber) {if(keys[i] == key) {return this;}if(keys[i] > key) {break;}i++;}// 执行到此时,keys[i] > key 或 i==keyNumberif(leaf) {return null;}// 非叶子节点情况return children[i].get(key);}

3. 插入key和child

为上面节点类添加insertKey和insertChild方法

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

作用是向keys数组或children数组指定index处插入新数据,注意

①由于使用了静态数组,并且不会在新增或删除时改变它的大小,因此需要额外的keyNumber来指定数组内有效key的数目

  • 插入时keyNumber++
  • 删除时减少keyNumber的值即可

②children不会单独维护数目,它比keys多一个

③如果这两个方法同时调用,注意它们的先后顺序,insertChild后调用,因为它计算复制元素时用到了keyNumber

4. 定义树

public class BTree {final int t;final int MIN_KEY_NUMBER;final int MAX_KEY_NUMBER;Node root;public BTree() {this(2);}public BTree(int t) {this.t = t;MIN_KEY_NUMBER = t - 1;MAX_KEY_NUMBER = 2 * t - 1;root = new Node(t);}
}

5. 插入

    /*** 新增* @param key*/public void put(int key) {doPut(root, key, null, 0);}private void doPut(Node node, int key, Node parent, int index) {// 1. 查找本节点的插入位置iint i = 0;while(i < node.keyNumber) {if(node.keys[i] == key) {// 更新return;}if(node.keys[i] > key) {break;  // 找到插入位置,即为此时的i}i++;}// 2. 如果节点是叶子节点,可以直接插入了if(node.leaf) {node.insertKey(key, i);// 上限}// 3. 如果节点是非叶子节点,需要在children[i]处继续递归插入else {doPut(node.children[i], key, node, i);// 上限}if(isFull(node)) {split(node, parent, index);}}boolean isFull(Node node) {return node.keyNumber == MAX_KEY_NUMBER;}

首先查找本节点中的插入位置i,如果没有空位(key被找到),应该走更新的逻辑,目前什么没做

接下来分两种情况:

  • 如果节点是叶子节点,可以直接插入了
  • 如果节点是非叶子节点,需要在children[i]处继续递归插入

无论哪种情况,插入完成后都可能超过节点keys数目限制,此时应当执行节点分裂

  • 参数中的parent和index都是给分裂方法用的,代表当前节点父节点,和分裂节点都是第几个孩子

判断依据为:

boolean isFull(Node node) {return node.keyNumber == MAX_KEY_NUMBER;
}

6. 分裂

    /*** 分裂* @param left 要分裂的节点* @param parent 分裂节点的父节点* @param index 分裂节点是第几个孩子*/private void split(Node left, Node parent, int index) {// 分裂节点为根节点if(parent == null) {Node newRoot = new Node(t);newRoot.leaf = false;newRoot.insertChild(left, 0);this.root = newRoot;parent = newRoot;}// 1. 创建right节点,把left节点中t之后的key和child移动过去Node right = new Node(t);// 新增节点是否是叶子节点与待分裂节点一致right.leaf = left.leaf;System.arraycopy(left.keys, t, right.keys, 0, t - 1);// 如果分裂节点为非叶子节点if(!left.leaf) {System.arraycopy(left.children, t, right.children, 0, t);}right.keyNumber = t - 1;left.keyNumber = t - 1;// 2. 中间的key(t - 1处)插入到父节点int mid = left.keys[t - 1];parent.insertKey(mid, index);// 3. right节点作为父节点的孩子parent.insertChild(right, index + 1);}

分为两种情况:

①如果parent == null,表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的0孩子

②否则

  • 创建right节点(分裂后大于当前left节点的)把t以后的key和child都拷贝过去
  • t - 1处的key插入到parent的index处,index指left作为孩子时的索引
  • right节点作为parent的孩子插入到index+1处

7. 删除

case 1:当前节点是叶子节点,没找到

case 2:当前节点是叶子节点,找到了

case 3:当前节点是非叶子节点,没找到

case 4:当前节点是非叶子节点,找到了

case 5:删除后key数目 < 下限(不平衡)

case 6:根节点

Node节点类添加一些方法:

        /*** 向keys指定索引处插入key* @param key* @param index*/public void insertKey(int key, int index) {System.arraycopy(keys, index, keys, index + 1, keyNumber - index);keys[index] = key;keyNumber++;}/*** 向children指定索引处插入child* @param child* @param index*/public void insertChild(Node child, int index) {System.arraycopy(children, index, children, index + 1, keyNumber - index);children[index] = child;}/*** 移除指定index处的key* @param index* @return*/int removeKey(int index) {int t = keys[index];System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);return t;}/*** 移除最左边的key* @return*/public int removeLeftmostKey() {return removeKey(0);}/*** 移除最右边的key* @return*/public int removeRightmostKey() {return removeKey(keyNumber - 1);}/*** 移除指定index处的child* @param index* @return*/public Node removeChild(int index) {Node t = children[index];System.arraycopy(children, index + 1, children, index, keyNumber - index);children[keyNumber] = null;  // help GCreturn t;}/*** 移除最左边的child* @return*/public Node removeLeftmostChild() {return removeChild(0);}/*** 移除最右边的child* @return*/public Node removeRightmostChild() {return removeChild(keyNumber);}/*** index 孩子处左边的兄弟* @param index* @return*/public Node childLeftSibling(int index) {return index > 0 ? children[index - 1] : null;}/*** index 孩子处右边的兄弟* @param index* @return*/public Node childRightSibling(int index) {return index == keyNumber ? null : children[index + 1];}/*** 复制当前节点的所有key和child到target* @param target*/public void moveToTarget(Node target) {int start = target.keyNumber;if(!leaf) {for (int i = 0; i <= keyNumber; i++) {target.children[start + i] = children[i];}}for (int i = 0; i < keyNumber; i++) {target.keys[target.keyNumber++] = keys[i];}}

删除代码:

    /*** 删除key* @param key*/public void remove(int key) {doRemove(null, root, 0, key);}private void doRemove(Node parent, Node node, int index, int key) {int i = 0;// 在有效范围内while(i < node.keyNumber) {if(node.keys[i] >= key) {break;}i++;}// 情况1:找到,i代表待删除key的索引// 情况2:没找到,i代表到第i个孩子继续查找if (node.leaf) {  // 当前节点是叶子节点if(!found(node,key, i)) {  // case 1 没找到return;} else {  // case 2 找到了node.removeKey(i);}} else {  // 当前节点不是叶子节点if(!found(node,key, i)) {  // case 3 没找到// 到孩子节点继续查找doRemove(node, node.children[i], i, key);} else {  // case 4 找到了// 1. 找后继keyNode s = node.children[i + 1];  // 当前节点的后一个孩子while(!s.leaf) {// 直到叶子节点,取最左边的s = s.children[0];}int skey = s.keys[0];// 2. 替换待删除keynode.keys[i] = skey;// 3. 删除后继keydoRemove(node, node.children[i + 1], i + 1, skey);}}// 删除后key数目小于下限if(node.keyNumber < MIN_KEY_NUMBER) {// 调整平衡 case 5 case 6balance(parent, node, index);}}/*** 是否找到key* @param node* @param key* @param i* @return*/private boolean found(Node node, int key, int i) {return i < node.keyNumber && node.keys[i] == key;}/*** 调整平衡* @param parent 父节点* @param x 待调整节点* @param i 索引*/private void balance(Node parent, Node x, int i) {// case 6 根节点if(x == root) {if(root.keyNumber == 0 && root.children[0] != null) {root = root.children[0];}return;}// 获取左右两边的兄弟Node left = parent.childLeftSibling(i);Node right = parent.childRightSibling(i);if(left != null && left.keyNumber > MIN_KEY_NUMBER) {// case 5-1 左边富裕,右旋// a) 父节点中前驱key旋转下来x.insertKey(parent.keys[i - 1], 0);if(!left.leaf) {// b) 左边的兄弟不是叶子节点,把最右侧的孩子过继给被调整的节点x.insertChild(left.removeRightmostChild(), 0);}// c) 删除左边兄弟的最右节点,移到父节点(旋转上去)parent.keys[i - 1] = left.removeRightmostKey();return;}if(right != null && right.keyNumber > MIN_KEY_NUMBER) {// case 5-2 右边富裕,左旋// a) 父节点中后去key旋转下来x.insertKey(parent.keys[i], x.keyNumber);if(!right.leaf) {// b) 右边的兄弟不是叶子节点,把最左侧的孩子过继给被调整的节点x.insertChild(right.removeLeftmostChild(), x.keyNumber + 1);}// c) 删除右边兄弟的最左节点,移到父节点(旋转上去)parent.keys[i] = right.removeLeftmostKey();return;}// case 5-3 两边都不富裕,向左合并if(left != null) {// 向左兄弟合并// 将待删除节点从父节点移除parent.removeChild(i);// 从父节点合并一个key到左兄弟left.insertKey(parent.removeKey(i - 1), left.keyNumber);// 将待删除节点的剩余节点和孩子移到到左边x.moveToTarget(left);} else {// 没有左兄弟,向自己合并// 把它的右兄弟移除parent.removeChild(i + 1);// 父节点移除一个key,插入到待删除节点x.insertKey(parent.removeKey(i), x.keyNumber);// 将右兄弟合并过来right.moveToTarget(x);}}

8. 完整代码

package com.itheima.datastructure.BTree;import java.util.Arrays;public class BTree {static class Node {int[] keys;  // 关键字Node[] children;  // 孩子int keyNumber;  // 有效关键字数目boolean leaf = true;  // 是否是叶子节点int t; // 最小度数public Node(int t) {  // t >= 2this.t = t;this.children = new Node[2 * t];this.keys = new int[2 * t - 1];}public Node(int[] keys) {this.keys = keys;}@Overridepublic String toString() {return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));}/*** 多路查找* @param key* @return*/public Node get(int key) {int i = 0;while(i < keyNumber) {if(keys[i] == key) {return this;}if(keys[i] > key) {break;}i++;}// 执行到此时,keys[i] > key 或 i==keyNumberif(leaf) {return null;}// 非叶子节点情况return children[i].get(key);}/*** 向keys指定索引处插入key* @param key* @param index*/public void insertKey(int key, int index) {System.arraycopy(keys, index, keys, index + 1, keyNumber - index);keys[index] = key;keyNumber++;}/*** 向children指定索引处插入child* @param child* @param index*/public void insertChild(Node child, int index) {System.arraycopy(children, index, children, index + 1, keyNumber - index);children[index] = child;}/*** 移除指定index处的key* @param index* @return*/int removeKey(int index) {int t = keys[index];System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);return t;}/*** 移除最左边的key* @return*/public int removeLeftmostKey() {return removeKey(0);}/*** 移除最右边的key* @return*/public int removeRightmostKey() {return removeKey(keyNumber - 1);}/*** 移除指定index处的child* @param index* @return*/public Node removeChild(int index) {Node t = children[index];System.arraycopy(children, index + 1, children, index, keyNumber - index);children[keyNumber] = null;  // help GCreturn t;}/*** 移除最左边的child* @return*/public Node removeLeftmostChild() {return removeChild(0);}/*** 移除最右边的child* @return*/public Node removeRightmostChild() {return removeChild(keyNumber);}/*** index 孩子处左边的兄弟* @param index* @return*/public Node childLeftSibling(int index) {return index > 0 ? children[index - 1] : null;}/*** index 孩子处右边的兄弟* @param index* @return*/public Node childRightSibling(int index) {return index == keyNumber ? null : children[index + 1];}/*** 复制当前节点的所有key和child到target* @param target*/public void moveToTarget(Node target) {int start = target.keyNumber;if(!leaf) {for (int i = 0; i <= keyNumber; i++) {target.children[start + i] = children[i];}}for (int i = 0; i < keyNumber; i++) {target.keys[target.keyNumber++] = keys[i];}}}Node root;  // 根节点int t;  // 树中节点最小度数final int MIN_KEY_NUMBER;  // 最小key数目final int MAX_KEY_NUMBER;  // 最大key数目public BTree() {this(2);}public BTree(int t) {this.t = t;root = new Node(t);MAX_KEY_NUMBER = 2 * t - 1;MIN_KEY_NUMBER = t - 1;}/*** key是否存在* @param key* @return*/public boolean contains(int key) {return root.get(key) != null;}/*** 新增* @param key*/public void put(int key) {doPut(root, key, null, 0);}private void doPut(Node node, int key, Node parent, int index) {// 1. 查找本节点的插入位置iint i = 0;while(i < node.keyNumber) {if(node.keys[i] == key) {// 更新return;}if(node.keys[i] > key) {break;  // 找到插入位置,即为此时的i}i++;}// 2. 如果节点是叶子节点,可以直接插入了if(node.leaf) {node.insertKey(key, i);// 上限}// 3. 如果节点是非叶子节点,需要在children[i]处继续递归插入else {doPut(node.children[i], key, node, i);// 上限}if(isFull(node)) {split(node, parent, index);}}boolean isFull(Node node) {return node.keyNumber == MAX_KEY_NUMBER;}/*** 分裂* @param left 要分裂的节点* @param parent 分裂节点的父节点* @param index 分裂节点是第几个孩子*/private void split(Node left, Node parent, int index) {// 分裂节点为根节点if(parent == null) {Node newRoot = new Node(t);newRoot.leaf = false;newRoot.insertChild(left, 0);this.root = newRoot;parent = newRoot;}// 1. 创建right节点,把left节点中t之后的key和child移动过去Node right = new Node(t);// 新增节点是否是叶子节点与待分裂节点一致right.leaf = left.leaf;System.arraycopy(left.keys, t, right.keys, 0, t - 1);// 如果分裂节点为非叶子节点if(!left.leaf) {System.arraycopy(left.children, t, right.children, 0, t);}right.keyNumber = t - 1;left.keyNumber = t - 1;// 2. 中间的key(t - 1处)插入到父节点int mid = left.keys[t - 1];parent.insertKey(mid, index);// 3. right节点作为父节点的孩子parent.insertChild(right, index + 1);}/*** 删除key* @param key*/public void remove(int key) {doRemove(null, root, 0, key);}private void doRemove(Node parent, Node node, int index, int key) {int i = 0;// 在有效范围内while(i < node.keyNumber) {if(node.keys[i] >= key) {break;}i++;}// 情况1:找到,i代表待删除key的索引// 情况2:没找到,i代表到第i个孩子继续查找if (node.leaf) {  // 当前节点是叶子节点if(!found(node,key, i)) {  // case 1 没找到return;} else {  // case 2 找到了node.removeKey(i);}} else {  // 当前节点不是叶子节点if(!found(node,key, i)) {  // case 3 没找到// 到孩子节点继续查找doRemove(node, node.children[i], i, key);} else {  // case 4 找到了// 1. 找后继keyNode s = node.children[i + 1];  // 当前节点的后一个孩子while(!s.leaf) {// 直到叶子节点,取最左边的s = s.children[0];}int skey = s.keys[0];// 2. 替换待删除keynode.keys[i] = skey;// 3. 删除后继keydoRemove(node, node.children[i + 1], i + 1, skey);}}// 删除后key数目小于下限if(node.keyNumber < MIN_KEY_NUMBER) {// 调整平衡 case 5 case 6balance(parent, node, index);}}/*** 是否找到key* @param node* @param key* @param i* @return*/private boolean found(Node node, int key, int i) {return i < node.keyNumber && node.keys[i] == key;}/*** 调整平衡* @param parent 父节点* @param x 待调整节点* @param i 索引*/private void balance(Node parent, Node x, int i) {// case 6 根节点不平衡if(x == root) {if(root.keyNumber == 0 && root.children[0] != null) {root = root.children[0];}return;}// 获取左右两边的兄弟Node left = parent.childLeftSibling(i);Node right = parent.childRightSibling(i);if(left != null && left.keyNumber > MIN_KEY_NUMBER) {// case 5-1 左边富裕,右旋// a) 父节点中前驱key旋转下来x.insertKey(parent.keys[i - 1], 0);if(!left.leaf) {// b) 左边的兄弟不是叶子节点,把最右侧的孩子过继给被调整的节点x.insertChild(left.removeRightmostChild(), 0);}// c) 删除左边兄弟的最右节点,移到父节点(旋转上去)parent.keys[i - 1] = left.removeRightmostKey();return;}if(right != null && right.keyNumber > MIN_KEY_NUMBER) {// case 5-2 右边富裕,左旋// a) 父节点中后去key旋转下来x.insertKey(parent.keys[i], x.keyNumber);if(!right.leaf) {// b) 右边的兄弟不是叶子节点,把最左侧的孩子过继给被调整的节点x.insertChild(right.removeLeftmostChild(), x.keyNumber + 1);}// c) 删除右边兄弟的最左节点,移到父节点(旋转上去)parent.keys[i] = right.removeLeftmostKey();return;}// case 5-3 两边都不富裕,向左合并if(left != null) {// 向左兄弟合并// 将待删除节点从父节点移除parent.removeChild(i);// 从父节点合并一个key到左兄弟left.insertKey(parent.removeKey(i - 1), left.keyNumber);// 将待删除节点的剩余节点和孩子移到到左边x.moveToTarget(left);} else {// 没有左兄弟,向自己合并// 把它的右兄弟移除parent.removeChild(i + 1);// 父节点移除一个key,插入到待删除节点x.insertKey(parent.removeKey(i), x.keyNumber);// 将右兄弟合并过来right.moveToTarget(x);}}}

B站视频链接:基础算法-175-B树-remove-演示2_哔哩哔哩_bilibili

相关文章:

数据结构与算法 - B树

一、概述 1. 历史 B树(B-Tree)结构是一种高效存储和查询数据的方法&#xff0c;它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database Systems》中的&#xff…...

Java二十三种设计模式-观察者模式(15/23)

观察者模式&#xff1a;实现对象间的松耦合通知机制 引言 在当今的软件开发领域&#xff0c;设计模式已成为创建可维护、可扩展和可重用代码的基石。在众多设计模式中&#xff0c;观察者模式以其独特的能力&#xff0c;实现对象间的松耦合通信而脱颖而出。本文将深入探讨观察…...

opencv-python图像增强二:图像去雾(暗通道去雾)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、简介&#xff1a;二、暗通道去雾方案简述&#xff1a;三、算法实现步骤3.1最小值滤波3.2 引导滤波3.3 计算图像全局光强 四&#xff1a;整体代码实现五&#xf…...

自研Vue3低代码海报制作平台第一步:基础拖拽组件开发

学习来源&#xff1a;稀土掘金 - 幽月之格大佬的技术专栏可拖拽、缩放、旋转组件 - 著作&#xff1a;可拖拽、缩放、旋转组件实现细节 非常感谢大佬&#xff01;受益匪浅&#xff01; 前面我们学习了很多vue3的知识&#xff0c;是时候把它们用起来做一个有意思的平台&#xf…...

QT 的 QSettings 读写 INI 文件的示例

在Qt中&#xff0c;QSettings 类提供了一种便捷的方式来存储和访问应用程序的设置&#xff0c;这些设置可以存储在多种格式的文件中&#xff0c;包括INI、Windows注册表&#xff08;仅Windows平台&#xff09;、XML和JSON等。以下是一些使用 QSettings 读写INI文件的示例。 写…...

【零基础学习CAPL语法】——testStep:测试结果输出函数

文章目录 1.函数介绍2.在报告中体现 1.函数介绍 testStep——测试结果输出函数 2.在报告中体现 //testStep() void PrintTxMsg() {testStep("Tx","[%x] [%.2x %.2x %.2x %.2x %.2x %.2x %.2x %.2x]",Diag_Req.id,Diag_Req.byte(0),Diag_Req.byte(1),Di…...

8.5.数据库基础技术-规范化

函数依赖 函数依赖&#xff1a;给定一个X,能唯一确定一个Y,就称X决定&#xff08;确定&#xff09;Y,或者说Y依赖于X。 例如&#xff1a;YX*X函数&#xff0c;此时X能确定Y的值&#xff0c;但是Y无法确定X的值&#xff0c;比如x2,y4,但是y4无法确定x2。函数依赖又可扩展以下两…...

于博士Cadence视频教程学习笔记备忘

标签&#xff1a;PCB教程 PCB设计步骤 cadence教程 Allegro教程 以下是我学习该视频教程的笔记&#xff0c;记录下备忘&#xff0c;欢迎大家在此基础上完善&#xff0c;能回传我一份是最好了&#xff0c;先谢过。 备注&#xff1a; 1、未掌握即未进行操作 2、操作软件是15.…...

8.3.数据库基础技术-关系代数

并&#xff1a;结果是两张表中所有记录数合并&#xff0c;相同记录只显示一次。交&#xff1a;结果是两张表中相同的记录。差&#xff1a;S1-S2,结果是S1表中有而S2表中没有的那些记录。 笛卡尔积&#xff1a;S1XS2,产生的结果包括S1和S2的所有属性列&#xff0c;并且S1中每条记…...

【Vue3】vue模板中如何使用enum枚举类型

简言 有的时候&#xff0c;我们想在vue模板中直接使用枚举类型的值&#xff0c;来做一些判断。 ts枚举 枚举允许开发人员定义一组命名常量。使用枚举可以更容易地记录意图&#xff0c;或创建一组不同的情况。TypeScript 提供了基于数字和字符串的枚举。 枚举的定义这里不说了…...

组合求和2

题目描述&#xff1a; Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target. Each number in candidates may only be used once in the combinati…...

Apple Maps现在可在Firefox和Mac版Edge浏览器中使用

Apple Maps最初只能在 Windows 版 Safari、Chrome 浏览器和 Edge 浏览器上运行&#xff0c;现在已在其他浏览器上运行&#xff0c;包括 Mac 版 Firefox 和 Edge。经过十多年的等待&#xff0c;Apple Maps于今年 7 月推出了新版地图应用的测试版&#xff0c;但只能在有限的浏览器…...

基于嵌入式Linux的数据库

数据库 数据库是在数据库管理系统和控制之下&#xff0c;存放在存储 介质上的数据集合。 基于嵌入式的数据库 基于嵌入式linux的数据库主要有SQlite&#xff0c; Firebird,Berkeley DB,eXtremeDB Firebird是关系型数据库&#xff0c;功能强大&#xff0c;支持存储过 程&…...

C# 使用LINQ找出一个子字符串在另一个字符串中出现的所有位置

一、实现步骤 遍历主字符串&#xff0c;使用IndexOf方法查找子字符串的位置。如果找到了子字符串&#xff0c;记录其位置&#xff0c;并且从该位置的后面继续查找。重复上述步骤直到遍历完整个字符串。 二、简单代码示例 using System; using System.Collections.Generic; usi…...

YOLOv8添加MobileViTv3模块(代码+free)

目录 一、理由 二、方法 &#xff08;1&#xff09;导入MobileViTv3模块 &#xff08;2&#xff09;在ultralytics/nn/tasks.py的函数parse_model中修改 &#xff08;3&#xff09;在yaml配置文件中写入 &#xff08;4&#xff09;开始训练&#xff0c;先把其他梯度关闭&…...

从概念到落地:全面解析DApp项目开发的核心要素与未来趋势

随着区块链技术的迅猛发展&#xff0c;去中心化应用程序&#xff08;DApp&#xff09;逐渐成为Web3时代的重要组成部分。DApp通过智能合约和分布式账本技术&#xff0c;提供了无需信任中介的解决方案&#xff0c;这种去中心化的特性使其在金融、游戏、社交等多个领域得到了广泛…...

仓颉编程入门 -- 泛型概述 , 如何定义泛型函数

泛型概述 , 如何定义泛型函数 1 . 泛型的定义 在仓颉编程语言中&#xff0c;泛型机制允许我们定义参数化类型&#xff0c;这些类型在声明时不具体指定其操作的数据类型&#xff0c;而是作为类型形参保留&#xff0c;待使用时通过类型实参来明确。这种灵活性在函数和类型声明中…...

SOC估算方法之(OCV-SOC+安时积分法)

一、引言 此方法主要参考电动汽车用磷酸铁锂电池SOC估算方法这篇论文 总结&#xff1a; 开路电压的测量需要将电池静止相当长的一段时间才能达到平衡状态进行测量。 安时积分法存在初始SOC的估算和累积的误差。 所以上述两种方法都存在一定的缺陷&#xff0c;因此下面主要讲…...

指针(下)

文章目录 指针(下)野指针、空指针野指针空指针 二级指针**main**函数的原型说明 常量指针与指针常量常量指针指针常量常量指针常量 动态内存分配常用函数**malloc****calloc****realloc****free** **void**与**void***的区别扩展&#xff1a;形式参数和实际参数的对应关系 指针…...

C# 浅谈IEnumerable

一、IEnumerable 简介 IEnumerable 是一个接口&#xff0c;它定义了对集合进行迭代所需的方法。IEnumerable 接口主要用于允许开发者使用foreach循环来遍历集合中的元素。这个接口定义了一个名为 GetEnumerator 的方法&#xff0c;该方法返回一个实现了 IEnumerator 接口的对象…...

mmdebstrap:创建 Debian 系统 chroot 环境的利器 ️

文章目录 mmdebstrap 的一般性参数说明 &#x1f4dc;mmdebstrap 的常见用法示例 &#x1f308;使用 mmdebstrap 的注意事项 ⚠️ &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&am…...

【Linux SQLite数据库】一、SQLite交叉编译与移植

SQLite 是一个用 C 语言编写的开源、轻量级、快速、独立且高可靠性的 SQL 数据库引擎&#xff0c;它提供了功能齐全的数据库解决方案。SQLite 几乎可以在所有的手机和计算机上运行&#xff0c;它被嵌入到无数人每天都在使用的众多应用程序中。此外&#xff0c;SQLite 还具有稳定…...

每天写两道(数组篇)移除元素、

27.移除元素 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。 假设 nums 中不等于 val 的元素数量为 k&#xff0c;要通过此题&#xff0c;您需要执行以下操作&#…...

Unity 使用 NewtonSoft Json插件报错

JsonReaderException: Unexpected character encountered while parsing value: . Path , line 0, position 0. 通过断点发现&#xff0c;头有一串ZWNBSP&#xff0c;这个是BOM格式的JSON。在文件下看不到。 解决方法&#xff1a;改编码格式&#xff0c;Remove BOM....

k8s 部署 Mysqld_exporter 以及添加告警规则

最近监控 mysql 数据库&#xff0c;用了 pmm-server、pmm-client 发现监控是真的不太好用&#xff0c;还是用回 prometheus 吧。 部署mysqld_exporter k8s 部署最新版本的 mysqld_exporter&#xff0c;支持的数据库版本 MySQL >5.6、MariaDB > 10.3。 先在数据库创建用…...

基于STM32开发的智能农业环境监测系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码控制代码应用场景 农田环境监测温室环境控制常见问题及解决方案 常见问题解决方案结论 1. 引言 智能农业环境监测系统通过集成多种环境传感器&#xff0c;实时监测土壤湿度、温度…...

【SQL】平均售价

目录 题目 分析 代码 题目 表&#xff1a;Prices ------------------------ | Column Name | Type | ------------------------ | product_id | int | | start_date | date | | end_date | date | | price | int | ---------------…...

存储器与CPU的连接

1.单块存储芯片与CPU的连接 单独的一块独立的存储芯片提供的线有&#xff1a;地址总线&#xff0c;数据总线&#xff0c;读写控制线&#xff0c;片选线&#xff0c;如果该存储器只有八根数据总线用于输出数据&#xff0c;而cpu一次可以读64位的数据呢&#xff1f; 我们可以将八…...

unity--webgl 访问本地index.html

目录 1:使用本地服务器 1.1 使用 Python 的 SimpleHTTPServer 1.2 使用 Node.js 的 http-server 2&#xff1a;让其他人通过 IP 地址来访问你的 Unity WebGL 项目 2.1: 确保服务器可访问 2.2 获取公共 IP 地址 2.3 配置本地服务器 1.使用 Python 的 SimpleHTTPServer 2…...

慢慢欣赏DPDK RTE_MAX_ETHPORTS的定义

DPDK代码里面&#xff0c;RTE_MAX_ETHPORTS是一个常见的宏定义&#xff0c;但是在.c和.h文件找不到其定义&#xff0c;在全文件搜索条件下&#xff0c;在config/meson.build找到这么一个定义 dpdk_conf.set(RTE_MAX_ETHPORTS, get_option(max_ethports)) 该宏定义是根据构建输…...