DC3算法生成后缀数组详解

news2024/9/17 7:35:33

文章目录

  • 1、何为后缀数组
  • 2、暴力生成后缀数组
  • 3、用DC3算法生成后缀数组的流程
  • 4、DC3算法代码实现
    • C++版
    • Java版
  • 5、DC3算法的地位

1、何为后缀数组

假设有一个字符串 “aabaabaa”,从每个位置开始往后到最后一个位置得到的所有的「后缀字符串」”

  • 下标7开头:“a”
  • 下标6开头:“aa”
  • 下标5开头:“baa”
  • 下标4开头:“abaa”
  • 下标3开头:“aabaa”
  • 下标2开头:“baabaa”
  • 下标1开头:“abaabaa”
  • 下标0开头:“aabaabaa”

将这些后缀字符串以字典序从小到大进行排序:

  • 0:下标7开头——“a”
  • 1:下标6开头——“aa”
  • 2:下标3开头——“aabaa”
  • 3:下标0开头——“aabaabaa”
  • 4:下标4开头——“abaa”
  • 5:下标1开头——“abaabaa”
  • 6:下标5开头——“baa”
  • 7:下标2开头——“baabaa”

将这些排好序的后缀字符串中以哪个下标位置做为起点的记录在数组中,依次就是:

数组:[7, 6, 3, 0, 4, 1, 5, 2]
下标: 0  1  2  3  4  5  6  7

这个数组就是「后缀数组」。

因为后缀字符串的长度都是不同的,所以不可能存在排名相同的情况。

2、暴力生成后缀数组

假设字符串 str 长度为 N。

  1. 要生成后缀字符串,得从字符串的最后一个位置开始,依次将每个位置下标到最后一个位置下标的字符串进行拷贝,即一个大字符串生成所有后缀字符串的代价为 O ( N 2 ) O(N^2) O(N2);
  2. 对所有的后缀字符串进行排序,后缀字符串的数量是 N N N,所以排序的时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN),而在排序过程中涉及到字符串的比较,这个比较的代价和字符串的长度有关,后缀串的平均长度为 N / 2 N/2 N/2,故比较任何两个后缀串的大小的代价或者说每次比较的代价是 O ( N ) O(N) O(N),也就是说排序过程总的时间复杂度为 O ( N l o g N ∗ N ) = O ( N 2 l o g N ) O(NlogN * N) = O(N^2logN) O(NlogNN)=O(N2logN)

所以暴力生成后缀数组的代价为 O ( N 2 l o g N ) O(N^2logN) O(N2logN)

而利用DC3算法,长度为N的字符串,生成后缀数组的复杂度为 O ( N ) O(N) O(N)

3、用DC3算法生成后缀数组的流程

之所以叫做DC3是因为这个算法是根据下标模3来进行分组,做一个类似递归的事情,来解决问题。

举例: N N N 个样本( N N N可以巨大),每个样本中有三维数据(数据并不大),现在要对这 N N N个样本进行排序。排序的原则是先根据一维数据排序,小的在前面;如果一维数据一样,则根据二维数据排序,小的在前面;如果二维数据一样,根据三维数据排序,小的在前面。请问,这样的排序怎么做才最快?「基数排序」最快。

基数排序流程讲解

有如下5个样本:
在这里插入图片描述
假设每个样本的三维数据值都是在0~9范围的,准备10个桶,编号为0 ~ 9。

  • 先根据样本中的第三维数据选择进哪个桶:
    在这里插入图片描述
    从前往后按照桶的编号顺序地将桶中的数据倒出来,得到 [B, D, A, C, E],该顺序是经过第三维数据排序。

  • 然后根据当前的[B, D, A, C, E]样本顺序的第二维数据进桶:
    在这里插入图片描述
    依次将桶中的数据倒出,先入先出,得到顺序为 [B, D, C, E, A],该顺序是经过第二维数据排序的。

  • 最后根据当前的[B, D, C, E, A]样本顺序的第一维数据进桶:
    在这里插入图片描述
    依次将桶中的数据倒出,得到的顺序为 [A, D, C, B, E],该顺序即是最后的顺序。

