数据结构与算法(九):分治与回溯算法

news2024/12/27 11:39:56

参考引用

  • Hello 算法
  • Github:hello-algo

1. 分治算法

  • 分治(divide and conquer),全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括 “分” 和 “治” 两个步骤

    • 分(划分阶段):递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止
    • 治(合并阶段):从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解
  • “归并排序” 是分治策略的典型应用之一

    • 分:递归地将原数组(原问题)划分为两个子数组(子问题),直到子数组只剩一个元素(最小子问题)
    • 治:从底至顶地将有序的子数组(子问题的解)进行合并,从而得到有序的原数组(原问题的解)

在这里插入图片描述

1.1 如何判断分治问题

  • 一个问题是否适合使用分治解决,通常可以参考以下几个判断依据
    • 问题可以被分解:原问题可以被分解成规模更小、类似的子问题,以及能够以相同方式递归地进行划分
    • 子问题是独立的:子问题之间是没有重叠的,互相没有依赖,可以被独立解决
    • 子问题的解可以被合并:原问题的解通过合并子问题的解得来

1.2 通过分治提升效率

  • 分治不仅可以有效地解决算法问题,往往还可以带来算法效率的提升。在排序算法中,快速排序、归并排序、堆排序相较于选择、冒泡、插入排序更快,就是因为它们应用了分治策略
1.2.1 操作数量优化
  • 以 “冒泡排序” 为例,其处理一个长度为 n n n 的数组需要 O ( n 2 ) O(n^2) O(n2) 时间。假设按下图所示方式,将数组从中点分为两个子数组,则划分需要 O ( n ) O(n) O(n) 时间,排序每个子数组需要 O ( ( n 2 ) 2 ) O((\frac{n}{2})^2) O((2n)2) 时间,合并两个子数组需要 O ( n ) O(n) O(n) 时间,总体时间复杂度为 O ( n + ( n 2 ) 2 × 2 + n ) = O ( n 2 2 + 2 n ) O(n+(\frac{n}{2})^2\times2+n)=O(\frac{n^2}{2}+2n) O(n+(2n)2×2+n)=O(2n2+2n)

在这里插入图片描述

  • 下式中左边和右边分别为划分前和划分后的操作总数

n 2 − n 2 2 − 2 n > 0 n ( n − 4 ) > 0 \begin{aligned}n^2-\frac{n^2}2-2n&>0\\n(n-4)&>0\end{aligned} n22n22nn(n4)>0>0

  • 这意味着当 n > 4 n > 4 n>4 时,划分后的排序效率应该更高
    • 划分后的时间复杂度仍然是平方阶 O ( n 2 ) O(n^2) O(n2) ,只是复杂度中的常数项变小了
  • 如果把子数组不断地再从中点划分为两个子数组,直至子数组只剩一个元素时停止划分,这种思路实际上就是 “归并排序”,时间复杂度为 O ( n l o g n ) O(nlog n) O(nlogn)
  • 如果多设置几个划分点,将原数组平均划分为 k k k 个子数组,这种情况与 “桶排序” 非常类似,它非常适合排序海量数据,理论上时间复杂度可以达到 O ( n + k ) O(n + k) O(n+k)
1.2.2 并行计算优化
  • 分治生成的子问题是相互独立的,因此通常可以并行解决

    • 也就是说,分治不仅可以降低算法的时间复杂度,还有利于操作系统的并行优化
  • 并行优化在多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分地利用计算资源,从而显著减少总体的运行时间。如下图所示的 “桶排序” 中,将海量数据平均分配到各个桶中,则可将所有桶的排序任务分散到各个计算单元,完成后再进行结果合并

在这里插入图片描述

1.3 分治常见应用

  • 寻找最近点对
    • 该算法首先将点集分成两部分,然后分别找出两部分中的最近点对,最后再找出跨越两部分的最近点对
  • 大整数乘法
    • 例如 Karatsuba 算法,它是将大整数乘法分解为几个较小的整数的乘法和加法
  • 矩阵乘法
    • 例如 Strassen 算法,它是将大矩阵乘法分解为多个小矩阵的乘法和加法
  • 汉诺塔问题
    • 汉诺塔问题可以视为典型的分治策略,通过递归解决
  • 求解逆序对
    • 在一个序列中,如果前面的数字大于后面的数字,那么这两个数字构成一个逆序对。求解逆序对问题可以通过分治的思想,借助归并排序进行求解

