Disjoint-set data structure--并查集

news2025/1/10 22:57:38

Disjoint-set data structure

不相交集, 通常称作并查集的一种数据结构。

  1. 应用范围:处理不相交集合的合并查询问题,它在处理这两种的时间复杂度在实际应用上往往认为是 O ( 1 ) O(1) O(1),稍后本篇会略加说明。
  2. 接受两种操作:判断两元素是否在同一集合里(isSameSet), 合并两个不相交的集合(Union)。
    3.应用算法: 无向图的连通分量, 网络连接 , 图的最小生成树算法(Kruskal)。

不过这里我们直接说明优化版本的不相交集, 舍去一些繁琐的步骤, 把最佳的方式呈现出来。
优化方式: 路径压缩和按秩合并。-----不过这些篇幅在靠后, 前面我们还得为并查集铺垫一会儿。

前言

学习难度:完成初阶数据结构 - 重点哈希表。 略微涉及图的术语和表示(若您看过有关概念,学习过离散数学,那么您不会有任何障碍。否则请自行)
编程语言:Java为主, Python(方便引入举例, 会比较详细地说明)–C语言版本未写

序幕

首先,我们先要把单个对象封装成集合, isSameSetUnion ,都是作为集合之间的运算。
也许你可以实现这一样一个接口makeSet ,将单个对象封装成只要该对象的集合。
为什么要这样做?而不是单独地就认为它是一个集合
逻辑上简单处理:无论对象的类型是什么,并查集永远地对自身内的集合操作,避免了不必要的类型检查。
将单个对象弄成集合,避免了单个对象和多对象集合之间操作的讨论。
其次,为每个单对象建立一个新集合,可以确定集合之间肯定不相交,因为它们在内存中分配了不同的地址。假设不这么做,如一个对象就是一个单独的字符串"Malan",另一个对象是字符串"Malan",但认为两者是不同的人,可能重名了,执行合并操作时。可能会出现误判的情况,即两个相同的字符串被认为是同一个对象,从而错误地合并它们,而封装成集合就没事了。
对象是什么类型不关心,因为内部接口始终操作的是自定义的集合类型element,这也给并查集带来极强的拓展性和更方便维护。

  1. 下面解释isSameSet这个函数,判断两个元素是否在同一集合里。
    isSameSet(V x,V y),传递两个对象,如果这两个对象在现有集合里,那么判断两个集合是否是一个。
    可能有点抽象,不过下面会配图理解。

  2. union操作, 将包含两个对象的集合合并在一起(如果它们在集合里,且不在同一个集合里)。假设x对象对应的集合有 S x S_x Sx,y对象对应集合是 S y S_y Sy, 那么相当于 S x U S y S_x U S_y SxUSy。, 一般通过,销毁其中一个集合,把集合元素放入另一个集合 。

  3. 下面我们谈谈如何设计集合, 当我们为单个元素执行makeSet操作时,需要一个指针指向自身。
    当我们连接两个集合时,只要更改指针指向即可。
    每个集合均有一个代表元素,类似树的根节点, 判断两个对象是否在同一集合就是看其代表元素根节点是不是同一个。

下面举一个例子理解。
并查集动画

举例

图1a
图片摘自算法导论
并查集求解图的连通分量
连通分量(Connected Components)是图论中的一个重要概念, 图1a是原图,那么从左往右的三部分就是一个一个的连通分量。因此, 连通分量是原图的子图, 原图是不连通的, 而其各个顶点两两连通的最大子图就是它其中之一的连通分量,图·1a的连通分量数量有三个。
无向图中,一个连通分量是一个极大连通子图。

显然,若原图本身就是一个连通图, 那么连通分量就是它本身。
接下来,方便叙述,我们采用python实现一下。
这里用到了图的邻接表表示,这里python一个字典(哈希表)就能实现。
表示图1a

graph = {
    'a': ['b', 'c'],
    'b': ['a', 'c', 'd'],
    'c': ['a', 'b'],
    'd': ['b'],
    'e': ['f', 'g'],
    'f': ['e'],
    'g': ['f'],
    'h': ['i'],
    'i': ['h'],
    'j': []
}