由此,可以得到一个结论:N个样本,每个样本的维度不超过3,每个维度的值不太大的时候,可以做到时间复杂度为 O ( N ) O(N) O(N) 的排序,有几个维度就进出桶几次,这是不基于比较的排序。

后缀数组并不局限于字符串,数组也可以求后缀数组

比如整数数组 arr = [103, 56, 27, 103],它的所有后缀数组为:

  • 下标0开头:[103, 56, 27, 103]
  • 下标1开头:[56, 27, 103]
  • 下标2开头:[27, 103]
  • 下标3开头:[103]

根据数组中依次的数据大小进行排序得到从小到大为:

  • 0:下标2开头——[27, 103] (第一个数是27,所有数组的第1个元素中最小)
  • 1:下标1开头——[56, 27, 103]
  • 2:下标3开头——[103]
  • 3:下标0开头——[103, 56, 27, 103]

所以原数组arr的后缀数组为 [2, 1, 3, 0]。

DC3算法生成后缀数组的流程

首先,将数组/字符串的下标根据 %3 的结果分成三类,%3 结果为0的是 S0 类,结果为1的是 S1 类,结果为2的是 S2 类。

例如:
在这里插入图片描述
将属于S12类的后缀串选出来:

下标后缀串排名
1ababaa1
2babaa3
4baa2
5aa0(字典序最小)

如果得到S12类的后缀串排名,就可以得到所有后缀串的总排名。怎么说?

来看S0类的后缀串:0下标开头的后缀串、3下标开头的后缀串、6下标开头的后缀串,现在不知道这三个后缀串的排名情况,但是有了S12类的后缀串排名,就可以得到S0类内部的排名了。

S0类的后缀串,用如下的两维数据表示:
在这里插入图片描述

如此一来,0开头的、3开头的和6开头的就两维数据,可以用基数排序了。

所以当有了S12类的排名后,S0内部的排名可以通过一个基数排序直接决定。

可以得到S0类的排名:

下标后缀串排名
6a0(字典序最小)
0aababaa1
3abaa2

此时,已经获得了S12类的排名和S0类的排名,要想得到总排名,即S012的排名,只需要像归并排序那样,进行合并操作。

整体逻辑就是:先得到S12类的精确排名,利用它得到S0内部的排名,利用merge 的方式得到总排名。

在这里插入图片描述
先比较 『6开头的』 VS 『5开头的后缀串』:

  • 6开头的后缀串的第一位数据是 a a a,5开头的后缀串的第一位数据是 a a a,没分出大小;
  • 但是6开头的后缀串除了 a a a,后面没有数据了;而5开头的后缀串除了 a a a,后面还有数据,说明 6 开头的比 5 开头的字典序小。
  • 也就是说 6 开头的是全局最小。

将 6 开头的从S0类中去除:

在这里插入图片描述
然后是 『0 开头的』 VS 『5开头的后缀串』:

  • 0开头的第一位数据是字符 a a a,5开头的第一位数据是字符 a a a,没分出大小;
  • 0开头的第二位数据是字符 a a a,5开头的第二位数据是字符 a a a,没分出大小;
  • 0开头的后缀串的后面信息(即2开头的后缀串),用排名信息代替:即 a a 3 aa3 aa3;而5开头后面没有信息了,所以5开头的更小。
  • 对于更普遍的情况:如果S0类的下标(0开头)和S2类的下标(5开头)后缀串比较大小。
    • 比如6开头的要和5开头的后缀串比较大小:
      • 首先是6开头的后缀串的第一个字符(即下标6位置的字符)和5开头的第一个字符(即下标5位置的字符)比较,如果比较出了大小,则直接merge;
      • 没有比较出大小则继续看下标7位置的字符串 和 下标6位置的字符,如果比较出大小则直接merge;
      • 否则后面直接比较 「8位置下标开头的」和 「7位置下标开头」的排名,因为下标7和8都属于S12类,此前已经得到了S12类的排名。
      • 只需要比较三次,即进行三次基数排序,必能merge出结果。
  • 也就是说:S0类和S2类相遇的时候,最多要比较三维。 (S0 S1 S2) VS (S2 S0 S1)