2. 分治搜索策略

  • 搜索算法分为两大类

    • 暴力搜索
      • 通过遍历数据结构实现
      • 时间复杂度为 O ( n ) O(n) O(n)
    • 自适应搜索
      • 利用特有的数据组织形式或先验信息
      • 时间复杂度可达到 O ( l o g n ) O(log n) O(logn) 甚至 O ( 1 ) O(1) O(1)
  • 实际上,时间复杂度为 O ( l o g n ) O(log n) O(logn) 的搜索算法通常都是基于分治策略实现的,例如二分查找和树

    • 二分查找的每一步都将问题(在数组中搜索目标元素)分解为一个小问题(在数组的一半中搜索目标元素),这个过程一直持续到数组为空或找到目标元素为止
    • 树是分治关系的代表,在二叉搜索树、AVL 树、堆等数据结构中,各种操作的时间复杂度皆为 O ( l o g n ) O(log n) O(logn)

分治能够提升搜索效率,本质上是因为暴力搜索每轮只能排除一个选项,而分治搜索每轮可以排除一半选项

基于分治实现二分查找

  • 之前内容中的二分查找是基于递推(迭代)实现,现在基于分治(递归) 来实现

问题:给定一个长度为 n 的有序数组 nums,数组中所有元素都是唯一的,请查找元素 target

  • 将搜索区间 [ i , j ] [i, j] [i,j] 对应的子问题记为 f ( i , j ) f(i, j) f(i,j)。从原问题 f ( 0 , n − 1 ) f(0, n-1) f(0,n1) 为起始点,通过以下步骤进行二分查找
    • 计算搜索区间 [ i , j ] [i, j] [i,j] 的中点 m m m,根据它排除一半搜索区间
    • 递归求解规模减小一半的子问题,可能为 f ( i , m − 1 ) f(i, m-1) f(i,m1) f ( m + 1 , j ) f(m+1, j) f(m+1,j)
    • 循环第 1 和 2 步,直至找到 target 或区间为空时返回
  • 下图展示了在数组中二分查找元素 6 的分治过程

在这里插入图片描述

/* 二分查找:问题 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
    // 若区间为空,代表无目标元素,则返回 -1
    if (i > j) {
        return -1;
    }
    // 计算中点索引 m
    int m = (i + j) / 2;
    if (nums[m] < target) {
        // 递归子问题 f(m+1, j)
        return dfs(nums, target, m + 1, j);
    } else if (nums[m] > target) {
        // 递归子问题 f(i, m-1)
        return dfs(nums, target, i, m - 1);
    } else {
        // 找到目标元素,返回其索引
        return m;
    }
}

/* 二分查找 */
int binarySearch(vector<int> &nums, int target) {
    int n = nums.size();
    // 求解问题 f(0, n-1)
    return dfs(nums, target, 0, n - 1);
}

3. 构建二叉树问题

给定一个二叉树的前序遍历和中序遍历,请从中构建二叉树,返回二叉树的根节点

在这里插入图片描述

3.1 如何划分子树

  • 前序遍历和中序遍历都可被划分为三个部分

    • 前序遍历:[ 根节点 | 左子树 | 右子树 ] ,如上图的树对应 [ 3 | 9 | 2 1 7 ]
    • 中序遍历:[ 左子树 | 根节点 | 右子树 ] ,如上图的树对应 [ 9 | 3 | 1 2 7 ]
  • 以上图数据为例,可以通过下图所示的步骤得到划分结果

    • 前序遍历的首元素 3 是根节点的值
    • 查找根节点 3 在中序遍历中的索引,利用该索引可将中序遍历划分为 [ 9 | 3 | 1 2 7 ]
    • 根据中序遍历划分结果,易得左子树和右子树的节点数量分别为 1 和 3,从而可将前序遍历划分为 [ 3 | 9 | 2 1 7 ]