忽略并查集的实现, 求解连通分量

# 实现一个函数,统计图所有连通分量的个数
# @param: graph-图
def count_connected_components(graph):
    # 将图的所有顶点(即键key的集合)传参,并查集初始化其为一个个集合
    uf = UnionFind(graph.keys())

    # 合并关联的所有顶点
    for v in graph:
        for nei in graph[v]:
            uf.union(v, nei)

    # 将各个连通分量加入集合中
    root_set = set()
    for v in graph:
        root_set.add(uf.find(v))
    
    # 返回连通分量的个数
    return len(root_set)
  1. python解释:
uf = UnionFind(graph.keys())
  1. 这里未提供UnionFind这个类的实现细节。
    graph.keys() 是python字典(内置哈希表)的一个方法, 这里返回所有的键,它是一个字典视图对象, 而不是一个所谓的动态数组(列表), 它会根据内容改变实时更新。
  2. 如同上面序幕所说, ′ a ′ 'a' a 成为了 { a } \{ a \} {a} , ′ b ′ 'b' b成为了 { b } \{b \} {b}等等。
# 合并关联的所有顶点
    for v in graph:
        for nei in graph[v]:
            uf.union(v, nei)
  1. 依次遍历每个顶点,将每个顶点与它相邻的顶点合并成一个集合。
    或许你可能问, 万一有重复的两顶点关联呢?别担心union方法内部细节已经判断了重复的情形。—不过具体实现union在下文展开。
 # 将各个连通分量加入集合中
    root_set = set()
    for v in graph:
        root_set.add(uf.find(v))

set():创建一个空集合, 集合的特点是不允许添加重复元素。
依次遍历图的顶点, 并取该顶点所在集合的代表元素(根节点)。
比如:a,b,c,d的代表元素假设是a, 那么调用uf.find(v)的结果都是a,那么相当于set只记录一次。 同理,e,f,g的代表元素假设为e,那么这三个点调用find方法结果都是e,那么set也只记录一次。后面,h,i为1次, j是单独的孤立点也记录一次。 那么set里面有4个元素。
# 返回连通分量的个数 return len(root_set),那么对于这个图1a的连通分量结果为4,显然也是符合图中的结果。
结果

实现

🆗,举例完成,让我们讨论一下实现细节。

列表版本的并查集

事先声明,这是一个糟糕的设计。
集合列表(List of Sets)来实现并查集。
有关于python中的set。

class SetUnionFind:
		# 将对象们转化成单元素集合,挨个存放在列表里
    def __init__(self, elements):
        self.set_list = [{element} for element in elements]
	
	# 找每个集合的代表元素
    def find_head(self, value):
		# 遍历集合列表
        for s in self.set_list:
            if value in s: # 判断对象是否在对应集合里。
                return s
        return None
	
	# 合并
    def union(self, head1, head2):

        set1 = self.find_head(head1)
        set2 = self.find_head(head2)

        if set1 != set2:
            set1.update(set2)
            self.set_list.remove(set2)
	
	# 判断两对象是否在同一集合。
    def is_same_set(self, head1, head2):
        return self.find_head(head1) == self.find_head(head2)

时间复杂度分析, 其中n输入elements个数。 m为单个集合的平均长度。

Initfind_headunionis_same_set
O ( n ) O(n) O(n) O ( n × m ) O(n\times m) O(n×m) O ( n × m ) O(n\times m) O(n×m) O ( n × m ) O(n\times m) O(n×m)

当然,这种实现方式处理小规模的合并还可以, 至于大规模数据效率非常低下。

快速查找

find_head方法查询根节点速度太慢了,所以我们尝试用哈希表实现一个快速查找的并查集。
🆗,让我们把语言切换到Java。
使用两个哈希表, 一个elementMap封装成集合,另一个fatherMap记录每个节点所在集合的代表元素。

//QuickFindUnionFindSet.java
import java.util.Collection;
import java.util.HashMap;
public class QuickFindUnionFindSet<T> {
	public static class Element<T>{
		T value;
		public Element(T value) {
			this.value = value;
		}
	}
	public HashMap<T,Element<T>> elementMap;
	public HashMap<Element<T>,Element<T>> fatherMap;
	
