最坏情况为线性时间的第k大元素

news2024/12/24 21:36:29

在统计和数据分析中,我们经常会遇到求最大值、最小值、中位数、四分位数、Top K等类似需求,其实它们都属于顺序统计量,本文将对顺序统计量的定义和求解算法进行介绍,重点介绍如何在最差时间复杂度也是线性的情况下求解第k大元素。

1. 顺序统计量与选择问题

在一个有 n n n个元素的集合中,第 i i i顺序统计量是该集合中第 i i i小的元素。例如在集合 ( 1 , 3 , 5 , 2 ) (1, 3, 5, 2) (1,3,5,2)中,第2个顺序统计量为2。

从一个有 n n n个元素的集合中,选择出(求解)其第 i i i个顺序统计量的问题被称为选择问题。选择问题的输入输出如下:

输入:一个包含 n n n个不同的数的集合 A A A和一个数 i i i 1 ≤ i ≤ n 1 \leq i \leq n 1in);
输出:元素 x ∈ A x \in A xA,它恰大于A中其它 ( i − 1 ) (i-1) (i1)个元素。

2. 选择问题的求解方法

显然,对输入集合 A A A进行排序之后就可以解决选择问题,使用堆排序或归并排序对输入集合进行排序,然后在排序后的数组中标出第 i i i个元素,即可在 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)时间内完成求解,但还有更快的算法。

2.1 最大值与最小值

我们先考虑选择问题的特殊情况,只求解最大值或最小值,可以发现很容易在 O ( n ) O(n) O(n)时间内完成求解。只需要遍历数组,进行(n-1)次比较即可。以求解最小值为例,伪码如下:

MINIMUM(A)
min = A[1]
n = length[A]
for(i=2; i<=n; i++)
	if(min > A[i])
		min = A[i]
return min

在此基础上,增加一点难度,我们希望同时找最大值和最小值,是否还可以在 O ( n ) O(n) O(n)时间内完成求解呢?答案是肯定的,在遍历过程中不再每次比较一个元素,而是每次比较两个元素,两个元素中较小的元素与当前最小值比较,较大的元素和当前最大值比较,即每对元素需要3次比较即可。伪码如下:

MAX-MINIMUM(A)
if(length[A] is odd)
	min = A[1]
	max = min
else 
	min = MIN(A[1], A[2])
	max = MAX(A[1], A[2])
i++
while(i <= length[A])
	min = MIN(MIN(A[i], A[i+1]), min)
	max = MAX(MAX(A[i], A[i+1]), max)
	i = i + 2 
return min, max

在当前最大值和最小值初始值的设定上,如果n是奇数,将最大值和最小值均设置为第一个元素的值;如果n是偶数,就对前两个元素做一次比较来决定最大值与最小值的初始值。因此,如果n是奇数,那么总共做了 3 ⌊ n / 2 ⌋ 3\lfloor n/2 \rfloor 3n/2次比较;如果n是偶数,那么总共做了 3 n / 2 − 2 3n/2-2 3n/22次比较,时间复杂度为 O ( n ) O(n) O(n)

2.2 期望时间为线性的选择问题

我们回到一般的选择问题,看起来一般的选择问题比求最小值和最大值复杂得多,但神奇的算法仍然可以让我们在平均为线性的时间完成求解。这里再次使用了分治思想,借鉴了快速排序的随机划分方法,如果刚好划分元的左边有(i-1)个元素,则找到第i小的元素;否则,在划分元的左侧或右侧继续进行随机划分。伪码如下:

RANDOMIZED-SELECT(A, p, r, i) // A为数组,p为数组左边界,r为数组右边界,i为待求的顺序统计量序号
if(p == r) // 临界问题处理
	return A[p]
q = RANDOMIZED-PARTITION(A, p, r) //进行划分,返回划分元下标
k = q – p + 1 // k=rank(A[q]) in A[p,…,r], 返回划分元的序号
if(i == k)
	return A[q]