在这里插入图片描述

3.2 基于变量描述子树区间

  • 根据以上划分方法,已经得到根节点、左子树、右子树在前序遍历和中序遍历中的索引区间。而为了描述这些索引区间,需要借助几个指针变量

    • 将当前树的根节点在前序遍历中的索引记为 i
    • 将当前树的根节点在中序遍历中的索引记为 m
    • 将当前树在中序遍历中的索引区间记为 [l, r]
  • 如下图所示,通过以上变量即可表示根节点在前序遍历中的索引,以及子树在中序遍历中的索引区间

    • 右子树根节点索引中的 m-l 含义是 “左子树的节点数量”

在这里插入图片描述

在这里插入图片描述

/* 构建二叉树:分治 */
// 时间复杂度:O(n)
// 空间复杂度:O(n)
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
    // 子树区间为空时终止
    if (r - l < 0)
        return NULL;
    // 初始化根节点
    TreeNode *root = new TreeNode(preorder[i]);
    // 查询 m ,从而划分左右子树
    int m = inorderMap[preorder[i]];
    // 子问题:构建左子树
    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
    // 子问题:构建右子树
    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
    // 返回根节点
    return root;
}

/* 构建二叉树 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
    // 初始化哈希表,存储 inorder 元素到索引的映射
    unordered_map<int, int> inorderMap;
    for (int i = 0; i < inorder.size(); i++) {
        inorderMap[inorder[i]] = i;
    }
    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
    return root;
}

在这里插入图片描述

4. 汉诺塔问题

  • 给定三根柱子,记为 A、B 和 C 。起始状态下,柱子 A 上套着 n 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 n 个圆盘移到柱子 C 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则。
    • 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入
    • 每次只能移动一个圆盘
    • 小圆盘必须时刻位于大圆盘之上

将规模为 i 的汉诺塔问题记做 f(i)。例如 f(3) 代表将 3 个圆盘从 A 移动至 C 的汉诺塔问题

在这里插入图片描述

4.1 考虑基本问题

  • 对于问题 f(1),即当只有一个圆盘时,将它直接从 A 移动至 C 即可

在这里插入图片描述

  • 对于问题 f(2),即当有两个圆盘时,由于要时刻满足小圆盘在大圆盘之上,因此需要借助 B 来完成移动
    • 先将上面的小圆盘从 A 移至 B
    • 再将大圆盘从 A 移至 C
    • 最后将小圆盘从 B 移至 C

其中,C 称为目标柱、B 称为缓冲柱

在这里插入图片描述

4.2 子问题分解

  • 对于问题 f(3),即当有三个圆盘时,因为已知 f(1) 和 f(2) 的解,所以可从分治角度思考,将 A 顶部的两个圆盘看做一个整体,执行下图所示的步骤。这样三个圆盘就被顺利地从 A 移动至 C 了
    • 令 B 为目标柱、C 为缓冲柱,将两个圆盘从 A 移动至 B
    • 将 A 中剩余的一个圆盘从 A 直接移动至 C
    • 令 C 为目标柱、A 为缓冲柱,将两个圆盘从 B 移动至 C

在这里插入图片描述

  • 本质上看,将问题 f(3) 划分为两个子问题 f(2) 和子问题 f(1)。按顺序解决这三个子问题之后,原问题随之得到解决。这说明子问题是独立的,而且解是可以合并的。至此,可总结下图所示的汉诺塔问题的分治策略:将原问题 f(n) 划分为两个子问题 f(n-1) 和一个子问题 f(1),并按照以下顺序解决这三个子问题
    • 将 n-1 个圆盘借助 C 从 A 移至 B
    • 将剩余 1 个圆盘从 A 直接移至 C
    • 将 n-1 个圆盘借助 A 从 B 移至 C

在这里插入图片描述

对于这两个子问题 f(n-1),可以通过相同的方式进行递归划分,直至达到最小子问题 f(1)。而 f(1) 的解是已知的,只需一次移动操作即可

4.3 代码实现

/* 移动一个圆盘 */
void move(vector<int> &src, vector<int> &tar) {
    // 从 src 顶部拿出一个圆盘
    int pan = src.back();
    src.pop_back();
    // 将圆盘放入 tar 顶部
    tar.push_back(pan);
}

