【算法与数据结构】回溯算法、贪心算法、动态规划、图论(笔记三)

news2024/11/19 21:24:07

文章目录

  • 七、回溯算法
  • 八、贪心算法
  • 九、动态规划
    • 9.1 背包问题
    • 9.2 01背包
    • 9.3 完全背包
    • 9.4 多重背包
  • 十、图论
    • 10.1 深度优先搜索
    • 10.2 广度优先搜索
    • 10.3 并查集

  最近博主学习了算法与数据结构的一些视频,在这个文章做一些笔记和心得,本篇文章就写了一些基础算法和数据结构的知识点,具体题目解析会放在另外一篇文章。在学习时已经有C, C++的基础。文章附上了学习的代码,仅供大家参考。如果有问题,有错误欢迎大家留言。算法与数据结构一共有三篇文章,剩余文章可以在 【CSDN文章】晚安66博客文章索引找到。

七、回溯算法

  回溯算法也可以叫回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,有递归就有回溯,因此回溯函数就是递归函数。回溯法的本质是穷举,穷举所有可能,然后选出我们想要的答案。如果想要令回溯法更加高效一些,那就加一些剪枝操作。虽然说回溯法并不高效,但是一些问题不得不用回溯法,能用暴力搜索解出来就不错了,在剪枝,除此之外没有更高效的解法。回溯法用来及诶觉以下的几个问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

  回溯法解决的问题可以抽象为树形结构,因为回溯法解决的问题都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成树的深度。递归就要有终止条件,所以必然是一颗高度有限的树。回溯算法的伪代码:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

八、贪心算法

  贪心算法的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法经典问题有:背包问题,买卖股票的最佳时机。贪心算法没有固定的套路,说白了就是常识性推导加上举反例。贪心算法一般分为以下四个步骤:

  • 1、将问题分解为若干个子问题
  • 2、找出合适的贪心策略
  • 3、求解每一个子问题的最优解
  • 4、将局部最优解堆叠成全局最优解

九、动态规划

  动态规划(Dynamic Programming, DP),如果一个问题有很多重叠的子问题,使用动态规划是最有效的。所有动态规划总每一个状态由上一个状态推导出来,这一点就区别于贪心算法,贪心算法没有状态变量的推导,而是从局部直接选最优的。动态规划问题可以分为下面五个步骤:

  • 1、确定dp数组(dp table)以及下标的含义
  • 2、确定递推公式
  • 3、dp数组如何初始化
  • 4、确定遍历顺序
  • 5、举例推导dp数组
      在很多动态规划题目当中,确定了递推公式,题目就自然的解出来了。同时,在debug动态规划题目是,将dp数组打印出来,观察其变化是否按照自己所预想的那样。

9.1 背包问题

  对于背包问题来说,主要可以分为两个部分:背包和物品。背包的最大容量为 V V V,物品具有价值 W W W,体积 v v v以及每个物品的数量。如果根据物品数量进行分类,可以分为01背包问题,完全背包问题,多重背包问题和分组背包问题

  • 01背包:每种物品的数量只有一个;
  • 完全背包:物品数量有无数个;
  • 多重背包:不同物品的数量可以不同;
  • 分组背包:按组打包,每组最多选一个。
      对于找工作面试来说,掌握01背包,完全背包和多重背包就够了。LeetCode的题库中没有纯01背包问题,需要转化成01背包问题。

9.2 01背包

  有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。题目假设如下,背包最大重量为4。