S0: 		x	x	()		-> 6开头的:		x	x	(8开头的后缀串排名)
下标类别:    S0	S1	S2			  下标:		6	7	
S2: 		x	x  ()	 	-> 5开头的:		x	x	(7开头的后缀串排名)
下标类别:    S2	S0	S1			  下标:		5	6	

同理的,S0类和S1类相遇,最多比较两次,两位信息足以merge出来。

结论如果有S12类的排名,可在 O ( N ) O(N) O(N) 的时间内得到S0内部的排名,再用 O ( N ) O(N) O(N) 的时间合并S0和S12类,得到S012的排名。也就是说只要S12类的排名一旦确定,这个算法就是 O ( N ) O(N) O(N)复杂度的。

问题的关键就变成了「如何得到S12类的排名」?

以字符串 mississippi 为例:
在这里插入图片描述
若只用前三维数据就能得到S12类的精准排名,即三次基数排序,则整个算法结束,然后得到S0内部的排名,合并S0、S12类;

但是本例不能用仅三维数据得到S12类的精准排名,因为存在相同排名。那么应该怎么求呢?组成一个新数组,规则是S1类放左边,S2类放右边:
在这里插入图片描述

因为任何一个数组都可以求得其后缀数组,所以新数组可以递归调用函数求出其后缀数组,最终得到的「新数组0下标开头的后缀串的排名」就是「原数组1下标开头的后缀串的排名」,「新数组1下标开头的后缀串排名」就是「原数组4下标开头的后缀串排名」,以此类推。也就是「新数组的后缀串的排名」可以指导「原数组后缀串的排名」。
在这里插入图片描述

  • 第0名是新数组 3 下标开头的,就代表原数组10下标开头的是第0名;
  • 第1名是新数组 2 下标开头的,就代表原数组 7 下标开头的是第1名;
  • 第2名是新数组 1 下标开头的,就代表原数组 4 下标开头的是第2名;
  • 第3名是新数组 0 下标开头的,就代表原数组 1 下标开头的是第3名;
  • 第4名是新数组 6 下标开头的,就代表原数组 8 下标开头的是第4名;
  • 第5名是新数组 5 下标开头的,就代表原数组 5 下标开头的是第5名;
  • 第6名是新数组 4 下标开头的,就代表原数组 2 下标开头的是第6名。

为什么S1类和S2类要分开放置到新数组中,为什么这种方式可以使得之前悬而未决的排名得到精确的结果?

首先,对于那些使用三维数据已经得到排名的数据来说,无论如何将其放置在新数组中,它的相对次序都不会改变,如「1开头的iss」和「5开头的ssi」大小已近分出来了,无论在新数组中这两个排名值如何摆放,相对次序都不会改变。

将S1类整体放左边,S2类整体放右边,是为了解决相同排名的问题,即在三维数据排名后出现的相同排名问题,如「1开头的iss」和「4开头的iss」都排第3名。「1开头的iss」和「4开头的iss」之所以分不出大小,是因为前三维数据都一样(下标123 和 下标456对应的值),缺的是「4开头的整体排名」和「7开头的整体排名」,而S1类和S2类当前的这种摆放方式,刚好将缺的部分放在了一起,「4开头的整体排名」就用4开头的三维数据的排名3代替了,「7开头的整体排名」用7开头的三维数据排名2代替了,以此类推,这就是DC3算法最精髓的地方

这种新数组的构建方式不仅可以解决S0类或S1类的内部排名相同问题,也可以解决S0类和S1类排名相同的问题。

举个例子:
在这里插入图片描述
注意,新构造数组的S1类的最后一个位置后面跟的是S2类的第一个位置,就比如数组长度为9,下标0 ~ 8,则构造的新数组中原始下标7开头的后面贴着2开头的后缀串,而原数组中7开头的三维数据后面是没有数据的,而新数组的意思就是7开头的后缀串除了前三维数据后面还跟着2开头的后缀串的信息,对于这种边界问题,会适时地在两个类的分界线上补最小的ASCII码字符,分隔开两个类,以避免产生误解。