	public QuickFindUnionFindSet(Collection<? extends T> list) {
		//初始化哈希表
		elementMap = new HashMap<>();
		fatherMap = new HashMap<>();
		for(T x: list) {
			Element<T> elem = new Element<>(x);
			elementMap.put(x, elem);
			fatherMap.put(elem, elem);
		}
	}
	
	//查询速度O(1)
	private Element<T> findHead(T x){
		Element<T> elemX = elementMap.get(x);
		return fatherMap.get(elemX) ;
	}
	//判断速度O(1)
	public boolean isSameSet(T x, T y) {
		Element<T> elemX = elementMap.get(x);
		Element<T> elemY = elementMap.get(y);
		return fatherMap.get(elemX) == fatherMap.get(elemY);
	}
	
	//合并操作,O(n)因为要遍历哈希表
	public void union(T x, T y) {
		if(!isSameSet(x, y)) {
			Element<T> headX = findHead(x);
			Element<T> headY = findHead(y);
			for(Element<T> key: fatherMap.keySet()) {
				if(fatherMap.get(key) == headX) {
					fatherMap.put(key, headY);
				}
			}
		}
	}
}

由于fatherMap存储的是每个集合的代表元素。
findHead查询速度为 O ( 1 ) O(1) O(1),内部只是调用了哈希表获取父节点的地址, isSameSet内部调用了两次findeHead函数,时间复杂度同样是 O ( 1 ) O(1) O(1)
union将所有父节点为headX的节点更改为headB, 逻辑图来看效果等同于合并了两个集合了, 不过由于要更改所有,不得不遍历整个哈希表,所以时间复杂度是 O ( n ) O(n) O(n)

不过只追求查询速度, 可不是并查集的最终形态。

快速合并

加快合并速度。
我们重新设计fatherMap,使得其不是表示元素到根节点的映射,而是元素到其父节点的映射。

import java.util.HashMap;
import java.util.Collection;

public class QuickUnion<T> {
    // 内部类,用于表示集合中的元素
    public static class Element<T> {
        T value; // 元素的值
        public Element(T value) {
            this.value = value;
        }
    }

    // 映射:元素值到元素对象的映射
    public HashMap<T, Element<T>> elementMap;
    // 映射:元素对象到父节点的映射
    public HashMap<Element<T>, Element<T>> fatherMap;

    // 构造函数:初始化并查集
    public QuickUnion(Collection<? extends T> list) {
        elementMap = new HashMap<>();
        fatherMap = new HashMap<>();
        // 遍历输入集合,为每个元素创建一个新的 Element 对象
        // 并将其自身作为父节点
        for (T value : list) {
            Element<T> elemV = new Element<>(value);
            elementMap.put(value, elemV); // 将元素值映射到元素对象
            fatherMap.put(elemV, elemV);  // 每个元素的父节点指向自身
        }
    }

    // 查找操作:找到元素 x 的根节点
    private Element<T> findHead(T x) {
        Element<T> elem = elementMap.get(x); // 获取元素对象
        if (elem == null) {
            return null; // 如果元素不存在,返回 null
        }
        // 路径压缩:将查找路径上的所有节点直接连接到根节点
        if (fatherMap.get(elem) != elem) {
            fatherMap.put(elem, findHead(fatherMap.get(elem)));
        }
        return fatherMap.get(elem); // 返回根节点
    }

    // 判断两个元素是否在同一个集合中
    public boolean isSameSet(T x, T y) {
        return findHead(x) == findHead(y); // 比较两个元素的根节点是否相同
    }

    // 合并操作:将两个元素所在的集合合并
    public void union(T x, T y) {
        Element<T> headX = findHead(x); // 查找 x 的根节点
        Element<T> headY = findHead(y); // 查找 y 的根节点
        // 如果 x 和 y 不在同一个集合中,将 x 的根节点的父节点指向 y 的根节点
        if (headX != null && headY != null && headX != headY) {
            fatherMap.put(headX, headY);
        }
    }
}

InitfindHeadunionisSameSet
O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)