else if(i < k)
	return RANDOMIZED-SELECT(A, p, q - 1, i)
else
	return RANDOMIZED-SELECT(A, q + 1, r, i – k)

可以证明在平均情况下,算法的时间复杂度为 O ( n ) O(n) O(n)。而当运气不好时,每次都只能去除一个元素,算法的时间复杂度就可能达到 O ( n 2 ) O(n^2) O(n2)

2.3 最差时间为线性的选择问题

在上述RANDOMIZED-SELECT算法的基础上,保证每次对数组的划分是个好划分,我们就能进一步在最差情况下也用线性时间解决选择问题。主要步骤如下:

  1. n个元素每5个分为一组,一共 ⌈ n / 5 ⌉ \lceil n/5 \rceil n/5组。最后一组有n mod 5个元素。
  2. 对每组进行排序,取其中位数。若最后一组有偶数个元素,则取较小的中位数。
  3. 递归地使用本算法寻找 ⌈ n / 5 ⌉ \lceil n/5 \rceil n/5个中位数的中位数x
  4. x作为划分元对数组A进行划分,并设x是第k个最小元。
  5. 如果i = k,则返回x;否则如果i < k,则找左区间的第i个最小元;如果i > k,则找右区间的第i - k个最小元。

伪码如下:

SELECT(A, p, r, i)
if(r - p <= 140)
	用简单的排序算法对数组A[p..r进行排序
	return A[p + k - 1]
n = r - p + 1
for(i = 0; i <= floor(n/5); i++) //寻找每组的中位数
	将A[p+5*i]至A[p+5*i+4]的第3小元素与A[p+i]交换位置
x = SELECT(A, p, p+floor(n/5), floor(n/10)) //找中位数的中位数
i = PARTITION(A, p, r, x)
j = i - p + 1
if(k <= j)
	return SELECT(A, p, i, k)
else 
	return SELECT(A, i + 1, r, k - j)

3. 程序代码

以下C语言程序代码实现了最坏情况为线性的select算法,将“求数组a[1..n]中第k大的元素”转化为“求数组a[1..n]中第(n-k+1)小的元素”。递归调用select时,设置数组长度不大于140时,即直接使用插入排序。

3.1 linearSelect_kth.cpp

#include <stdio.h>
#include <stdlib.h>

#define N 1000000     //定义输入数组的最大长度 
#define LEN 5         //定义select中每组元素的个数 

int a[N];

void swap(int *a, int *b) { //交换 a 与 b 的值 
	int tmp = *a;
    *a = *b;
    *b = tmp;
}

int partition(int a[], int low, int high, int pivot) { //将数组a[low..high]划分为 <= pivot和 > pivot的两部分 
    int x;
    int i = low - 1;
    int j;
    for (j = low; j < high; j++) { //在数组中找到值等于privot的元素作为主元,交换到数组最右端 
        if (a[j] == pivot) {
            swap(&a[j], &a[high]);
        }
    }
    x = a[high];
    for (j = low; j < high; j++) { //维护低区a[low..i] <= x, 高区a[i+1..j-1] > x  
        if (a[j] <= x) {           //如果发现a[j] <= x,则将a[j]交换到低区 
            i++;
            swap(&a[i], &a[j]);
        }
    }
    swap(&a[i + 1], &a[high]);    //将主元与最左的大于 x 的元素a[i+1]交换,此时主元到了它应在的位置 
    return i + 1;                 //返回分区完成后主元所在的新下标 
}

void insertSort(int a[], int low, int high) { //对a[low..high]进行插入排序 
    int i, j;
    for (i = low + 1; i <= high; i++) {
        int temp = a[i];
        for (j = i - 1; j >= low && temp < a[j]; j--) {
            a[j + 1] = a[j];
        }
        a[j + 1] = temp;
    }
}

int select(int a[], int begin, int end, int k) { //选出数组a[begin..end]的第k小元素 
    int length = end - begin + 1;   //数组长度,即数组中元素的个数 
    if (length <= 140) {            //长度较小,直接用插入排序 
        insertSort(a, begin, end);
        return a[begin + k - 1];
    }
    int groups = (length + LEN) / LEN;  //组数 
    int i;
    for (i = 0; i < groups; i++) {
        int left = begin + LEN * i;  //第i组的左边界 
        int right = (begin + LEN * i + LEN - 1) > end ? end : (begin + LEN * i + LEN - 1);  //第i组的右边界 
        insertSort(a, left, right);  //组内进行插入排序 
        //将第i组中位数与数组a[]的第i个元素互换位置,方便递归select寻找中位数的中位数
        int mid = (left + right) / 2;
        swap(&a[begin + i], &a[mid]); 
    }
    int pivot = select(a, begin, begin + groups - 1, (groups + 1) / 2);  //找出中位数的中位数
    int p = partition(a, begin, end, pivot);  //用中位数的中位数作为划分的主元
	int leftNum =  p - begin;                 //低区元素的数量 
    if (k == leftNum + 1) {
    	return a[p];
	}
    else if (k <= leftNum) {
    	return select(a, begin, p - 1, k);  //在低区递归调用select来找出第k小的元素 
	}
    else {
    	return select(a, p + 1, end, k - leftNum -1);  //在高区递归调用select来找出第(k-leftNum-1)小的元素 
	}   
}


int main() {
    FILE *fp = fopen("data_1022.txt","r");            //打开文件 
	if (fp == NULL) {
		printf("Can not open the file!\n");
		exit(0);
	}
	int i = 0;
	while (fscanf(fp, "%d\n", &a[i]) != EOF) {  //读取文件中的数据到数组a[]中 
		i++;
	}
	fclose(fp);                                 //关闭文件 
	int k;
	while (1) {
		printf("Please enter an integer k, and you will get the k-th largest element in the array!\n");
		printf("(Enter negative or zero to quit): ");
		scanf("%d", &k);
		if (k <= 0) {
			printf("Bye\n"); 
			break;
		}
    	printf("The %dth largest element in the array is: %d\n", k, select(a, 0, i - 1, i - k + 1));
    	printf("\n==================================================================================\n");
	}
    return 0;
}

3.2 linearSelect_kth_grouplenth_5_vs_7_vs_3.cpp

然后,在3.1节代码的基础上,我尝试改变每组元素的个数,分别设置每组元素个数为5、7、3,比较算法运行时间的差异。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

#define N 1000000     //定义输入数组的最大长度 
#define LEN1 5        //尝试改变select中每组元素的个数
#define LEN2 7
#define LEN3 3

int a[N];

void swap(int *a, int *b) { //交换 a 与 b 的值 
	int tmp = *a;
    *a = *b;
    *b = tmp;
}

int partition(int a[], int low, int high, int pivot) { //将数组a[low..high]划分为 <= pivot和 > pivot的两部分 
    int x;
    int i = low - 1;
    int j;
    for (j = low; j < high; j++) { //在数组中找到值等于privot的元素作为主元,交换到数组最右端 
        if (a[j] == pivot) {
            swap(&a[j], &a[high]);
        }
    }
    x = a[high];
    for (j = low; j < high; j++) { //维护低区a[low..i] <= x, 高区a[i+1..j-1] > x  
        if (a[j] <= x) {           //如果发现a[j] <= x,则将a[j]交换到低区 
            i++;
            swap(&a[i], &a[j]);
        }
    }
    swap(&a[i + 1], &a[high]);    //将主元与最左的大于 x 的元素a[i+1]交换,此时主元到了它应在的位置 
    return i + 1;                 //返回分区完成后主元所在的新下标 
}

void insertSort(int a[], int low, int high) { //对a[low..high]进行插入排序 
    int i, j;
    for (i = low + 1; i <= high; i++) {
        int temp = a[i];
        for (j = i - 1; j >= low && temp < a[j]; j--) {
            a[j + 1] = a[j];
        }
        a[j + 1] = temp;
    }
}

int select_5(int a[], int begin, int end, int k) { //选出数组a[begin..end]的第k小元素,分组长度为5 
    int length = end - begin + 1;  //数组长度,即数组中元素的个数
    if (length <= 140) {           //长度较小,直接用插入排序 
        insertSort(a, begin, end);
        return a[begin + k - 1];
    }
    int groups = (length + LEN1) / LEN1;  //组数
    int i;
    for (i = 0; i < groups; i++) {
        int left = begin + LEN1 * i;  //第i组的左边界 
        int right = (begin + LEN1 * i + LEN1 - 1) > end ? end : (begin + LEN1 * i + LEN1 - 1);  //第i组的右边界 
        insertSort(a, left, right);  //组内进行插入排序 
        //将第i组中位数与数组a[]的第i个元素互换位置,方便递归select寻找中位数的中位数
        int mid = (left + right) / 2;
        swap(&a[begin + i], &a[mid]); 
    }
    int pivot = select_5(a, begin, begin + groups - 1, (groups + 1) / 2);  //找出中位数的中位数
    int p = partition(a, begin, end, pivot);  //用中位数的中位数作为划分的主元
    int leftNum =  p - begin;                 //低区元素的数量 
    if (k == leftNum + 1) {
    	return a[p];
	}
    else if (k <= leftNum) {
    	return select_5(a, begin, p - 1, k);  //在低区递归调用select来找出第k小的元素 
	}
    else {
    	return select_5(a, p + 1, end, k - leftNum -1);  //在高区递归调用select来找出第(k-leftNum-1)小的元素 
	}   
}

int select_7(int a[], int begin, int end, int k) {
    int length = end - begin + 1;
    if (length <= 140) {
        insertSort(a, begin, end);
        return a[begin + k - 1];
    }
    int groups = (length + LEN2) / LEN2;
    int i;
    for (i = 0; i < groups; i++) {
        int left = begin + LEN2 * i;  //第i组的左边界 
        int right = (begin + LEN2 * i + LEN2 - 1) > end ? end : (begin + LEN2 * i + LEN2 - 1);  //第i组的右边界 
        insertSort(a, left, right);  //组内进行插入排序 
        //将第i组中位数与数组a[]的第i个元素互换位置,方便递归select寻找中位数的中位数
        int mid = (left + right) / 2;
        swap(&a[begin + i], &a[mid]); 
    }
    int pivot = select_7(a, begin, begin + groups - 1, (groups + 1) / 2);  //找出中位数的中位数
    int p = partition(a, begin, end, pivot);  //用中位数的中位数作为划分的主元
    int leftNum =  p - begin;                 //低区元素的数量 
    if (k == leftNum + 1) {
    	return a[p];
	}
    else if (k <= leftNum) {
    	return select_7(a, begin, p - 1, k);  //在低区递归调用select来找出第k小的元素 
	}
    else {
    	return select_7(a, p + 1, end, k - leftNum -1);  //在高区递归调用select来找出第(k-leftNum-1)小的元素 
	}   
}

int select_3(int a[], int begin, int end, int k) {
    int length = end - begin + 1;
    if (length <= 140) {
        insertSort(a, begin, end);
        return a[begin + k - 1];
    }
    int groups = (length + LEN3) / LEN3;
    int i;
    for (i = 0; i < groups; i++) {
        int left = begin + LEN3 * i;  //第i组的左边界 
        int right = (begin + LEN3 * i + LEN3 - 1) > end ? end : (begin + LEN3 * i + LEN3 - 1);  //第i组的右边界 
        insertSort(a, left, right);  //组内进行插入排序 
        //将第i组中位数与数组a[]的第i个元素互换位置,方便递归select寻找中位数的中位数
        int mid = (left + right) / 2;
        swap(&a[begin + i], &a[mid]); 
    }
    int pivot = select_3(a, begin, begin + groups - 1, (groups + 1) / 2);  //找出中位数的中位数
    int p = partition(a, begin, end, pivot);  //用中位数的中位数作为划分的主元
    int leftNum =  p - begin;                 //低区元素的数量 
    if (k == leftNum + 1) {
    	return a[p];
	}
    else if (k <= leftNum) {
    	return select_3(a, begin, p - 1, k);  //在低区递归调用select来找出第k小的元素 
	}
    else {
    	return select_3(a, p + 1, end, k - leftNum -1);  //在高区递归调用select来找出第(k-leftNum-1)小的元素 
	}   
}


int main() {
    FILE *fp = fopen("data_1022.txt","r");            //打开文件 
	if (fp == NULL) {
		printf("Can not open the file!\n");
		exit(0);
	}
	int i = 0;
	while (fscanf(fp, "%d\n", &a[i]) != EOF) {  //读取文件中的数据到数组a[]中 
		i++;
	}
	fclose(fp);                                 //关闭文件 
	printf("Please enter an integer k, and you will get the k-th largest element in the array:\n");
	int k;
	scanf("%d", &k);
	printf("********************* Group length is 5, array size is 945800**********************\n");
	LARGE_INTEGER nFreq;
	LARGE_INTEGER nBeginTime;
	LARGE_INTEGER nEndTime;
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_5(a, 0, i - 1, i - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	double time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("********************* Group length is 7, array size is 945800 **********************\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_7(a, 0, i - 1, i - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("********************* Group length is 3, array size is 945800 **********************\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_3(a, 0, i - 1, i - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("====================== Group length is 5, array size is 10000 ======================\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_5(a, 0, 9999, 10000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("====================== Group length is 7, array size is 10000 ======================\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_7(a, 0, 9999, 10000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("====================== Group length is 3, array size is 10000 ======================\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_3(a, 0, 9999, 10000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("######################## Group length is 5, array size is 1000 ########################\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_5(a, 0, 999, 1000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("######################## Group length is 7, array size is 1000 ########################\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_7(a, 0, 999, 1000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
	
	printf("######################## Group length is 3, array size is 1000 ########################\n");
	QueryPerformanceFrequency(&nFreq);
	QueryPerformanceCounter(&nBeginTime); 
    printf("The %dth largest element in the array is: %d\n", k, select_3(a, 0, 999, 1000 - k + 1));
    QueryPerformanceCounter(&nEndTime);  //计时结束 
	time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / nFreq.QuadPart * 1000;
	printf("Running time: %lfms\n\n", time);
    return 0;
}

4. 运行结果

在linearSelect_kth.cpp中,设置分组长度为5。运行该程序,程序循环提示输入整数k,按下回车后会输出第k大的元素,直至输入一个负数或0,程序终止。

在linearSelect_kth_grouplenth_5_vs_7_vs_3.cpp中,大致比较了算法在分组长度为5、7、3以及在不同问题规模的情况下的运行时间。运行该程序,程序提示输入一个整数k,按下回车后,程序依次输出算法分组长度为5、7、3分别在数组长度为945800、10000、1000时的运行时间。

4.1 linearSelect_kth.cpp

  • 第1大: 9999990
  • 第5大: 9999940
  • 第7大: 9999915
  • 第90大: 9998974
  • 第100大:9998835

在这里插入图片描述

4.2 linearSelect_kth_grouplenth_5_vs_7_vs_3.cpp

在这里插入图片描述
在这里插入图片描述
多次运行发现,在数组长度为945800时,基本上运行时间都是组长为7 < 组长为5 < 组长为3。在问题规模减小时,三者的运行时间大小关系略有波动,猜测可能是由于组长为3的select算法是非线性的以及程序运行计时存在误差等。

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

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

相关文章

代码随想录:栈与队列4-6

20.有效的括号 题目 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一…

OpenHarmony分布式软总线API调用测试工具 softbus_tool使用说明

softbus_tool 是 OpenHarmony 分布式软总线 API 调用测试工具&#xff0c;文件结构如下图所示。 softbus_tool 能够将软总线 interfaces 目录下的一些常用接口集中起来&#xff0c;供设备间搭建一些场景时使用&#xff08;比如设备绑定、BR 组网&#xff0c;BLE 组网&#xff…

红豆Cat 1开源|项目三: 从0-1设计一款HTTP版本RTU(支持GNSS)产品的软硬件全过程

HTTP版RTU&#xff08;支持GNSS&#xff09;项目概述 RTU&#xff08;Remote Terminal Unit&#xff09;&#xff0c;中文即远程终端控制系统&#xff0c;负责对现场信号、工业设备的监测和控制。RTU是构成企业综合自动化系统的核心装置&#xff0c;通常由信号输入/出模块、微…

ArrayList中多线程的不安全问题

ArrayList中的不安全问题 正常的输出 List<String> list Arrays.asList("1","2","3"); list.forEach(System.out::println);为什么可以这样输出&#xff0c;是一种函数是接口&#xff0c;我们先过个耳熟 Arrys.asList是返回一个ArrayL…

Redis高级-分布式缓存RDB原理

分布式缓存 1.1.2.RDB原理 bgsave开始时会fork主进程得到子进程&#xff0c;子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。 fork采用的是copy-on-write技术&#xff1a; 当主进程执行读操作时&#xff0c;访问共享内存&#xff1b;当主进程执行写操…

振弦式应变计在岩土工程中的应用

岩土工程是土木工程的一个重要分支&#xff0c;主要研究岩石、土壤等天然材料的工程特性及其在工程中的应用。它涉及地基基础、边坡工程、隧道工程、水利水电工程等众多领域&#xff0c;是保障建筑物安全、稳定的基础性工程。 点击输入图片描述&#xff08;最多30字&#xff09…

rabbitmq延迟队列的使用

rabbitmq延迟队列的使用 1、场景&#xff1a; 1.定时发布文章 2.秒杀之后&#xff0c;给30分钟时间进行支付&#xff0c;如果30分钟后&#xff0c;没有支付&#xff0c;订单取消。 3.预约餐厅&#xff0c;提前半个小时发短信通知用户。 A -> 13:00 17:00 16:30 延迟时间&a…

再也不怕面试官问 OOM了,一次生产环境 Metaspace OOM 排查流程实操!

问题背景 小奎公司的运维同时今天反映核心业务一个服务目前 CPU 的使用率、堆内存、非堆内存的使用率有点高。刚反映没有过多久该服务就直接 OOM 了&#xff0c;以下是生产监控平台监控信息。 CPU 使用率监控 堆内存和非堆内存使用率 OOM 产生的日志报错信息 问题分析 根…

kali使用msf+apkhook520+cploar实现安卓手的攻击

学习网络安全的过程中&#xff0c;突发奇想怎么才能控制或者说是攻击手机 边找工作边实验 话不多说启动kali 一、使用msfapktool生成简单的木马程序 首先使用kali自带的msfvenom写上这样一段代码 选择安卓 kali的ip 一个空闲的端口 要输出的文件名 msfvenom -p android/met…

9个最受欢迎的开源自动化测试框架盘点!

自动化测试框架可以帮助测试人员评估多个Web和移动应用程序的功能&#xff0c;安全性&#xff0c;可用性和可访问性。尽管团队可以自己构建复杂的自动化测试框架&#xff0c;但是当他们可以使用现有的开源工具&#xff0c;库和测试框架获得相同甚至更好的结果时&#xff0c;通常…

ubuntu系统逻辑卷Logical Volume扩容根分区

Linux LVM详解 https://blog.csdn.net/qq_35745940/article/details/119054949 https://blog.csdn.net/weixin_41891696/article/details/118805670 https://blog.51cto.com/woyaoxuelinux/1870299 LVM&#xff08;Logical Volume Manager&#xff09;逻辑卷管理&#xff0c…

C++入门语法(命名空间缺省函数函数重载引用内联函数nullptr)

目录 前言 1. 什么是C 2. C关键字 3. 命名空间 3.1 命名空间的定义 3.2 命名空间的使用 4. C输入和输出 5. 缺省函数 5.1 概念 5.2 缺省参数分类 6. 函数重载 6.1 概念 6.2 为何C支持函数重载 7. 引用 7.1 概念 7.2 特性 7.3 常引用 7.4 引用与指针的区别 7…

OSCP靶场--Hetemit

OSCP靶场–Hetemit 考点(python代码注入 systemctrl提权) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.173.117 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-10 05:52 EDT Nmap scan report for 192.168.1…

详解多态、虚继承、多重继承内存布局及虚表(C++)

本篇文章深入分析多态、虚继承、多重继承的内存布局及虚函数表以及实现原理。编译器使用VS 2022&#xff0c;直接放结论&#xff0c;代码及内存调试信息在后文。 结论 内存布局 一个没有虚函数的类&#xff0c;它的大小其实就是所有成员变量的大小&#xff0c;此时它就是一个…

13 指针(上)

指针是 C 语言最重要的概念之一&#xff0c;也是最难理解的概念之一。 指针是C语言的精髓&#xff0c;要想掌握C语言就需要深入地了解指针。 指针类型在考研中用得最多的地方&#xff0c;就是和结构体结合起来构造结点(如链表的结点、二叉树的结点等)。 本章专题脉络 1、指针…

redis string底层为什么使用sds, sds好处?redis 的动态字符串优点?

1. redis 的键值对&#xff0c;都是由对象组成的&#xff0c; 其中键总是一个字符串对象&#xff08;string object&#xff09; 而键的value则可以是&#xff1a;“字符串对象”&#xff0c; “列表对象 &#xff08;list object&#xff09;”&#xff0c;“哈希对象 (hash o…

《由浅入深学习SAP财务》:第2章 总账模块 - 2.6 定期处理 - 2.6.3 月末操作:外币评估

2.6.3 月末操作&#xff1a;外币评估 企业的外币业务在记账时一般使用期初的汇率或者即时汇率&#xff0c;但在月末&#xff0c;需要按照月末汇率对外币的余额或者未清项进行重估&#xff08;revaluation&#xff09;。 企业在资产负债表日&#xff0c;应当按照下列规…

nandgame中的Code generation(代码生成)

题目说明&#xff1a; 代码生成为语言的语法规则定义代码生成&#xff0c;以支持加法和减法。 您可以使用在前面级别中定义的堆栈操作&#xff08;如ADD和SUB&#xff09;。代码生成模板通常需要包含规则中其他符号的代码。 这些可以通过方括号中的符号名称插入。例如&#xf…

【初中生讲机器学习】15. EM 算法一万字详解!一起来学!

创建时间&#xff1a;2024-04-08 最后编辑时间&#xff1a;2024-04-10 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名初三学生&#xff0c;热爱计算机和数学&#xff0c;我们一起加…

西门子PLC(S7-200 SMART)学习笔记1:初识PLC可编程逻辑器件

今日开始我的西门子PLC学习之路&#xff0c;学习的型号以S7-200 SMART为主 主要认识一下PLC是什么、型号怎么看、 通信相关、编程软件、构造及工作原理 目录 西门子官方PLC手册获取&#xff1a; 1、PLC可编程逻辑器件的基本认识&#xff1a; PLC的结构及各部分的作用&#xff…