/* 求解汉诺塔:问题 f(i) */
// dfs() 作用是将柱 src 顶部的 i 个圆盘借助缓冲柱 buf 移动至目标柱 tar
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
    // 若 src 只剩下一个圆盘,则直接将其移到 tar
    if (i == 1) {
        move(src, tar);
        return;
    }
    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf
    dfs(i - 1, src, tar, buf);
    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar
    move(src, tar);
    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar
    dfs(i - 1, buf, src, tar);
}

/* 求解汉诺塔 */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
    int n = A.size();
    // 将 A 顶部 n 个圆盘借助 B 移到 C
    dfs(n, A, B, C);
}
  • 如下图所示,汉诺塔问题形成一个高度为 n 的递归树,每个节点代表一个子问题、对应一个开启的 dfs() 函数,因此时间复杂度为 O ( 2 n ) O(2^n) O(2n),空间复杂度为 O ( n ) O(n) O(n)

在这里插入图片描述

5. 回溯算法

  • 回溯算法是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止
    • 回溯算法通常采用 “深度优先搜索” 来遍历解空间
    • 前序、中序和后序遍历都属于深度优先搜索

例题一:给定一个二叉树,搜索并记录所有值为 7 的节点,请返回节点列表

/* 前序遍历:例题一 */
void preOrder(TreeNode *root) {
    if (root == nullptr) {
        return;
    }
    if (root->val == 7) {
        // 记录解
        res.push_back(root);
    }
    preOrder(root->left);
    preOrder(root->right);
}

在这里插入图片描述

5.1 尝试与回退

  • 之所以称之为回溯算法,是因为该算法在搜索解空间时会采用 “尝试” 与 “回退” 策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,回退到之前的状态,并尝试其他可能选择

  • 对于例题一,访问每个节点都代表一次 “尝试”,而越过叶结点或返回父节点的 return 则表示 “回退”,回退并不仅仅包括函数返回

例题二:在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径

  • 在例题一代码的基础上,需要借助一个列表 path 记录访问过的节点路径。当访问到值为 7 的节点时,则复制 path 并添加进结果列表 res 。遍历完成后,res 中保存的就是所有的解
/* 前序遍历:例题二 */
void preOrder(TreeNode *root) {
    if (root == nullptr) {
        return;
    }
    // 尝试
    path.push_back(root);
    if (root->val == 7) {
        // 记录解
        res.push_back(path);
    }
    preOrder(root->left);
    preOrder(root->right);
    // 回退
    path.pop_back();
}

在这里插入图片描述

5.2 剪枝

  • 复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于 “剪枝”

例题三:在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的节点

  • 为满足以上约束条件,需要添加剪枝操作:在搜索过程中,若遇到值为 3 的节点,则提前返回并停止搜索
/* 前序遍历:例题三 */
void preOrder(TreeNode *root) {
    // 剪枝
    if (root == nullptr || root->val == 3) {
        return;
    }
    // 尝试
    path.push_back(root);
    if (root->val == 7) {
        // 记录解
        res.push_back(path);
    }
    preOrder(root->left);
    preOrder(root->right);
    // 回退
    path.pop_back();
}

在这里插入图片描述

5.3 框架代码

/* 回溯算法框架 */
// state 表示问题的当前状态,choices 表示当前状态下可以做出的选择
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (Choice choice : choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}

5.4 常用术语

在这里插入图片描述