在这里插入图片描述

  根据动态规划的五个步骤,我们首先确认dp数组的含义。假设一个二维 d p [ i ] [ j ] dp[i][j] dp[i][j]数组代表了从下标为 [ 0 − i ] [0-i] [0i]的物品里任意取,放进容量为 j j j的背包,最大价值总和。第二步确认递归公式。不放物品:当第 i i i个物品不放进去时,此时的价值和前面的相同,有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i1][j]放物品:放物品的前提是放入的物品重量不大于背包现有容量,当然这个可以用 i f if if语句控制。假设能放进去,那么放进去之后的价值 d p [ i ] [ j ] dp[i][j] dp[i][j],可以表示为 d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[i - 1][j - weight[i]] + value[i] dp[i1][jweight[i]]+value[i]。其中, d p [ i − 1 ] [ j − w e i g h t [ i ] ] dp[i - 1][j-weight[i]] dp[i1][jweight[i]]为背包容量为 j − w e i g h t [ i ] j - weight[i] jweight[i]的时候不放物品i的最大价值, v a l u e [ i ] value[i] value[i]为物品 i i i的价值。综合以上分析,我们可以得到递归公式: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i])

  第三步我们进行初始化,因为 i i i是由 i − 1 i-1 i1初始化而来的,那么我们将 d p [ i ] [ 0 ] dp[i][0] dp[i][0]初始化为0。实际上我们在构建dp数组的时候可以将二维数组中的所有元素初始化为0,而非零的值将在循环遍历中被覆盖。然后初始化第一行当 j > w e i g h t [ i ] j>weight[i] j>weight[i]时(背包可以放假物品0), d p [ 0 ] [ j ] dp[0][j] dp[0][j]应该是 v a l u e [ 0 ] value[0] value[0]。第四步,确定遍历顺序。遍历的两个维度分别是物品和背包重量,遍历物品相对比遍历背包重量更容易理解:

在这里插入图片描述

  

// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

  当然,上述的dp数组也可以写成一维滚动数组形式。下面的代码舍去了初始化的代码,舍去下标二维dp数组的下标 i i i从而变成了一维数组。主要原因是 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j]完全可以用 d p [ j ] dp[j] dp[j]来表示。二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

  举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15。如果正序遍历dp[1] = dp[1 - weight[0]] + value[0] = 15,dp[2] = dp[2 - weight[0]] + value[0] = 30。此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

  倒序就是先算dp[2]。dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0),dp[1] = dp[1 - weight[0]] + value[0] = 15。所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

class Solution {	// 一维dp数组(滚动数组形式)
public:
	int bag01(const vector<int> weight, const vector<int> value, const int bagweight) {
		vector<int> dp(vector<int>(bagweight + 1, 0));
		for (int i = 0; i < weight.size(); i++) {			// 遍历物品
			for (int j = bagweight; j >= weight[i]; j--) {			// 遍历背包容量
				dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
			}
		}
		return dp[bagweight];
	}
};

  简言之,一维dp数组和二维dp数组的区别在于一维的空间复杂度低,二维的更容易理解(初学者用二维即可)。以上的完整代码如下:

# include <iostream>
# include <vector>
using namespace std;

class Solution {
public:
	int bag01(const vector<int> weight, const vector<int> value, const int bagweight) {
		vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
		for (int j = weight[0]; j <= bagweight; j++) {		// 初始化
			dp[0][j] = value[0];
		}
		for (int i = 1; i < weight.size(); i++) {			// 遍历物品
			for (int j = 0; j <= bagweight; j++) {			// 遍历背包容量
				if (j < weight[i]) dp[i][j] = dp[i - 1][j];
				else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			}
		}
		return dp[weight.size() - 1][bagweight];
	}
};

int main() {
	Solution s1;
	vector<int> weight = { 1, 3, 4 };
	vector<int> value = { 15, 20, 30 };
	int bagweight = 4;
	int result = s1.bag01(weight, value, bagweight);
	cout << result << endl;
	system("pause");
	return 0;
}

9.3 完全背包

  完全背包问题可以描述为:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。依然假设背包最大重量为4。

在这里插入图片描述

  为了保证每个物品仅被添加一次,01背包内嵌的循环是从大到小遍历。而完全背包的物品是可以添加多次的,所以要从小到大去遍历。

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

9.4 多重背包

  有N种物品和一个容量为V的背包。第 i i i种物品最多有 M i M_i Mi件可用,每件耗费的空间是 C i C_i Ci ,价值是 W i W_i Wi。求解将哪些物品装入背包可使这些物品的耗费的空间,总和不超过背包容量,且价值总和最大。

  我们将物品数量摊开,其实可以将多重背包问题转换成01背包。例如:背包最大重量为10。物品的重量、价值和数量如下。那么可以转化成7个物品,每个物品只能用一次。这样就是一个01背包问题。因此我们在01背包的基础之上加上遍历个数即可。