小结

  • 首先找到S12类的后缀串的前三维数据,如果能通过前三维数据得到精确排名,算法结束;
  • 如果不能则要生成新数组,因为只将S12类放在了数组中,假设原数组长度为 N N N,则新数组是原数组长度的 2 / 3 2/3 2/3,即 2 N / 3 2N/3 2N/3,而在这个数组中只放入了排名,排名不会超过 N N N,所以该数组的最大值为 N N N,于是递归调用DC3算法,准备 N N N个桶,进行三次基数排序,就能得到最终的结果。

整体的复杂度 T ( N ) = T ( 2 N / 3 ) + O ( N ) T(N) = T(2N/3) + O(N) T(N)=T2N/3)+O(N),就是 O ( N ) O(N) O(N) 复杂度。

4、DC3算法代码实现

C++版

//论文源码
inline bool leq(int a1, int a2, int b1, int b2) { // lexicographic order
	return(a1 < b1 || a1 == b1 && a2 <= b2); 
} // for pairs

inline bool leq(int a1, int a2, int a3, int b1, int b2, int b3) {
	return(a1 < b1 || a1 == b1 && leq(a2,a3, b2,b3)); 
} // and triples

// stably sort a[0..n-1] to b[0..n-1] with keys in 0..K from r
static void radixPass(int* a, int* b, int* r, int n, int K) {
	// count occurrences
	int* c = new int[K + 1]; // counter array
	
	for (int i = 0; i <= K; i++) 
		c[i] = 0; // reset counters
		
	for (int i = 0; i < n; i++) 
		c[r[a[i]]]++; // count occurrences
		
	for (int i = 0, sum = 0; i <= K; i++) // exclusive prefix sums
	{
		int t = c[i]; 
		c[i] = sum; 
		sum += t; 
	}
	
	for (int i = 0; i < n; i++) 
		b[c[r[a[i]]]++] = a[i]; // sort
		
	delete [] c;
}

// find the suffix array SA of s[0..n-1] in {1..K}ˆn
// require s[n]=s[n+1]=s[n+2]=0, n>=2
void suffixArray(int* s, int* SA, int n, int K) {
	int n0=(n+2)/3, n1=(n+1)/3, n2=n/3, n02=n0+n2;
	
	int* s12 = new int[n02 + 3]; 
	s12[n02]= s12[n02+1]= s12[n02+2]=0;
	
	int* SA12 = new int[n02 + 3]; 
	SA12[n02]=SA12[n02+1]=SA12[n02+2]=0;
	
	int* s0 = new int[n0];
	int* SA0 = new int[n0];
	
	// generate positions of mod 1 and mod 2 suffixes
	// the "+(n0-n1)" adds a dummy mod 1 suffix if n%3 == 1
	for (int i=0, j=0; i < n+(n0-n1); i++) 
		if (i%3 != 0) 
			s12[j++] = i;
			
	// lsb radix sort the mod 1 and mod 2 triples
	radixPass(s12 , SA12, s+2, n02, K);
	radixPass(SA12, s12 , s+1, n02, K);
	radixPass(s12 , SA12, s , n02, K);
	
	// find lexicographic names of triples
	int name = 0, c0 = -1, c1 = -1, c2 = -1;
	
	for (int i = 0; i < n02; i++) {
		if (s[SA12[i]] != c0 || s[SA12[i]+1] != c1 || s[SA12[i]+2] != c2) {
			name++; 
			c0 = s[SA12[i]]; 
			c1 = s[SA12[i]+1]; 
			c2 = s[SA12[i]+2]; 
		}
		
		if (SA12[i] % 3 == 1) { 
			s12[SA12[i]/3] = name; 
		} // left half
		else {
			s12[SA12[i]/3 + n0] = name; 
		} // right half
	}

	// recurse if names are not yet unique
	if (name < n02) {
		suffixArray(s12, SA12, n02, name);
		// store unique names in s12 using the suffix array
		for (int i = 0; i < n02; i++) 
			s12[SA12[i]] = i + 1;
	} else // generate the suffix array of s12 directly
		for (int i = 0; i < n02; i++) 
			SA12[s12[i] - 1] = i;
			
		// stably sort the mod 0 suffixes from SA12 by their first character
		for (int i=0, j=0; i < n02; i++) 
			if (SA12[i] < n0) 
				s0[j++] = 3*SA12[i];
				
		radixPass(s0, SA0, s, n0, K);

		// merge sorted SA0 suffixes and sorted SA12 suffixes
		for (int p=0, t=n0-n1, k=0; k < n; k++) {
			#define GetI() (SA12[t] < n0 ? SA12[t]*3+1: (SA12[t] - n0) * 3 + 2)
			int i = GetI(); // pos of current offset 12 suffix
			int j = SA0[p]; // pos of current offset 0 suffix
			if (SA12[t] < n0 ? // different compares for mod 1 and mod 2 suffixes
				leq(s[i], s12[SA12[t] + n0], s[j], s12[j/3]) :
			leq(s[i],s[i+1],s12[SA12[t]-n0+1], s[j],s[j+1],s12[j/3+n0]))
			{
				// suffix from SA12 is smaller
				SA[k] = i; t++;
				if (t == n02) // done --- only SA0 suffixes left
				for (k++; p < n0; p++, k++) SA[k] = SA0[p];
			} else {// suffix from SA0 is smaller
				SA[k] = j; p++;
				if (p == n0) // done --- only SA12 suffixes left
					for (k++; t < n02; t++, k++) SA[k] = GetI();
			}
		}
		delete [] s12; 
		delete [] SA12; 
		delete [] SA0; 
		delete [] s0;
}