5.5 优缺点

  • 回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解
    • 这种方法的优势在于它能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率
    • 在处理大规模或者复杂问题时,回溯算法的运行效率可能难以接受
      • 时间:回溯算法通常需要遍历状态空间的所有可能,时间复杂度可以达到指数阶或阶乘阶
      • 空间:在递归调用中需要保存当前的状态(例如路径、用于剪枝的辅助变量等),当深度很大时,空间需求可能会变得很大
    • 常见的效率优化方法
      • 剪枝:避免搜索那些肯定不会产生解的路径,从而节省时间和空间
      • 启发式搜索:在搜索过程中引入一些策略或者估计值,从而优先搜索最有可能产生有效解的路径

6. N 皇后问题

问题:根据国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。给定 n 个皇后和一个 n×n 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案

  • 下图所示:当 n = 4 时,共可以找到两个解。从回溯算法的角度看,n×n 大小的棋盘共有 n 2 n^2 n2 个格子,给出了所有的选择 choices 。在逐个放置皇后的过程中,棋盘状态在不断地变化,每个时刻的棋盘就是状态 state

在这里插入图片描述

  • 下图展示了本题的三个约束条件:多个皇后不能在同一行、同一列、同一对角线
    • 值得注意的是,对角线分为主对角线 \ 和次对角线 / 两种

在这里插入图片描述

6.1 逐行放置策略

  • 皇后的数量和棋盘的行数都为 n,因此容易得到一个推论:棋盘每行都允许且只允许放置一个皇后。也就是说,可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束

  • 下图所示,为 4 皇后问题的逐行放置过程

    • 受画幅限制,下图仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝
    • 本质上看,逐行放置策略起到了剪枝的作用,它避免了同一行出现多个皇后的所有搜索分支

在这里插入图片描述

6.2 列与对角线剪枝

  • 为满足列约束,可以利用一个长度为 n 的布尔型数组 cols 记录每一列是否有皇后。在每次决定放置前,通过 cols 将已有皇后的列进行剪枝,并在回溯中动态更新 cols 的状态

  • 那么,如何处理对角线约束呢?

    • 设棋盘中某个格子的行列索引为 (row, col),选定矩阵中的某条主对角线,发现该对角线上所有格子的行索引减列索引都相等,即对角线上所有格子的 row - col 为恒定值
    • 也就是说,如果两个格子满足 r o w 1 − c o l 1 = r o w 2 − c o l 2 row_1 - col_1 = row_2 - col_2 row1col1=row2col2,则它们一定处在同一条主对角线上。利用该规律,可以借助下图所示的数组 diag1,记录每条主对角线上是否有皇后
    • 同理,次对角线上的所有格子的 row + col 是恒定值,同样也可以借助数组 diag2 来处理次对角线约束

6.3 代码实现

  • n 维方阵中 row - col 的范围是 [-n+1, n-1],row + col 的范围是 [0, 2n-2],所以主对角线和次对角线的数量都为 2n-1,即数组 diag1 和 diag2 的长度都为 2n-1
    /* 回溯算法:N 皇后 */
    // 时间复杂度:O(n!),逐行放置 n 次,考虑列约束,则从第一行到最后一行分别有 n、n-1、...、2、1 个选择
    // 空间复杂度:O(n^2),数组 state 使用 O(n^2)空间,数组 cols、diags1 和 diags2 皆使用 O(n) 空间
    void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res,
                   vector<bool> &cols, vector<bool> &diags1, vector<bool> &diags2) {
        // 当放置完所有行时,记录解
        if (row == n) {
            res.push_back(state);
            return;
        }
        // 遍历所有列
        for (int col = 0; col < n; col++) {
            // 计算该格子对应的主对角线和副对角线
            int diag1 = row - col + n - 1;
            int diag2 = row + col;
            // 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
            if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
                // 尝试:将皇后放置在该格子
                state[row][col] = "Q";
                cols[col] = diags1[diag1] = diags2[diag2] = true;
                // 放置下一行
                backtrack(row + 1, n, state, res, cols, diags1, diags2);
                // 回退:将该格子恢复为空位
                state[row][col] = "#";
                cols[col] = diags1[diag1] = diags2[diag2] = false;
            }
        }
    }
    
    /* 求解 N 皇后 */
    vector<vector<vector<string>>> nQueens(int n) {
        // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
        vector<vector<string>> state(n, vector<string>(n, "#"));
        vector<bool> cols(n, false);           // 记录列是否有皇后
        vector<bool> diags1(2 * n - 1, false); // 记录主对角线是否有皇后
        vector<bool> diags2(2 * n - 1, false); // 记录副对角线是否有皇后
        vector<vector<vector<string>>> res;
    
        backtrack(0, n, state, res, cols, diags1, diags2);
    
        return res;
    }
    

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

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

