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

news2025/3/5 21:23:10

本文以 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. Java 实现多阶 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));  // true
        System.out.println("Contains 10: " + tree.contains(10));  // true
        tree.printTree();

        // 删除内部节点键
        tree.delete(30);
        System.out.println("Contains 30 after deletion: " + tree.contains(30));  // false
        tree.printTree();

        // 边界测试:删除后树结构调整
        tree.delete(10);
        tree.delete(20);
        tree.delete(40);
        System.out.println("Contains 50: " + tree.contains(50));  // true
        System.out.println("Contains 10 after deletion: " + tree.contains(10));  // false
    }
}

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

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

相关文章

论文阅读笔记:UniFace: Unified Cross-Entropy Loss for Deep Face Recognition

论文阅读笔记&#xff1a;UniFace: Unified Cross-Entropy Loss for Deep Face Recognition 1 背景2 创新点3 方法3.1 回顾softmax损失3.2 统一交叉熵损失3.3 人脸验证中的UCE损失3.4 进一步的优化3.4.1 边际UCE损失3.4.2 平衡BCE损失 4 实验4.1 消融实验4.2 和SOTA方法对比 论…

从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十) 收发消息

1.聊天框 首先我们完善前端的消息输入框 components下面新建MessageInput组件 import { useState,useRef } from "react" import {X,Image,Send} from "lucide-react"import { useChatStore } from "../store/useChatStore" import toast from…

5分钟看懂Deepseek开源周之六:Deepseek-V3/R1推理系统设计----揭开深度求索模型系统设计和运营成本之谜

前言 众所周知&#xff0c;四大天王一般有五个人。所以开源周五连发有第六天也很正常。贴上了开源周活动的github主贴&#xff0c;大家可以不上推特就能了解详情。 deepseek-ai/open-infra-index: Production-tested AI infrastructure tools for efficient AGI development a…

C++发展

目录 ​编辑C 的发展总结&#xff1a;​编辑 1. C 的早期发展&#xff08;1979-1985&#xff09; 2. C 标准化过程&#xff08;1985-1998&#xff09; 3. C 标准演化&#xff08;2003-2011&#xff09; 4. C11&#xff08;2011年&#xff09; 5. C14&#xff08;2014年&a…

动态规划/贪心算法

一、动态规划 动态规划 是一种用于解决优化问题的算法设计技术&#xff0c;尤其适用于具有重叠子问题和最优子结构性质的问题。它通过将复杂问题分解为更简单的子问题&#xff0c;并保存这些子问题的解以避免重复计算&#xff0c;从而提高效率。 动态规划的核心思想 最优子结…

python全栈-Linux基础

python全栈-Linux基础 文章目录 Linux安装/配置网络配置配置Linux远程登录配置虚拟机内部ip配置真机的ip安装XShell和Xftp目录结构用户和用户组用户管理添加用户useradd查看用户id修改用户usermod (选项)(参数)用户密码设置passed (选项)(参数)删除用户userdel [选项] 用户名 用…

基于https虚拟主机配置

一、https介绍 http 明文&#xff0c;80/tcp https 密文&#xff0c;443/tcp 二、安全性保障 1、数据安全性 数据加密 2、数据完整性 3、验证身份的真实性、有效性 三、数据安全性 手段&#xff1a;加密 发送方加密数据&#xff0c;接收方解密数据 对称加密算法 加密、解密数据…

Kmeans算法来实现RFM指标计算步骤

K-Means&#xff08;K均值&#xff09;是一种经典的无监督聚类算法&#xff0c;主要用于将数据集划分为 KKK 个不同的簇&#xff08;Cluster&#xff09;。 它基于最小化簇内样本的平方误差&#xff0c;即最小化数据点与簇中心的距离之和。 1. K-Means 算法原理 (1) 主要步骤 …

Vue2-3 优雅的在子组件修改父组件传递过来的v-model

在子组件修改父组件传递过来的v-model&#xff0c;这样会破坏单向数据流&#xff0c;造成屎山代码&#xff0c;为了避免这个问题&#xff0c;需要给一个中间层来相对舒服的使用v-model。方法就是用computed去拦截v-model,然后在computed 里面去触发 emit 事件来修改父组件传来的…

threejs:用着色器给模型添加光带扫描效果