Java版

//可作为模板使用
public class DC3 {

	public int[] sa; //前文提到的所有后缀数组的排名,sa数组中下标代表排名,值代表每个后缀数组排名后的起始下标

	public int[] rank;//每个位置开始的后缀串的排名,rank数组中下标代表原数组中的下标,值表示排名
	//如[aaba]
	//  1234
	//sa:[3, 0, 1, 2] ->下标
	//	   0  1  2  3  ->排名
	//rank:[1, 2, 3, 0] ->排名
	//		 0	1  2  3 ->下标
	//通过sa数组加工得到rank数组的复杂度:O(N)
		
	public int[] height;

	// 构造方法的约定:
	// 数组叫nums,如果你是字符串,请转成整型数组nums
	// 数组中,最小值>=1,做这种限制是因为可能会需要补0
	// 如果不满足,处理成满足的,也不会影响使用
	// 对于负数数组:[-30, -17, 103]等同于每个数都+31,得到[1, 14, 134],后缀串的排名相同
	// max是nums里面最大值,根据max准备桶的数量
	public DC3(int[] nums, int max) {
		sa = sa(nums, max);
		rank = rank();
		height = height(nums);
	}

	private int[] sa(int[] nums, int max) {
		int n = nums.length;
		int[] arr = new int[n + 3]; //可能会补空,所以多增加了三个长度
		for (int i = 0; i < n; i++) {
			arr[i] = nums[i];
		}
		return skew(arr, n, max);
	}