相关文章

【c++Leetcode】141. Linked List Cycle

问题入口 思想&#xff1a;Floyds Tortoise and Hare 这个算法简单来说就是设置一个慢指针&#xff08;一次移动一个位置&#xff09;和一个快指针&#xff08;一次移动两个位置&#xff09;。在遍历过程中&#xff0c;如果慢指针和快指针都指向同一个元素&#xff0c;证明环…

pycharm无法加载第三方库问题解决

pycharm无法加载第三方库 1、问题展示 2、在下面窗口点击转到工具窗口&#xff0c;pycharm社区版没有这个选项 3、在设置中添加镜像源 4、应用即可&#xff0c;然后就可以在第3步中搜索需要的库了

如何在一个CSS文件中引入其他CSS文件

import 规则可以在一个CSS文件中引用另一个CSS文件。它的语法如下所示&#xff1a; import url("path/to/another.css");在这个例子中&#xff0c;我们使用 import 规则将另一个名为”another.css”的CSS文件引入到当前的CSS文件中。可以使用相对路径或绝对路径指定…

C# 使用 LibUsbDotNet 实现 USB 设备检测

国庆节回来后的工作内容&#xff0c;基本都在围绕着各种各样的硬件展开&#xff0c;这无疑让本就漫长的 “七天班” &#xff0c;更加平添了三分枯燥&#xff0c;我甚至在不知不觉中学会了&#xff0c;如何给打印机装上不同尺寸的纸张。华为的 Mate 60 发布以后&#xff0c;人群…

RabbitMQ 消息模型

参考 ​​​​​​【RabbitMQ】RabbitMQ架构模型_rabbitmq结构模型-CSDN博客 之前的学习都只是知道名字&#xff0c;但并没有真正的理解&#xff0c;每次看还是不懂&#xff0c;所以今日理解透 &#xff01; RabbitMQ 收发消息过程如下&#xff1a; 首先从消费者开始&#xff1…

云南毕业旅游攻略

第一站&#xff1a;长沙-大理 大理景点推荐 苍山&#xff1a;大理的最佳观景台&#xff0c;拥有变幻万千的云景和素负盛名的大理“风花雪月”四景之一的苍山雪景。可以乘坐索道上山&#xff0c;观赏珍珑棋局、清碧溪、七龙女池、苍山大峡谷、玉带云游路等。洱海&#xff1a;大理…

文件的逻辑结构(顺序文件,索引文件)

所谓的“逻辑结构”&#xff0c;就是指在用户看来&#xff0c;文件内部的数据应该是如何组织起来的。 而“物理结构”指的是在操作系统看来&#xff0c;文件的数据是如何存放在外存中的。 1.无结构文件 无结构文件:文件内部的数据就是一系列二进制流或字符流组成。无明显的逻…

ChatGPT的狂飙之路

ChatGPT的狂飙之路 第一章&#xff1a;AI顶流-闪耀互联网世界的新宠 根据UBS发布的研究报告显示&#xff0c;ChatGPT在1月份的月活跃用户数已达1亿&#xff0c;成为史上用户数增长最快的消费者应用。TikTok在全球上线后花了大约9个月的时间才增加了1亿用户&#xff0c;而Inst…

冰箱监控温度需要安装温度采集器需要什么条件

冰箱监控温度需要安装一个温度采集器在冰箱内部&#xff0c;以实时监测冰箱的温度。采集器可以是数字温度传感器、热敏电阻或其他类型的温度传感器。 当然也需要安装信号中继器也就是我们的智能网关&#xff0c;用于接收和记录温度采集器的数据。这一套系统就是温度监控系统&am…