第一步&#xff1a;给模型添加光带 首先创建一个立方体&#xff0c;不进行任何缩放平移操作&#xff0c;也不要set position。 基础代码如下&#xff1a; 在顶点着色器代码里varying vec3 vPosition;vPosition position;获得threejs自动计算的顶点坐标插值&#xff08;也就…

1.从0搭建前端Vue项目工程

我们通过vue官方提供的脚手架Vue-cli来快速生成一个Vue的项目模板。 **注意&#xff1a;**需要先安装NodeJS&#xff0c;然后才能安装Vue-cli。 环境准备好了&#xff0c;接下来我们需要通过Vue-cli创建一个vue项目&#xff0c;然后再学习一下vue项目的目录结构。Vue-cli提供了…

开放鸿蒙OpenHarmony 5.0.0 Release 兼容性测试实战经验分享

OpenHarmony 5.0版本的发布时间是2024年12月20日至21日。这个版本带来了许多新特性和改进。现在5.0出了两个release 版本&#xff0c;分别是5.0.0和5.0.1。 就在5.0版本发布不到2周的时间内&#xff0c;2025年01月01日起&#xff0c;不支持新产品基于老分支&#xff08;OpenHar…

Chromium_src源码

Chromium_src源码 码云上有一个OpenHarmony-TPC/chromium_src项目&#xff0c;目前已经停止维护了&#xff0c;迁移到GitCode上了&#xff0c;源代码项目地址为&#xff1a;openharmony-tpc/chromium_chrome 特此记录一下老的项目的相关软件架构 Chromium 简介 软件架构 软…

深度学习的正则化深入探讨

文章目录 一、说明二、学习目标三、什么是机器学习中的正则化四、了解过拟合和欠拟合五、代价函数的意义六、什么是偏差和方差&#xff1f;七、机器学习中的正则化&#xff1f; 一、说明 在训练机器学习模型时&#xff0c;模型很容易过拟合或欠拟合。为了避免这种情况&#xf…

《OpenCV》——dlib(人脸应用实例)

文章目录 dlib库dlib库——人脸应用实例——表情识别dlib库——人脸应用实例——疲劳检测 dlib库 dlib库的基础用法介绍可以参考这篇文章&#xff1a;https://blog.csdn.net/lou0720/article/details/145968062?spm1011.2415.3001.5331&#xff0c;故此这篇文章只介绍dlib的人…

tauri2+typescript+vue+vite+leaflet等的简单联合使用(一)

项目目标 主要的目的是学习tauri。 流程 1、搭建项目 2、简单的在项目使用leaflet 3、打包 准备项目 环境准备 废话不多说&#xff0c;直接开始 需要有准备能运行Rust的环境和Node&#xff0c;对于Rust可以参考下面这位大佬的文章&#xff0c;Node不必细说。 Rust 和…

本地部署阿里万象2.1文生视频模型(Wan2.1-T2V)完全指南

在生成式AI技术爆发式发展的今天,阿里云开源的万象2.1(Wan2.1)视频生成模型,为创作者提供了从文字/图像到高清视频的一站式解决方案。本文针对消费级显卡用户,以RTX 4060 Ti 16G为例,详解本地部署全流程与性能调优方案,涵盖环境配置、多模型选择策略、显存优化技巧及实战…

【Vue CLI脚手架开发】——3.组件交互props配置

文章目录 前言一、props数据接收方式二、代码实现1. 父组件2.子组件 三、分析 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;随着人工智能的不断发展&#xff0c;机器学习这门技术也越来越重要&#xff0c;很多人都开启了学习机器学习…

FPGA之USB通信实战:基于FX2芯片的Slave FIFO回环测试详解

FPGA之Usb数据传输 Usb 通信 你也许会有疑问&#xff0c;明明有这么多通信方式和数据传输&#xff08;SPI、I2C、UART、以太网&#xff09;为什么偏偏使用USB呢? 原因有很多&#xff0c;如下&#xff1a; 1. 高速数据传输能力 高带宽&#xff1a;USB接口提供了较高的数据传…

【Office-Word】如何自动生成中英文目录

1.目录介绍 Word这个自动生成目录非常强大&#xff0c;涉及的功能很琐碎&#xff0c;想要完美的生成目录不仅仅是只会目录这么简单&#xff0c;前后涉及到的大纲级别、目标样式和域代码等操作是比较头疼的。 下面就一步一步开始介绍 2.多级标题级别编号设置 目录想要设置好…