	private int[] skew(int[] nums, int n, int K) {
		int n0 = (n + 2) / 3, n1 = (n + 1) / 3, n2 = n / 3, n02 = n0 + n2; //统计每个类型(S0/S1/S2)的数量,+2,+1 是因为可能会补空
		int[] s12 = new int[n02 + 3], sa12 = new int[n02 + 3]; //S12类组成一个数组
		for (int i = 0, j = 0; i < n + (n0 - n1); ++i) {
			if (0 != i % 3) {
				s12[j++] = i;
			}
		}
		//最多三维数据,进行三次基数排序
		radixPass(nums, s12, sa12, 2, n02, K);
		radixPass(nums, sa12, s12, 1, n02, K);
		radixPass(nums, s12, sa12, 0, n02, K);
		int name = 0, c0 = -1, c1 = -1, c2 = -1;
		
		for (int i = 0; i < n02; ++i) {
			if (c0 != nums[sa12[i]] || c1 != nums[sa12[i] + 1] || c2 != nums[sa12[i] + 2]) {
				name++;
				c0 = nums[sa12[i]];
				c1 = nums[sa12[i] + 1];
				c2 = nums[sa12[i] + 2];
			}
			if (1 == sa12[i] % 3) {
				s12[sa12[i] / 3] = name;
			} else {
				s12[sa12[i] / 3 + n0] = name;
			}
		}
		
		if (name < n02) { //如果没有得到严格排名,则递归调用
			sa12 = skew(s12, n02, name);
			for (int i = 0; i < n02; i++) {
				s12[sa12[i]] = i + 1;
			}
		} else {
			for (int i = 0; i < n02; i++) {
				sa12[s12[i] - 1] = i;
			}
		}
		//此时已经得到S12类的准确排名
		
		//先解决S0内部的排名
		int[] s0 = new int[n0], sa0 = new int[n0]; 
		for (int i = 0, j = 0; i < n02; i++) {
			if (sa12[i] < n0) {
				s0[j++] = 3 * sa12[i];
			}
		}
		radixPass(nums, s0, sa0, 0, n0, K);
		
		//再解决S0和S12类合并的问题
		int[] sa = new int[n];
		for (int p = 0, t = n0 - n1, k = 0; k < n; k++) {
			int i = sa12[t] < n0 ? sa12[t] * 3 + 1 : (sa12[t] - n0) * 3 + 2;
			int j = sa0[p];
			if (sa12[t] < n0 ? leq(nums[i], s12[sa12[t] + n0], nums[j], s12[j / 3])
					: leq(nums[i], nums[i + 1], s12[sa12[t] - n0 + 1], nums[j], nums[j + 1], s12[j / 3 + n0])) {
				sa[k] = i;
				t++;
				if (t == n02) {
					for (k++; p < n0; p++, k++) {
						sa[k] = sa0[p];
					}
				}
			} else {
				sa[k] = j;
				p++;
				if (p == n0) {
					for (k++; t < n02; t++, k++) {
						sa[k] = sa12[t] < n0 ? sa12[t] * 3 + 1 : (sa12[t] - n0) * 3 + 2;
					}
				}
			}
		}
		return sa;
	}

	private void radixPass(int[] nums, int[] input, int[] output, int offset, int n, int k) {
		int[] cnt = new int[k + 1];
		for (int i = 0; i < n; ++i) {
			cnt[nums[input[i] + offset]]++;
		}
		for (int i = 0, sum = 0; i < cnt.length; ++i) {
			int t = cnt[i];
			cnt[i] = sum;
			sum += t;
		}
		for (int i = 0; i < n; ++i) {
			output[cnt[nums[input[i] + offset]]++] = input[i];
		}
	}

	private boolean leq(int a1, int a2, int b1, int b2) {
		return a1 < b1 || (a1 == b1 && a2 <= b2);
	}

	private boolean leq(int a1, int a2, int a3, int b1, int b2, int b3) {
		return a1 < b1 || (a1 == b1 && leq(a2, a3, b2, b3));
	}

	private int[] rank() {
		int n = sa.length;
		int[] ans = new int[n];
		for (int i = 0; i < n; i++) {
			ans[sa[i]] = i;
		}
		return ans;
	}

	private int[] height(int[] s) {
		int n = s.length;
		int[] ans = new int[n];
		for (int i = 0, k = 0; i < n; ++i) {
			if (rank[i] != 0) {
				if (k > 0) {
					--k;
				}
				int j = sa[rank[i] - 1];
				while (i + k < n && j + k < n && s[i + k] == s[j + k]) {
					++k;
				}
				ans[rank[i]] = k;
			}
		}
		return ans;
	}

	// 为了测试
	public static int[] randomArray(int len, int maxValue) {
		int[] arr = new int[len];
		for (int i = 0; i < len; i++) {
			arr[i] = (int) (Math.random() * maxValue) + 1;
		}
		return arr;
	}

	// 为了测试
	public static void main(String[] args) {
		int len = 100000;
		int maxValue = 100;
		long start = System.currentTimeMillis();
		new DC3(randomArray(len, maxValue), maxValue);
		long end = System.currentTimeMillis();
		System.out.println("数据量 " + len + ", 运行时间 " + (end - start) + " ms");
	}
}

5、DC3算法的地位

笔试的时候,通常会有根据算法有以下几种数据通过情况:

  1. 暴力解,不通过;
  2. 优化解,假设数据通过70%;
  3. 出题人给定的答案解,数据通过100%,是根据这个解设置的数据量