【毕设必备】手把手带你用Python搭建一个简单的后端服务- API的创建,前后端交互的数据传递,GET,POST,JSON,FLASK

目录 Python 介绍Python的特性Python的使用场景python基本语法 FlaskViewModelControlhtmlsimple api连接数据库 跨域 Mojo比python快68000倍相关链接 Python 介绍 Python是一种流行的高级编程语言&#xff0c;具有易于学习和使用的特性&#xff0c;被广泛应用于各种领域。 P…

移动硬盘被格式化了如何恢复数据?四步教你如何恢复

在日常生活中&#xff0c;我们常常会使用各种存储设备来保存和备份我们的重要数据。移动硬盘作为一种便携式的存储设备&#xff0c;被广泛应用于数据的存储和传输。然而&#xff0c;有时候我们会不小心将移动硬盘格式化&#xff0c;从而丢失了里面的数据。本文将介绍移动硬盘格…

如何通过沉浸式投影技术提升文旅夜游的互动体验?

伴随着国民经济的提升&#xff0c;文旅夜游市场也开始通过各类创新设计形式&#xff0c;来吸引更多的游客前来打卡游玩&#xff0c;使其逐渐成为了当下热度较高的一种游玩模式&#xff0c;其中在收集各类用户的体验反馈时&#xff0c;沉浸式投影依靠新颖的视觉体验以及沉浸式观…

安装Apache2.4

二、安装配置Apache&#xff1a; 中文官网&#xff1a;Apache 中文网 官网 (p2hp.com) 我下的是图中那个版本&#xff0c;最新的64位 下载下后解压缩。如解压到D:\Program Files\Apache\Apache24 PS&#xff1a;特别要注意使用的场景和64位还是32位版本 2、修改Apcahe配置文…

2023.10.18

头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);~Widget();private slot…

电商数据采集的10个经典方法

电商数据采集的网页抓取数据、淘宝、天猫、京东等平台的电商数据抓取&#xff0c;网页爬虫、采集网站数据、网页数据采集软件、python爬虫、HTM网页提取、APP数据抓包、APP数据采集、一站式网站采集技术、BI数据的数据分析、数据标注等成为大数据发展中的热门技术关键词。那么电…

什么是零拷贝

普通拷贝流程 在实际应用中&#xff0c;如果我们需要把磁盘中的某个文件内容发送到远程服务器上&#xff0c;那么它必须要经过几个拷贝的过程&#xff0c;。从磁盘中读取目标文件内容拷贝到内核缓冲区&#xff0c;CPU 控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中&…

全志R128外设模块配置——ADC按键配置方法

ADC 按键配置方法 FreeRTOS平台上使用的按键为ADC-KEY&#xff0c;采用的ADC模块为GPADC。 按键功能驱动的实现是通过ADC分压&#xff0c;使每个按键检测的电压值不同&#xff0c;从而实现区分不同的按键。按下或者弹起中断之后&#xff0c;通过中断触发&#xff0c;主动检测…

电子技术基础之一(电容和电感)

Electronic Techonolgy 1、电容和电感1.1、电容(Capacitor)1.1.1、滤波功能1.1.2、储能功能 1.2、电感(Inductor)1.2.1、楞次定律1.2.2、储能作用 1、电容和电感 先讲一个概念&#xff0c;电流分为直流电和交流电&#xff0c;其中直流电再分为稳定直流电和脉动直流电。 直流电…

如何使用VSCode将iPad Pro转化为功能强大的开发工具?

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. iPad通过软件远程vscode6.1 创建TCP隧道 7. ipad远…

SpringBoot (3) Profiles,外部化配置,自定义starter

目录 1 Profiles 1.1 "组件"环境隔离 1.1.1 标识环境 1.1.2 激活环境 1.2 "配置"环境隔离 1.2.1 添加"副配置文件" 1.2.2 激活环境 2 外部化配置 2.1 配置优先级 2.2 快速部署 3 自定义starter 3.1 基本抽取 3.1.1 导yaml提示包 3…