findHead由于需要循环fatherMap往上爬找到根节点, 循环次数取决于树的高度,最坏情况要走 O ( n ) − 类 似 链 表 最 坏 情 况 。 O(n)-类似链表最坏情况。 O(n)
isSameSet的时间复杂度取决于findHead,时间复杂度也是 O ( n ) O(n) O(n)
union,单论合并这个操作确实是常数时间, 但实际上调用findHead时间开销也是 O ( n ) O(n) O(n)
上述分析说明,findHead这个函数是影响整体时间的罪魁祸首。
对比篇二的QuickFind,findHead函数为常数时间,所以isSameSet`也是常数时间,但这样设计合并要遍历整个哈希表。而该篇的快速合并,合并操作确实快了但受了findHead的牵连,速度被拖累了。

结论:快速合并确实减少了Union的时间开销, 它可能是 O ( 1 ) O(1) O(1)~ O ( n ) O(n) O(n)取决于findHead函数,但论合并操作次数即使在 O ( n ) O(n) O(n)也比快速查找的Union的 O ( n ) O(n) O(n)快。
这种写法有点像哈希表实现链式结构, 合并组装成一个一个的树,从逻辑结构,它也确实是一棵树。
不过, 合并没有保证 O ( 1 ) O(1) O1,这样findHead带来的负面影响让我们很是不爽,而且查询和合并总是一优一劣,难道要我们看应用场景择一吗?

最优版本的并查集

并查集历史

  1. 初期发展
    并查集的概念最早可以追溯到20世纪60年代。1964年,Bernard A. GallerMichael J. Fischer 在一篇名为《An Improved Equivalence Algorithm》的论文中首次提出了并查集的思想。这篇论文讨论了如何更高效地处理等价关系问题,其中就提到了“合并”和“查找”操作。
  2. 优化版本
    最初的并查集并不高效,就像我们前面的快速查找和快速合并操作,不完美。后来的研究者为了提高效率开发了优化版本。
  • 路径压缩(Path Compression): 由Robert Endre Tarjan在1975年提出, 在查找操作中,将节点直接链接到根节点,从而有效缩短了路径。

  • 按秩合并(Union by Rank): 同样由Tarjan提出。该优化在合并两个集合时,将秩(即树的高度)较小的集合连接到秩较大的集合,减少了树的高度,从而加快了查找操作。

这两种优化带来了什么效果呢? 时间复杂度均为 O ( 1 ) O(1) O(1)的查找和合并操作。
事实上, 其时间复杂度为近似线性的 O ( α ( n ) ) O(α(n)) O(α(n)),其中
α ( n ) α(n) α(n)是反阿克曼函数,增长非常缓慢,在实际应用中可视为常数。
关于反阿克曼函数,关于具体数学证明可以阅读相关论文或者算法导论(第三版的在21章不相交集21.4部分)

优化过程

我们先说明快速合并这种算法, 前面说过平均调用union时间复杂度 O ( n ) O(n) O(n),因为我们总是要找一个节点的根节点,最坏情况会跑单个表的长度查找路径,同样另一个节点也要该集合的根节点。若两者不是同一集合,那么就让其中一方根节点指向另一方根节点。
为了保证最终逻辑结构中的树相对比较均衡。显然, 我们让小树连接大树,这样合并得到树高比较好,反过来想,一个大树连接小树,这树看起来就要’倒‘了(最坏来看会极度倾斜一方)。
为什么要这么做呢?
这是为了保证节点的查找路径平均摊下来减少了。

这种改进运行时间的策略叫做按秩排序:
按秩合并是一种优化并查集的数据结构的方法,其中“秩”(rank)通常表示树的高度。合并两个集合时,总是将秩较小的树合并到秩较大的树上,以防止树的高度增加过快。
简单来说,就是统计每个集合树的高度( h e i g h t height height)或者权重( s i z e size size)。
显然,这是非常方便维护的,我们只需要一个哈希表记录每个集合的权重即可。
为方便实现,我们秩采用树结点的多少来近似替代高度。

引入sizeMap这个哈希表维护集合的大小,这个代码基于原先快速合并的代码拓展的。
注意我将类名改为了WeightQuickUnion。你应该新建一个同名的.java文件。
注释部分是区别。

//WeightQuickUnion.java
import java.util.Collection;
import java.util.HashMap;

public class WeightQuickUnion<T> {
	public static class Element<T>{
		T value;
		public Element(T value) {
			this.value = value;
		}
	}
	public HashMap<T,Element<T>> elementMap;
	public HashMap<Element<T>,Element<T>> fatherMap;
	public HashMap<Element<T>, Integer> sizeMap;//维护集合权重
	
	public WeightQuickUnion(Collection<? extends T> list) {
		elementMap = new HashMap<>();
		fatherMap = new HashMap<>();
		for(T value: list) {
			Element<T> elemV = new Element<>(value);
			elementMap.put(value, elemV);
			fatherMap.put(elemV, elemV);
			sizeMap.put(elemV, 1);// 初始化集合大小为1.
		}
	}
	
	private Element<T> findHead(T x){
		Element<T> elem = elementMap.get(x);
		if(elem==null) {
			return null;
		}
		while(fatherMap.get(elem) != elem) {
			elem = fatherMap.get(elem);
		}
		return elem;
	}
	
	public boolean isSameSet(T x, T y) {
		return findHead(x) == findHead(y);
	}
	
	public void union(T x, T y) {
		if(elementMap.containsKey(x) && elementMap.containsKey(y)) {
		Element<T> headX = findHead(x);
		Element<T> headY = findHead(y);
		//比较那个树更大--权重谁最大
		Element<T> greater = sizeMap.get(headX) >= sizeMap.get(headY) ? headX:headY;
        Element<T> less = greater == headX ? headY : headX;
		if(headX!=headY) {
			fatherMap.put(less, greater);
			//更新sizeMap,--由于小的跟大的合并了, 先更新大集合的size,然后记得销毁小集合的sizeMap
			sizeMap.put(greater, sizeMap.get(greater) + sizeMap.get(less));
		}
		}
	}
}

InitfindHeadunionisSameSet
O ( n ) O(n) O(n) O ( l o g 2 n ) O(log_2n) O(log2n) O ( l o g 2 n ) O(log_2n) O(log2n) O ( l o g 2 n ) O(log_2n) O(log2n)

由于相对保证平衡了高度,合并操作时间复杂度最坏降为 O ( l o g 2 n ) O(log_2n) O(log2n)
这是由于我们让高度维持到对数级别, findHead降到了对数,因此查询和合并也均是对数时间。

对数时间固然可观, 但是完全体并查集可以做到全 O ( 1 ) O(1) O(1)的时间复杂度。

组合优化

我们已经完成了按秩排序的优化了, 下面在此基础上继续优化。
图片-路径压缩
图片演示的是并查集的另一种优化方案—路径压缩法。
回忆findHead方法每次都会向上寻址找根节点,我们可以在这个函数内部过程做些什么。
做什么呢?将该集合除根节点本身的节点的前继节点全部改为根节点。


private Element<T> findHead(T x){
		Element<T> elem = elementMap.get(x);
		if(elem==null) {
			return null;
		}
		Stack<Element<T>>  stack = new Stack<>();
		while(fatherMap.get(elem) != elem) {
			stack.push(elem);
			elem = fatherMap.get(elem);
		}
		//elem表示该集合的根节点。
		while(!stack.empty()) {
			Element<T> tmp = stack.pop();
			fatherMap.put(tmp, elem);
		}
		return elem;
	}

通过栈这一容器, 将除根结点外的所有结点的父节点改为了根节点。这意味着下次调用findHead方法,时间复杂度 O ( 1 ) O(1) O(1)。虽然第一次调用仍然是 O ( l o g 2 n ) O(log_2n) O(log2n), 但我们每次调用都作调整, 这意味该优化使得查询根节点的速度是越用越快。那么多次调用摊还这个时间消耗, 就可以认为是 O ( 1 ) O(1) O(1)

那么最终的时间复杂度呢?
在样本量可观的情况下, 时间复杂度为:

InitfindHeadunionisSameSet
O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)


前面说明并查集历史说过, 时间复杂度是 O ( α ( n ) ) O(\alpha(n)) O(α(n)),l里面是一个反阿克曼函数,这意味操作时间还是和数据规模有关,但其增长速度缓慢。
有多慢呢? ,在实际数据规模内(例如 n < = 1 0 12 n<=10^{12} n<=1012 , α ( n ) \alpha (n) α(n) 通常都不会超过 5)
何时视为常数时间?

  1. n < = 1 0 12 n<=10^{12} n<=1012 , α ( n ) \alpha (n) α(n) 通常都不会超过 5
  2. 哪怕现实可能存在的数据规模,都视为常数时间

何时将其视为对数时间?

  1. 几乎不需要。现实不可能有那么大的数据规模。
  2. 第一次调用findHead函数可以近似认为是对数时间, 但平均来看很小。

何时将其视为线性时间?

  1. 完全不需要, 现实世界不可能出现。非要形容的话,至少是 1 0 80 个 数 据 规 模 10^{80}个数据规模 1080

结论:最优版并查集的时间复杂度就认为是O(1)。

结尾

分享一下,个人初次学习并查集的代码

public class Coding_UnionFindCode {
    public static class Element<V>{
        V value;
        public Element(V value){
            this.value = value;
        }
    }
    /**
     *
     * @param <V>  元素类型
     */
    public static class UnionFindSet<V>{
        //3个哈希表搞定完结。

        //将元素封装成集合
        public HashMap<V,Element<V>> ElemMap;
        //跟踪父亲节点
        public HashMap<Element<V>,Element<V>> fatherMap;
        //存储集合的数据大小
        public HashMap<Element<V>,Integer>  sizeMap;

        /**
         * 给定一个有序表将其封装成并查集。
         * 时间复杂度:O(N).
         * @param list---有序表(实现list接口的均可)可以改成Collection接口,这样可以放栈,队列,堆了.
         */
        public UnionFindSet(List<V> list) {
            ElemMap = new HashMap<V, Element<V>>();
            fatherMap = new HashMap<Element<V>, Element<V>>();
            sizeMap = new HashMap<Element<V>, Integer>();
            for (var value : list) {
                Element<V> elem = new Element<V>(value);
                ElemMap.put(value, elem);
                fatherMap.put(elem, elem);
                sizeMap.put(elem, 1);
            }

        }

        /**
         * 寻找某个元素所在集合的根节点,同时进行路径压缩优化,提高查询效率,知道什么叫做O(1)吗。
         * @param element
         * @return
         */
        private Element findHead(Element<V> element){
            //用栈,用队列,递归都可以。
            Deque<Element<V>> queue = new ArrayDeque<>();
            while(element != fatherMap.get(element)){
                queue.offer(element);
                element = fatherMap.get(element);
            }
            //摊还数据。并查集结构越用越快。
            while(!queue.isEmpty()){
                fatherMap.put(queue.poll(),element);
            }
            return element;
        }
        public boolean isSameSet(V a,V b){
            if(ElemMap.containsKey(a) && ElemMap.containsKey(b)){
                return findHead(ElemMap.get(a)) == findHead(ElemMap.get(b));
            }
            else {
                return false;
            }
        }
        public void union(V a, V b){
            //给定两个元素必须在对应的集合里
            if(ElemMap.containsKey(a) && ElemMap.containsKey(b)){
                //找最上面的根节点
                Element<V> aF = findHead(ElemMap.get(a));
                Element<V> bF = findHead(ElemMap.get(b));
                //不相交集合,开始合并,否则跳过直接往后返回。
                if(aF != bF){
                    //小的集合挂大的集合
                    //先假设最大,代码简单
                    Element<V> greater = sizeMap.get(aF) >= sizeMap.get(bF) ? aF:bF;
                    Element<V> less = greater == aF ? bF : aF;
                    //更新小集合的上集
                    fatherMap.put(less,greater);
                    //更新sizeMap的数据。
                    sizeMap.put(greater, sizeMap.get(aF) + sizeMap.get(bF));
                    sizeMap.remove(less);
                }
            }
        }
    }

}

关于其它语言实现,如C/C++,Go?
个人精力有限,无法兼顾其它语言。
C语言需要调用自己写的通用数据结构容器, 不过框架太复杂了, 不好说明。
C++语法太复杂了, 停留在STL库就没学了, 一般都把C++当成CwithClasses用。
Go语言目前还在基本语法阶段学习。
关于并查集的再次出现
下次出现, 估计会在个人博客的最小生成树(K)算法实现吧, ()。
参考

  1. 算法导论
  2. 数据结构与算法分析-C语言描述
  3. ChatGpt—修改代码, 加注释。

最后,谢谢你看到最后。ㄟ(≧◇≦)ㄏ

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

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

相关文章

【进程间通信】管道应用场景---简易进程池

#include<iostream> #include<vector> #include<string> #include<cstring> #include<cstdlib> #include<unistd.h> #include<sys/stat.h> #include<sys/wait.h>//把5个子进程要管理起来&#xff0c;要先描述再组织 const int…

SPI驱动学习二(驱动框架)

目录 一、回顾平台总线设备驱动模型二、SPI设备驱动1. 数据结构1.1 SPI控制器数据结构1.2 SPI设备数据结构1.3 SPI设备驱动 2. SPI驱动框架2.1 SPI控制器驱动程序2.2 SPI设备驱动程序 三、SPI设备树处理过程1. SPI Master2. SPI Device3. 设备树示例4. 设备树实例4.1 使用GPIO模…

leetcode 899. Orderly Queue

原题链接 You are given a string s and an integer k. You can choose one of the first k letters of s and append it at the end of the string. Return the lexicographically smallest string you could have after applying the mentioned step any number of moves. …

Java集合类之Collection

文章目录 1 准备部分1.1 数据结构1.1.1 数组1.1.2 链表 1.2 集合是什么 2 Collection2.1 特点2.2 常用API2.3 遍历Collection的方法2.3.1 toArray方法2.2.2 iterator方法2.3.3 foreach2.3.4 总结 3 List 接口3.1 内容提要3.2 特点3.3 List的API3.3.1 listIterator方法3.3.4 sub…

【RabbitMQ应用篇】常见应用问题

1. 消息幂等性保障 1.1 幂等性介绍 幂等性&#xff1a;这个概念在数学和计算机领域中相当常见&#xff0c;表示可以被应用多次但是不会改变初始应用结果的性质。 应用程序的幂等性&#xff1a;指的是在一个应用系统中&#xff0c;重复调用多次请求&#xff08;相同参数&#…

【Python机器学习】神经网络的组成

目录 感知机 数字感知机 认识偏置 Python版神经元 “课堂时间” 有趣的逻辑学习问题 下一步 代价函数 反向传播算法 求导 误差曲面 不同类型的误差曲面 多种梯度下降算法 Keras&#xff1a;用Python实现神经网络 展望 归一化&#xff1a;格式化输入 神经网络对…

C语言 面向对象编程

注意事项 在使用面向对象编程的时候&#xff0c;我们得问自己&#xff1a;任务中有什么对象&#xff0c;对象应该怎么使用 项目中文档体系 我们可以规划一下任务得文档&#xff0c;可以为每一个对象的类单独编写源码&#xff0c;并发布对应的头文件作为接口&#xff0c;主控…

Android CCodec Codec2 (六)C2InterfaceHelper

通过前面几篇文章的学习&#xff0c;我们知道了Codec2参数结构&#xff0c;以及如何定义一个Codec2参数。接下来的几篇文章我们将简单了解上层是如何请求组件支持的参数、如何配置参数&#xff0c;以及参数是如何反射给上层的。本篇文章我们将了解接口参数实例化。 1、C2Interf…

Linux零基础到精通(二)-vmware虚拟机使用教程及Centos7操作系统安装

目录 前言Linux 操作系统运用领域vmware虚拟机安装与使用电脑硬件环境要求vmware虚拟机软件安装创建一个虚拟机配置vmware的虚拟化网络 通过vmware虚拟机安装操作系统下载Centos7系统镜像安装Centos7操作系统配置网络和主机名称信息配置系统分区软件包选择设置用户密码进入Cent…

入门Java编程的知识点—>静态方法(day11)

重点掌握final关键字特点&#xff1f;final的语法使用?重点掌握静态变量是什么&#xff1f;静态变量的语法与使用?了解方法区内存图执行过程?重点掌握静态方法是什么&#xff1f;静态方法的语法特点与使用?重点掌握常量语法如何定义与使用? final(最终) final可以用于修…

IT运维问题深度剖析与一体化解决方案探索

在当今信息化高速发展的时代&#xff0c;IT运维作为保障企业业务连续性和稳定性的关键环节&#xff0c;其重要性日益凸显。然而&#xff0c;随着企业规模的扩大和业务的复杂化&#xff0c;IT运维面临着诸多挑战和问题。本文旨在深度剖析当前IT运维中的紧迫性问题与需求&#xf…

C++学习, 指针的指针

指针的指针&#xff1a; 是一种间接寻址的形式&#xff0c;指针的指针就是将指针的地址存放在另一个指针里面。一般&#xff0c;指针包含一个变量的地址&#xff0c;当定义一个指向指针的指针时&#xff0c;第一个指针包含了第二个指针的地址&#xff0c;第二个指针指向实际值…

day35-测试之性能测试JMeter的测试报告、并发数计算和性能监控

目录 一、JMeter的测试报告 1.1.聚合报告 1.2.html报告 二、JMeter的并发数计算 2.1.性能测试时的TPS&#xff0c;大都是根据用户真实的业务数据&#xff08;运营数据&#xff09;来计算的 2.2.运营数据 2.3.普通计算方法 2.4.二八原则计算方法 2.5.计算稳定性测试并发量 2.6…

Java性能优化传奇之旅--Java万亿级性能优化之Java 性能优化传奇:热门技术点亮高效之路

💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的博客,正是这样一个温暖美好的所在。在这里,你们不仅能够收获既富有趣味又极为实…

MOELoRA —— 多任务医学应用中的参数高效微调方法

人工智能咨询培训老师叶梓 转载标明出处 在医疗场景中&#xff0c;LLMs可以应用于多种不同的任务&#xff0c;如医生推荐、诊断预测、药物推荐、医学实体识别、临床报告生成等。这些任务的输入和输出差异很大&#xff0c;给统一模型的微调带来了挑战。而且LLMs的参数众多&…

Nginx 维护与应用:最佳实践

文章目录 引言安装与基础维护macOS 上安装 NginxUbuntu 上安装 NginxCentOS 上安装 NginxWindows 上安装 Nginx查看 Nginx 运行状态与日志信息&#xff08;Linux&#xff09;版本升级与配置备份&#xff08;Linux&#xff09; Nginx 应用场景Web 服务器反向代理动静分离负载均衡…

“线程池中线程异常后:销毁还是复用?”

目录 一、验证execute提交线程池中 测试 结论 二、验证submit提交线程池中 测试 结论 三、源码解析 查看submit方法的执行逻辑 查看execute方法的执行逻辑 为什么submit方法&#xff0c;没有创建新的线程&#xff0c;而是继续复用原线程&#xff1f; 四、总结 需要说…

Android AOSP定制默认输入法为讯飞输入法

Android AOSP定制默认输入法为讯飞输入法 前言&#xff1a; ​ 最近在公司的项目中发现默认的输入法非常不好用&#xff0c;而且默认输入法中英文切换非常麻烦&#xff0c;被用户吐槽定制的AOSP镜像体验不好&#xff0c;于是查找资料&#xff0c;研究了一番&#xff0c;尝试了…

【C++】日期类函数(时间计数器)从无到有实现

欢迎来到HarperLee的学习笔记&#xff01; 博主主页传送门&#xff1a;HarperLee的博客主页 个人语录&#xff1a;他强任他强&#xff0c;清风拂山岗&#xff01; 一、前期准备 1.1 检查构造的日期是否合法 bool Date::CheckDate() {if (_month < 1 || _month > 12|| _d…

vercel免费在线部署TodoList网页应用

参考&#xff1a; TodoList网页应用&#xff1a;https://blog.csdn.net/weixin_42357472/article/details/140909096 1、项目首先上传github 直接vscode自带的上传项目&#xff0c;commit后在创建项目上传即可 2、vercel部署项目 1&#xff09;先注册 2&#xff09;impor…