但是如果一个题目可以使用DC3算法,那它是碾压答案解的,但是因为DC3算法太难,导致笔试的时候设置的答案解都会选一个比DC3差一点儿的解,这就是DC3算法的地位。

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

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

相关文章

电商项目8:平台属性

电商项目8&#xff1a;平台属性1、后端1.1、属性分组模糊查询1.2、商品属性新增功能&#xff1a;保存关联关系1、后端 1.1、属性分组模糊查询 需要改造。当前端传0时候。模糊查询功能有点问题 AttrGroupServiceImpl Overridepublic PageUtils queryPage(Map<String, Obje…

Java 进阶(10) 线程生命周期

线程的生命周期 五种基本状态 当线程被创建并启动以后&#xff0c;它既不是⼀启动就进⼊了执⾏状态&#xff0c;也不是⼀直处于执⾏状态。 新建状态&#xff08;New&#xff09; 当线程对象对创建后&#xff0c;即进⼊了新建状态&#xff0c;如&#xff1a; Thread t new M…

微服务架构中多级缓存设计

一.Nginx 缓存管理 在 Nginx 中自带将后端应用中图片、CSS、JS 等静态资源缓存功能&#xff0c; 我们只需在 Nginx 的核心配置 nginx.conf 中增加下面的片段&#xff0c; 便可对后端的静态资源进行缓存&#xff0c;关键配置我已做好注释&#xff0c; 可以直接使用; # 设置缓存…

同源策略与跨域

同源:协议、域名、端口号 必须完全相同。 违背同源策略就是跨域。 例如&#xff1a; 协议&#xff1a;http或者是https 域名&#xff1a;www.xxx.com 端口号&#xff1a;80&#xff0c;8000等。 同源&#xff1a;同一个来源。 同源&#xff1a;可以直接简写服务器页面的地址。…

激活数字营销新引擎,亚马逊云科技为企业带来数字化营销新体验

随着流量红利逐渐消失&#xff0c;营销触点呈现多元化&#xff0c;消费者决策变得复杂&#xff0c;利用数字化激活新的营销引擎成为破局关键。亚马逊云科技联合合作伙伴&#xff0c;基于智能湖仓打造了4个解决方案领域&#xff1a;一方数据平台、客户数字体验、广告智能分析、隐…

工具-win11系统,微软自带输入法输入“sj” 显示时间 【2022年01月11日 10:16:49】格式

文章目录1、前提2、操作3、碎碎念4、更新 2023年04月13日1、前提 下载某某输入法&#xff0c;输入“sj” 会自动显示【2023-04-11 09:57:01 】这样的格式&#xff0c;微软自带的输入法是显示【09点57分】的格式&#xff0c;但是由于个人工作学习需要&#xff0c;所以前者的键入…

FreeRTOS 任务调度及相关函数详解(二)

文章目录一、任务创建函数 xTaskCreate()二、任务初始化函数 prvInitialiseNewTask()三、任务堆栈初始化函数 pxPortInitialiseStack()四、添加任务到就绪列表 prvAddNewTaskToReadyList()五、任务删除 vTaskDelete()六、任务挂起 vTaskSuspend()七、任务恢复 vTaskResume()一、…

shadow机械手臂系统

机械手臂系统 Shadow机械手臂系统是由美国Shadow Robot Company开发的一款高精度机械手臂系统&#xff0c;主要用于工业自动化、医疗器械、科学研究等领域。Shadow机械手臂系统采用了多自由度的设计&#xff0c;可以实现高精度的三维运动和灵活的操作&#xff0c;其控制系统还支…

ds18b20-温度传感器-linux驱动-混杂设备

文章目录ds18b20读取温度数据步骤ds18b20时序图&#xff1a;初始化时序DS18B20初始化时序的步骤&#xff1a;读/写时序DS18B20写步骤&#xff1a;DS18B20读步骤&#xff1a;DS18B20驱动实现结果如下&#xff1a;参考&#xff1a;ds18b20读取温度数据步骤 初始化&#xff1a;将…

对话ChatGPT:Prompt是普通人“魔法”吗?