在这里插入图片描述

#include<iostream>
#include<vector>
using namespace std;

class Solution {
public:
    int Multip_Bag(int bagWeight, int nItem, vector<int> weight, vector<int> value, vector<int> nums) {
        vector<int> dp(bagWeight + 1, 0);
        for (int i = 0; i < nItem; i++) { // 遍历物品
            for (int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
                // 以上为01背包,然后加一个遍历个数
                for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
                    dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                }
            }
        }
        return dp[bagWeight];
    }
};

int main() {
    int bagWeight = 10, nItem = 3;
    vector<int> weight = {1, 3, 4}, value = {15, 20, 30}, nums = {2, 3, 2};
    Solution s1;
    int result = s1.Multip_Bag(bagWeight, nItem, weight, value, nums);
    cout << result << endl;
    system("pause");
    return 0;
}

十、图论

10.1 深度优先搜索

  DFS和BFS的区别

  • 深度优先搜索(Depth First Search, DFS)是沿着一个方向搜索,不到黄河不死心,直到遇到绝境了,搜不下去了,再换方向(回溯)。
  • 广度优先搜索(Breadth First Search, BFS)是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索的方向是四面八方的,因此被称为广度优先搜索。

  因为DFS搜索就一个方向,并且需要回溯,所以用递归来实现是最方便的。二叉树遍历的递归法是DFS,而二叉树遍历的迭代法是BFS。

void dfs(参数) {
    处理节点
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

  回溯算法本质上也是一种DFS过程,DFS搜索过程可以笼统的划分为三步。一是确定输入参数;二是确定终止条件;三是处理目前搜索节点的出发路径。

void dfs(输入参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本节点所连接的其他节点) {
        处理节点;
        dfs(图,选择的节点); // 递归
        回溯,撤销处理结果
    }
}

10.2 广度优先搜索

  如果说深搜是一条路跑到黑然后再回溯的搜索方式,那么广搜就是一圈一圈的搜索过程。广搜的搜索方式就适合解决两个点之间最短路径的问题。因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

10.3 并查集

  并查集是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。并查集常用来解决连通性问题。并查集有以下两个功能:

  • 将两个元素添加到一个集合中。
  • 判断两个元素在不在同一个集合。

  设想我们将三个元素A, B,C(都是int类型)放在同一集合,其实就是将三个元素连通在一起。只需要一个一维数组来表示:father[A] = B, father[B] = C,father[C] = C。当我们使用find函数去寻找数组的元素,如果数组元素的根相同,这样就表示A与B与C连通。

// 将v,u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return; // 如果发现根相同,则说明在一个集合,不同两个节点相连直接返回
    father[v] = u;
}

  find函数通过数组下标找到数组元素,一层一层寻根过程,代码如下:

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u; // 如果根就是自己,直接返回
    else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}

  find函数寻根的过程是通过递归的方式,不断获取father数组下标对应的数值,最终找到集合的根。而这很像在一个多叉树中,从叶子节点出发,找到根节点的过程。如果说这颗树的高度很深,每次寻根需要递归很多次。

在这里插入图片描述

  而我们的目的是需要知道这些节点在同一个根下就可以,因此让多茶树的叶子节点直接指向根即可,每次寻根只需要一次。

在这里插入图片描述

  要实现这样的路径压缩过程,只需要在递归过程中,让father[u]接住 递归函数 find(father[u])的返回结果。这样是让u的父节点直接变成find函数返回的根节点。进一步可以用三元表达式精简代码。

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u;
    else return father[u] = find(father[u]); // 路径压缩
}
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}

  同时,father数组初始化的时候要令 father[i] = i,默认指向自己。

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

  如果通过find函数找到两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

  结合以上加入并查集join、寻根find、初始化init和判断是否同一集合isSame函数,我们就得到一个并查集完整模板