在ChatGPT、Midjourney、Stable Diffusion等新事物的作用下&#xff0c;不少人或多或少听说过Prompt的概念。 虽然OpenAI掀起的大模型浪潮再度刷新了人们对AI的认知&#xff0c;但现阶段的AI终归还不是强人工智能&#xff0c;大模型里的“知识”存储在一个隐性空间里&#xff0…

工地高空作业安全带穿戴识别 python

工地高空作业安全带穿戴识别系统通过pythonopencv网络模型分析技术&#xff0c;工地高空作业安全带穿戴识别算法模型对现场监控画面中人员安全绳安全带穿戴进行检测&#xff0c;不需人为干预立即触发告警存档。OpenCV的全称是Open Source Computer Vision Library&#xff0c;是…

【Ruby 2D】【unity learn】抬头显示血条

说起游戏开发&#xff0c;大家一般会觉得控制角色移动和制作血条哪个难呢&#xff1f; 或许都会觉得血条比较难吧。 是的&#xff0c;正是如此。 那么我们让我们来看看血条该怎么做吧 这是效果图 受伤后是这样的 首先是创建一张Canvas画布 这个画布会很大 相比之下我们的小…

【redis】BigKey

【redis】BigKey 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章…

ChatGPT云桌面:无需科技挂载,即点即用

ChatGPT是一个由OpenAI开发的人工智能对话语言模型。它被设计为对话式人工智能代理&#xff0c;用于客户服务、个人助理和文娱等任务。它可以理解并生成多种语言的文本&#xff0c;包括中文、英语、西班牙语、德语等。但从某些地方访问ChatGPT可能很困难&#xff0c;特别是在注…

实验4 Matplotlib数据可视化

1. 实验目的 ①掌握Matplotlib绘图基础&#xff1b; ②运用Matplotlib&#xff0c;实现数据集的可视化&#xff1b; ③运用Pandas访问csv数据集。 2. 实验内容 ①绘制散点图、直方图和折线图&#xff0c;对数据进行可视化&#xff1b; ②下载波士顿数房价据集&#xff0c;并…

机器学习 -- 过拟合与欠拟合以及应对过拟合的方法 神经网络中的超参数如何选择

前言 在学习机器学习的过程中&#xff0c;训练模型时常遇到的问题就是模型的过拟合和欠拟合&#xff0c;下文我将解释过拟合和欠拟合的概念&#xff0c;并且学习应对过拟合以及神经网络中的超参数如何选择的方法。 过拟合和与欠拟合 过拟合&#xff1a;是指学习时选择的模型…

基于 Git 的开发工作流——主干开发特性总结

在参与开发的过程&#xff0c;得益与平台提供便捷的开发流程&#xff0c;简化很多开发过程操作分支的步骤&#xff1b;也就很好奇&#xff0c;为什么研发平台怎么设计&#xff0c;考虑的点是为什么&#xff0c;便有了这次对主干研发的学习与记录。当我们是构建软件项目的唯一开…

【计算机网络-传输层】TCP 协议

文章目录1 传输层概述1.1 传输层的功能1.2 端口号2 TCP 报文段2.1 TCP 报文段首部格式2.2 TCP 数据传送的过程3 TCP 连接管理3.1 TCP 连接的建立——三次握手3.1.1 客户机向服务器发送 TCP 连接请求报文段3.1.2 服务器向客户机发送 TCP 连接请求确认报文段3.1.3 客户机向服务器…

python数据可视化玩转Matplotlib subplot子图操作,四个子图(一包四),三个子图,子图拉伸

目录 一、创建子图 1.1 下图是绘制的子图&#xff1a; 1.2 代码释义&#xff1a; 二、绘制子图 2.1 代码引入 2.2 图形绘制 三、子图布局 3.1 子图布局说明 四、子图大小 4.1 子图大小调整 五、子图间距 5.1 子图代码调整 六、子图位置 6.1 代码引入 6.2 完整代码…

如何在 Windows10 下运行 Tensorflow 的目标检测?

前言 看过很多博主通过 Object Detection 实现了一些皮卡丘捕捉&#xff0c;二维码检测等诸多特定项的目标检测。而我跟着他们的案例来运行的时候&#xff0c;不是 Tensorflow 版本冲突&#xff0c;就是缺少什么包&#xff0c;还有是运行官方 object_detection_tutorial 不展示…