int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不同两个节点已经相连,直接返回
    father[v] = u;
}

  总结下来,并查集主要具有三个功能:

  • 1、寻找根节点函数find(int u),也就是判断这个节点的祖先节点是哪个;
  • 2、将两个节点接入到同一集合,join(int u, int v)函数,可以将两个节点连接在同一根节点上;
  • 3、判断两个节点是否在同一集合中,使用isSame(int u, int v)函数,就是判断两个节点是不是同一个根节点。

  复杂度分析

  • 时间复杂度: O ( 1 ) O(1) O(1),真实的复杂度在 O ( l o g n ) O(logn) O(logn) O ( 1 ) O(1) O(1)之间,且随着查询或者合并操作的增加,时间复杂度越来越趋近于 O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( n ) O(n) O(n), 申请一个father数组。

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

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

相关文章

HTML5和CSS3提高

一、HTML5的新特性 增加了一些新的标签&#xff0c;新的表单&#xff0c;新的表单属性&#xff0c;IE9以上版本的浏览器才支持 注意&#xff1a; 这些语义化标准主要针对搜索引擎的 新标签可以使用多次 在IE9中需要把这些元素转化为块级元素 新增的多媒体标签 主要包含两个…

Vue事件处理之v-on

1. 使用及定义 定义方法 function 方法名称(接受的event或是什么都不写) {//不管方法后括号内写与不写event,都可以接受到方法内表达式 }//定义一个接受参数的方法,此时也会在传入event function 方法名称(传入参数) {//可接受传入参数与event方法内表达式 } //定义一个接受参…

DevOps 周期的 6 个 C

中型到大型软件开发项目涉及许多人员、多个团队、资源、工具和开发阶段。它们都需要以某种方式进行管理和简化&#xff0c;不仅可以获得所需的产品&#xff0c;而且还要确保将来在不断变化的环境下易于管理和维护。组织通常遵循许多项目管理模型和技术。DevOps 是其中之一&…

如何通过Jenkins进行自动化构建项目

1. 介绍 Jenkins 是一个开源的持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;工具&#xff0c;旨在自动化软件开发过程中的构建、测试和部署。它是一个基于 Java 编写的自动化服务器&#xff0c;在软件开发生命周期的各个阶段提供自动化支持。 官方…

ARM服务器部署Kafka集群

安装前必备的条件是: (1)安装jdk(提供环境); (2)安装zookeeper(注册kafka信息); 需要这方面信息的可以查看我之前写的文档; 一.下载安装包 Kafka官网下载地址 Apache Kafka 根据自己需要下载相应的版本 目前最新的版本是3.6.1。 二.解压安装包 服务器上传下载好的kafk…

MIT6.S081学习——二、相关命令行整理

MIT6.S081学习——二、相关命令行整理 1 添加user代码到xv6中并编译2 git版本管理 1 添加user代码到xv6中并编译 问题&#xff1a;如何让在xv6中运行copy.c 答&#xff1a;在xv6中运行copy.c文件&#xff0c;你需要先将该文件添加到xv6源代码目录中&#xff0c;然后修改Makefil…

备战蓝桥杯---动态规划(应用3之空间优化)

话不多说&#xff0c;直接看题&#xff1a; 我们不妨把问题抽象一下&#xff1a; 首先&#xff0c;我们由裴蜀定理知道如果两个数互质&#xff0c;那么axbyc一定有整数解&#xff08;只要c为1的倍数也就是整数&#xff09;&#xff0c;因此问题就转换为求选一些数使他们gcd1&a…

240Hz高刷电竞显示器 - HKC VG253KM

&#x1f389;&#x1f389;&#x1f389; 各位电竞爱好者们&#xff0c;今天给大家带来一款神秘武器&#xff0c;一款能够让你在游戏中大展拳脚的高刷电竞显示器 - HKC VG253KM&#xff01;&#x1f525;&#x1f525;&#x1f525; 这款显示器&#xff0c;哎呀&#xff0c;真…

windows前后端项目部署

装好windows虚拟机 1.远程连接 计算机右击属性&#xff0c;高级防火墙设置&#xff0c;远程连接服务允许 2.安装jdk,tomcat&#xff0c;解压工具 把安装包拖进去 双击安装解压软件 jdk安装 双击安装 配置环境变量&#xff08;复制jdk路径&#xff09; 计算机右击属性高级…

Unity 2021.3发布WebGL设置以及nginx的配置

使用unity2021.3发布webgl 使用Unity制作好项目之后建议进行代码清理&#xff0c;这样会即将不用的命名空间去除&#xff0c;不然一会在发布的时候有些命名空间webgl会报错。 平台转换 将平台设置为webgl 设置色彩空间压缩方式 Compression Format 设置为DisabledDecompre…

Linux学习方法-框架学习法——Linux驱动架构的演进

配套视频学习链接&#xff1a;https://www.bilibili.com/video/BV1HE411w7by?p4&vd_sourced488bc722b90657aaa06a1e8647eddfc 目录 Linux驱动演进的过程 Linux驱动的原始架构(Linux V2.4) 平台总线架构(platform) Linux设备树 Linux驱动演进的趋势 Linux驱动演进的过程…

python 3.7.3的安装

参考 Linux安装Python3.7-良许Linux教程网 (lxlinux.net) 1、Index of /ftp/python/3.7.9/ 1、安装gcc&#xff0c;yum -y install gcc 2、 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel…

Redis篇之缓存雪崩、击穿、穿透详解

学习材料&#xff1a;https://xiaolincoding.com/redis/cluster/cache_problem.html 缓存雪崩 什么是缓存雪崩 在面对业务量较大的查询场景时&#xff0c;会把数据库中的数据缓存至redis中&#xff0c;避免大量的读写请求同时访问mysql客户端导致系统崩溃。这种情况下&#x…

【前端素材】推荐优质后台管理系统Frest平台模板(附源码)

一、需求分析 定义 后台管理系统是一种用于管理和控制网站、应用程序或系统后台操作的软件工具&#xff0c;通常由授权用户&#xff08;如管理员、编辑人员等&#xff09;使用。它提供了一种用户友好的方式来管理网站或应用程序的内容、用户、数据等方面的操作&#xff0c;并…

【SpringCloudAlibaba系列--OpenFeign组件】OpenFeign的配置、使用与测试以及OpenFeign的负载均衡

步骤一 准备两个服务&#xff0c;provider和consumer 本文使用kotlin语言 provider是服务的提供者&#xff0c;由provider连接数据库 RestController RequiredArgsConstructor RequestMapping("/provider/depart") class DepartController(private val departServ…

使用Postman和JMeter进行signature签名

一、前言 ​ 有些接口的请求会带上sign&#xff08;签名&#xff09;进行请求&#xff0c;各接口对sign的签名内容、方式可能不一样&#xff0c;但一般都是从接口的入参中选择部分内容组成一个字符串&#xff0c;然后再进行签名操作, 将结果赋值给sign; 完整规范的接口文档都会…

对Redis锁延期的一些讨论与思考

上一篇文章提到使用针对不同的业务场景如何合理使用Redis分布式锁&#xff0c;并引入了一个新的问题 若定义锁的过期时间是10s&#xff0c;此时A线程获取了锁然后执行业务代码&#xff0c;但是业务代码消耗时间花费了15s。这就会导致A线程还没有执行完业务代码&#xff0c;A线程…

C++案例->评委打分、员工分组

#include<iostream> using namespace std; #include<vector> #include<string> #include<deque> #include<algorithm> #include<ctime> /* 有5名选手&#xff1a;选手ABCDE&#xff0c;10个评委分别对每一名选手打分&#xff0c;去除最高分…

ACL权限管理

一&#xff0c;简介 ACL是Access Control List的缩写&#xff0c;即访问控制列表。可以通过下列的实例来理解ACL的作用&#xff1a; 二&#xff0c;操作步骤 1. 添加测试目录&#xff0c;用户&#xff0c;组&#xff0c;并将用户添加到组&#xff08;创建zs,ls添加到tgroup组中…

STM32F103x 的时钟源

AHB (Advanced High-performance Bus) 高速总线&#xff0c;用来接高速外设的。 APB (Advanced Peripheral Bus) 低速总线&#xff0c;用来接低速外设的&#xff0c;包含APB1 和 APB2。 APB1&#xff1a;上面连接的是低速外设&#xff0c;包括电源接口、备份接口、 CAN 、 US…