数据结构与算法分析:你真的理解排序算法吗——中值排序(万字长文+代码详解)

devtools/2024/10/23 8:05:28/

一、算法描述

在计算机科学中,分治是一种非常常见的方法,它将一个问题分成两个独立的子问题,每个子问题的规模是原始问题规模的一般。将两个原始数组细分成两个不同的子数组,每个子数组的大小是原始大小的一半,这两个子数组也同样需要排序。中值排序就这样递归的应用在每一个子数组上。值得注意的是, 中值排序并不是一个标准的算法>排序算法名称,但我们可以基于“中值”这个概念来讨论可能相关的排序方法。在计算机科学和数学中,“中值”是指一组数值中间的那个数。如果这组数有奇数个元素,那么中值就是正中间的那个数;如果有偶数个元素,中值通常是中间两个数的平均值。当我们提到与“中值”相关的排序方法时,可能指的是以下几种情况之一:
1.选择中值作为枢纽点的快速排序(Quick Sort)变种:
在快速算法>排序算法中,选择一个合适的枢纽点对于算法的效率至关重要。一种常见的优化方法是使用三数取中法(Median of Three),即从数组的第一个、最后一个和中间位置的三个数中选取中值作为枢纽点,以此来减少数据偏斜对快速排序性能的影响。
2.中值过滤排序:
这不是一种通用的算法>排序算法,但在某些特定的应用场景下,如图像处理中的噪声去除,会用到中值滤波器。中值滤波器通过计算邻域内的像素值的中值来代替中心像素的值,从而达到平滑图像的效果。虽然这不是一种算法>排序算法,但涉及到寻找一组数的中值的过程。
3.寻找中值的算法
寻找一组数的中值本身可以视为一个特殊的问题,可以通过不同的算法实现,比如快速选择算法(Quickselect)。这是一种用于找到第k小元素的选择算法,当k等于数组长度的一半时,该算法可以用来找到中值。快速选择算法的时间复杂度平均为O(n),最坏情况下为O(n^2)。
以下是中值排序的详细过程:

一、选择中值作为枢轴

子数组划分:
将待排序数组划分为若干个子数组,每个子数组包含固定数量的元素(例如5个)。
对于每个子数组,找到其真实的中值元素(即排序后位于中间的元素)。
中值选择:
从所有子数组的中值元素中,再次找到中值作为整个数组的枢轴。这一步可以使用快速选择算法或任何高效的查找中值的方法。

二、划分操作

初始枢轴位置:
将选定的中值枢轴元素放置到数组的某个位置(通常是数组的末尾或某个空位),以便进行后续的划分操作。
双向扫描:
使用两个指针(通常称为“哨兵”)从数组的两端向中间扫描。
一个指针用于查找小于枢轴的元素,另一个指针用于查找大于枢轴的元素。
当两个指针相遇或交错时,划分操作完成。
元素交换:
在扫描过程中,将小于枢轴的元素与左侧指针所指向的元素交换,将大于枢轴的元素与右侧指针所指向的元素交换。
确保枢轴元素最终位于其最终排序位置(即左侧所有元素都小于它,右侧所有元素都大于或等于它)。

三、递归排序

子数组划分:
根据枢轴元素的位置,将数组划分为两个子数组:左侧子数组(包含所有小于枢轴的元素)和右侧子数组(包含所有大于或等于枢轴的元素)。
递归调用:
对左侧子数组和右侧子数组分别进行递归排序。
递归调用中值算法>排序算法,直到子数组的大小减少到足够小(例如,小于某个阈值),此时可以使用其他算法>排序算法(如插入排序)进行排序。

四、合并结果

无需显式合并:
与归并排序不同,中值排序在递归过程中直接对子数组进行排序,因此无需显式地合并结果。
递归调用返回后,整个数组已经是有序的。

五、优化与注意事项

处理小数组:
对于较小的子数组,可以使用其他算法>排序算法(如插入排序)进行排序,以提高效率。
避免最坏情况:
通过选择中值作为枢轴,中值排序可以显著降低最坏情况(即划分极度不平衡)的发生概率。
并行化:
尽管中值排序的并行化相对复杂,但在硬件支持并行处理的情况下,可以考虑将排序任务分解为多个子任务,并在不同的处理器核心上并行执行。
内存使用:
中值排序通常是原地算法>排序算法,即它不需要额外的内存空间来存储排序过程中的中间结果。
综上可以看出,中值排序通过选择子数组的中值作为枢轴来确保划分后的两个子数组尽可能平衡,从而减少了最坏情况下的比较次数和移动次数。

二、复杂度分析

快速排序(使用中值作为枢纽点)
1.快速排序的基本时间复杂度:
最佳和平均情况下,快速排序的时间复杂度为 O(n log n)。
最坏情况下,如果每次选择的枢纽点都是最小或最大值,时间复杂度为 O( n 2 n^2 n2)。
2.使用中值作为枢纽点:
如果使用三数取中法(Median of Three)选择枢纽点,可以显著提高快速排序的性能,尤其是在数据已经部分排序的情况下。使用中值作为枢纽点,可以保证每次划分都比较均衡,从而使得快速排序在大多数情况下接近最佳情况,时间复杂度为 O(n log n)。
快速选择算法(用于寻找中值)
1.快速选择算法的基本时间复杂度:
平均情况下,快速选择算法的时间复杂度为 O(n)。
最坏情况下,时间复杂度为 O( n 2 n^2 n2),但这在实践中很少发生。
2.改进的快速选择算法
使用中值的中值(Median of Medians)作为枢纽点,可以保证快速选择算法在最坏情况下的时间复杂度为 O(n)。
在这里插入图片描述

三、适用情况

  1. 快速排序(使用中值作为枢纽点)
    适用环境:
    数据分布不均匀:当数据已经部分排序或存在大量重复值时,使用中值作为枢纽点可以避免最坏情况的发生,提高算法的性能。
    大数据集:快速排序是一种高效的算法>排序算法,适用于大规模数据集的排序任务。
    内存限制:快速排序是原地算法>排序算法,不需要额外的存储空间,适合内存受限的环境。
  2. 快速选择算法(用于寻找中值)
    适用环境:
    寻找第 k 小元素:快速选择算法特别适用于需要找到数组中第 k 小元素的场景,例如寻找中值(k = n/2)。
    统计分析:在统计学和数据分析中,经常需要找到一组数据的中值,快速选择算法可以高效地完成这一任务。
    实时系统:由于快速选择算法的平均时间复杂度为 O(n),它适合用于需要快速响应的实时系统。
  3. 中值滤波器(图像处理)
    适用环境:
    图像去噪:中值滤波器常用于去除图像中的椒盐噪声,因为它能够保留边缘信息,同时平滑噪声。
    信号处理:在信号处理中,中值滤波器可以用于去除脉冲噪声,保持信号的完整性。
    医学成像:在医学成像领域,中值滤波器可以用于改善图像质量,减少噪声干扰。
  4. 堆排序(使用中值优化)
    适用环境:
    需要稳定排序:堆排序是一种稳定的算法>排序算法,适用于需要保持相等元素相对顺序的场景。
    多线程环境:堆排序可以并行化,适用于多线程或多处理器环境。
    外部排序:在处理大规模数据集时,可以结合外部排序技术,使用堆排序进行局部排序。
    总结
    快速排序(使用中值作为枢纽点):适用于数据分布不均匀、大数据集和内存限制的环境。
    快速选择算法(用于寻找中值):适用于需要快速找到第 k 小元素、统计分析和实时系统的场景。
    中值滤波器:适用于图像去噪、信号处理和医学成像等领域。
    堆排序(使用中值优化):适用于需要稳定排序、多线程环境和外部排序的场景。

四、算法实现

使用中值排序的关键在于高效地从无序数组中选择出中值元素。我们先不回答这个问题,我们来看看另一个在计算机科学中非常典型的问题,这个问题最终能够给我们的初始问题一个很好的解决方案。想象一下一个人给了你一个函数p=partition(left,right, pivotIndex),这个函数将元素A[pivotlndex]作为一个特殊的中枢值,其将数组A[left,right]分成两半,其中一半中所有元素都小于等于中值元素,另一半的所有元素都大于等于中值元素。注意leftspivotlndexsright,返回值p是p在子序列A[left,righr]的最终位置。下面是这个partition函数的C实现:

#include<stdio.h>/*在线性时间, 根据给定的中枢值重新组合子序列ar[left, right],
将中枢值存储在其正确的位置store, 确保所有在子序列ar[left, store)中的
元素 <= pivot并且所有在ar[store + 1, right]中的元素 > pivot。*/int cmp(const void* x, const void* y)
{int standard;//这里比较时也是需要做强制类型转换standard = *(int*)x - *(int*)y;return standard;
}int partition(void **ar,int(*cmp)(const void *,const void *),int left,int right,int pivotIndex)
{int idx;int store;void* pivot = ar[pivotIndex];//将中枢值移动到数组结尾void* tmp = ar[right];ar[right] = ar[pivotIndex];ar[pivotIndex] = tmp;//将所有小于等于中枢值的元素都会移动到数组的前面,然后将中枢值插在它们后面store = left;for (idx = left;idx < right;idx++){if(cmp(ar[idx],pivot)<=0){tmp = ar[idx];ar[idx] = ar[store];ar[store] = tmp;store++;}}//又一次交换,中枢值插在后面tmp = ar[right];ar[right] = ar[store];ar[store] = tmp;return store;
}int main()
{int a = 5;int b = 7;int c = 4;int d = 6;int e = 3;int f = 8;int g = 1;int h = 0;int i = 9;int j = 2;void* base[10] = { &a,&b,&c,&d,&e,&f,&g,&h,&i,&j };int n = sizeof(base) / sizeof(base[0]);partition(base,cmp,0,n-1,5);for (i = 0;i < 10;i++){//printf("%d",*num[i]);这是错误的打印void数组方法,下面是正确的://将void*转换为int*并解引用  int* intPtr = (int*)base[i];printf("base[%d] = %d\n", i, *intPtr);}return 0;
}/*
我们是如何通过使用partition更加高效地选择中值呢?首先,让我们看看这个方法的
结果,以一个16个元素的无序数组为例。首先第一步是将中枢值和最右边的元素交换。
在partition每一次执行循环的时候,关键的变量如下图所示。store这个值用来区分
执行的是哪一次循环.下图每一次执行partition的循环都挑选出来了一个A[idx],这
个元素小于或者等于中枢值(在这里是元素“06”)。一旦不再有元素小于或者等于中枢
值,store这个位置的元素将会和最右边的元素进行交换,这样安全地将中枢值放到了
适当的位置。
*/

在这里插入图片描述

partition(0,15,9)执行并且返回p=5,你能看到A[left.p)中的所有元素都小于等于中枢值,反之,A[p+1,right]中的元素都大于等于中枢值。选择出中值元素的过程是怎样的呢?注意到p是在中值在有序数组的最终位置的左侧(标记为“中值位置”的黑色元素)。因此,p的左边没有元素会是中值!我们只需要反复调用partition(这次是一个不同的在A[p+1,right]中的A[pivotlndex])知道其返回p等于中值位置。注意到partition高效地将数组组织成两个不同的子数组,但是并没有排序每一个元素。partition返回中枢值的位置p,p能够用来递归地在A[left,righn]中检查第k个元素,
对于任何1<=k<=right-left+1,如下所示:
如果k=p+1
选择的中值元素是第k个值(回忆一下数组索引是从0开始的,但是这里的k却是从1开始)。
如果k<p+1
A的第k个元素是A[left.p]的第k个元素。
如果k>p+1
A的第k个元素是A[p+1,right]的第k-p个元素。
上图的目标是确定A的中值位置,或者换句话说,第八大(k=8)的元素。得到了调用partition的返回值p=5,我们下一步递归地在A[p+1,righi]中寻找第二大的元素。这个定义反复地在递归中使用,不过它也能够选代地在一个循环而不是在一个尾递归函数中定义(可以参考代码库中的这个例子)。selectkth是一个平均时间为线性的函数,给定一个适当的pivotIndex,其返回了数组ar中的第k个元素,下面是它的一个实现:

#include<stdio.h>/*
在ar中寻找第k大元素的位置的算法,其在平均情况下为线性时间。
A随着计算的进行不断被修改更新。注意1kright-left+1。
比较函数cmp将会严格地比较元素。最坏情况下性能为二次方,即o(n2)。
*/int cmp(const void* x, const void* y)
{int standard;//这里比较时也是需要做强制类型转换standard = *(int*)x - *(int*)y;return standard;
}int partition(void** ar, int(*cmp)(const void*, const void*), int left, int right, int pivotIndex)
{int idx;int store;void* pivot = ar[pivotIndex];//将中枢值移动到数组结尾void* tmp = ar[right];ar[right] = ar[pivotIndex];ar[pivotIndex] = tmp;//将所有小于等于中枢值的元素都会移动到数组的前面,然后将中枢值插在它们后面store = left;for (idx = left;idx < right;idx++){if (cmp(ar[idx], pivot) <= 0){tmp = ar[idx];ar[idx] = ar[store];ar[store] = tmp;store++;}}//又一次交换,中枢值插在后面tmp = ar[right];ar[right] = ar[store];ar[store] = tmp;//返回中值的位置return store;
}int selectpivotIndex(void** ar, int left, int right) 
{// 计算中间位置的索引int mid = (left + right) / 2;// 获取左端、中间和右端的值int left_val = *((int*)ar[left]);int mid_val = *((int*)ar[mid]);int right_val = *((int*)ar[right]);// 找出这三个值的中值if ((left_val < mid_val && mid_val < right_val) || (right_val < mid_val && mid_val < left_val)){return mid;}else if ((mid_val < left_val && left_val < right_val) || (right_val < left_val && left_val < mid_val)) {return left;}else{return right;}
}int selectKth(void** ar, int(*cmp)(const void*, const void*),int k,int left,int right)
{//找出中值int idx = selectpivotIndex(ar, left, right);//进行排序int pivotIndex = partition(ar, cmp, left, right, idx);if (left + k - 1 == pivotIndex){return pivotIndex;}//继续循环,并缩小范围,如果我们在中值的左边,那么k将不变。if (left + k - 1 < pivotIndex){return selectKth(ar, cmp, k, left, pivotIndex - 1);}else{return selectKth(ar, cmp, k - (pivotIndex - left + 1), pivotIndex + 1, right);}}int main()
{int a = 5;int b = 7;int c = 4;int d = 6;int e = 3;int f = 8;int g = 1;int h = 0;int i = 9;int j = 2;void* base[10] = { &a,&b,&c,&d,&e,&f,&g,&h,&i,&j };int n = sizeof(base) / sizeof(base[0]);int z=selectKth(base, cmp,5, 0, n - 1);printf("base[%d] = %d\n",z, *(int*)base[z]);return 0;
}

由于selectKth的特殊尾递归结构,一个非递归的实现会更直接一点。现在将讨论回到中值排序上来,你也许会非常惊讶,因为注意到无论pivotlndex的值是多少,selectkth都能很好地运行。当selectkth返回的时候,切分已经完成了。也就是说,左半部的元素都是小于或者等于中值的,反之,右半部的元素都是大于等于中值的。
下面的中值排序函数将要对A[0,n-1]进行排序,也是结合上面的函数的完整排序实现:

#include<stdio.h>/*
在ar中寻找第k大元素的位置的算法,其在平均情况下为线性时间。
A随着计算的进行不断被修改更新。注意1kright-left+1。
比较函数cmp将会严格地比较元素。最坏情况下性能为二次方,即o(n2)。
*/int cmp(const void* x, const void* y)
{int standard;//这里比较时也是需要做强制类型转换standard = *(int*)x - *(int*)y;return standard;
}int partition(void** ar, int(*cmp)(const void*, const void*), int left, int right, int pivotIndex)
{int idx;int store;void* pivot = ar[pivotIndex];//将中枢值移动到数组结尾void* tmp = ar[right];ar[right] = ar[pivotIndex];ar[pivotIndex] = tmp;//将所有小于等于中枢值的元素都会移动到数组的前面,然后将中枢值插在它们后面store = left;for (idx = left;idx < right;idx++){if (cmp(ar[idx], pivot) <= 0){tmp = ar[idx];ar[idx] = ar[store];ar[store] = tmp;store++;}}//又一次交换,中枢值插在后面tmp = ar[right];ar[right] = ar[store];ar[store] = tmp;//返回中值的位置return store;
}int selectpivotIndex(void** ar, int left, int right)
{// 计算中间位置的索引int mid = (left + right) / 2;// 获取左端、中间和右端的值int left_val = *((int*)ar[left]);int mid_val = *((int*)ar[mid]);int right_val = *((int*)ar[right]);// 找出这三个值的中值if ((left_val < mid_val && mid_val < right_val) || (right_val < mid_val && mid_val < left_val)){return mid;}else if ((mid_val < left_val && left_val < right_val) || (right_val < left_val && left_val < mid_val)){return left;}else{return right;}
}int selectKth(void** ar, int(*cmp)(const void*, const void*), int k, int left, int right)
{//三数取中法找出中值int idx = selectpivotIndex(ar, left, right);//进行排序,返回排序好的数组的中间值位置int pivotIndex = partition(ar, cmp, left, right, idx);//这里的比较很巧妙,排序好的数组的中间值的位置就是第几大,通过这种方法无需将数组完全排序完成就能找到第k大的元素if (left + k - 1 == pivotIndex){return pivotIndex;}//继续循环,并缩小范围,如果我们在中值的左边,那么k将不变。if (left + k - 1 < pivotIndex){return selectKth(ar, cmp, k, left, pivotIndex - 1);}else{return selectKth(ar, cmp, k - (pivotIndex - left + 1), pivotIndex + 1, right);}}//使用medianSort方法排序ar[left,right]。比较函数cmp将会严格的比较元素。
void mediansort(void** ar, int(*cmp)(const void*, const void*), int left, int right)
{//如果待排序的子数组只有一个(或者更少)元素,返回。if (right <= left){return;}//得到中点和中值元素位置k,(1<=k<=right-left-1)int mid = (right - left + 1) / 2;int me = selectKth(ar, cmp, mid + 1, left, right);mediansort(ar, cmp, left, left + mid - 1);mediansort(ar, cmp, left + mid + 1, right);
}int main()
{int a = 5;int b = 7;int c = 4;int d = 6;int e = 3;int f = 8;int g = 1;int h = 0;int i = 9;int j = 2;void* base[10] = { &a,&b,&c,&d,&e,&f,&g,&h,&i,&j };int n = sizeof(base) / sizeof(base[0]);int z = selectKth(base, cmp, 5, 0, n - 1);mediansort(base, cmp, 0, n - 1);for (i = 0;i < 10;i++){//printf("%d",*num[i]);这是错误的打印void数组方法,下面是正确的:// 将void*转换为int*并解引用  int* intPtr = (int*)base[i];printf("base[%d] = %d\n", i, *intPtr);}printf("中值为base[%d] = % d\n", z, *(int*)base[z]);return 0;
}

五、算法优化

中值排序做了很多不需要做的事。虽然生成的子问题是最优的(因为它们都是原始问题规模的一半左右),但产生子问题的额外开销应该考虑。在快速排序中可以看到,随机的选择pivotlndex就足够了,这样就能够避免退化的情况(如果原始数组是有序的,这种情况很可能会发生)。
中值排序保证递归子问题的规模都大致一样。这就意味着中值排序的平均性能是O(n log n)。但是,在最坏情况下,partition函数执行时间为0(n2),这样就使得中值排序的性能退化到O(n2)。因此,当n个元素几乎有序的时候,即使待递归排序的子问题是非常理想化的,总体性能也会受到影响。我们在中值排序中使用一个随机化的selectPivotIndex函数,来对最坏情况的数据进行测试,在这些数据上,selectPivotIndex总是返回最左边的位置。我们进行了10次试验,最好和最坏的结果都被排除,剩余8次实验的平均结果见下表。注意观察在最坏情况下,随着问题规模的加倍,中值排序的时间增加了4倍多,这就是经典的二次方算法的标志。
在这里插入图片描述
因此,看起来,任何依赖于切分的算法>排序算法的最坏情况都会退化到O(n2)。幸运的是,有一个线性时间的选择算法,能够确保最坏情况仍然是O(n log n)。这个选择算法被称为BFPRT(Blum-Floyd-Pratt-Rivest-Tarjan)算法(Blum等,1973),其性能见上表的最后一列。在统一生成的随机化数据上执行10次试验,最好和最坏的结果都被舍弃。下表给出了使用不同的切分子数组方法的中值排序的性能。
平均情况下,使用随机中枢选择算法的计算趋势线(见下表)是:
1.82 ∗ 1 0 − 7 n ∗ l o g ( n ) 1.82*10^{-7}n*\mathrm{log} (n) 1.82107nlog(n)
而BFPRT的趋势线是:
2.35 ∗ 1 0 − 6 ∗ n ∗ l o g ( n ) 2.35*10^{-6}*n*log(n) 2.35106nlog(n)
在这里插入图片描述
因为复杂的BFPRT算法的常数更高,所以10次运行都比较缓慢,即便如此其平均情况下的执行时间为O(nlogn)。
BFPRT选择算法能够保证性能稳定,因为这个算法在一个无序数组中选择的是一个可接受的近似中值。简单来说,BFPRT将含有n个元素的数组聚集成n/4个集合,每个集合有4个元素(忽略不能组成一个大小为4的集合的元素,)。BFPRT然后寻找到每一个4元集合的中值,这一步的开销是多少?从早前的下图的二叉决策树中,你能够回忆起来只需要5次比较就能给排序4个元素,所以这一步的开销最多是(n/4)5=1.25n,仍然是O(n)。得到这些4元集合,每一个中值是其第三个元素。我们将这些集合的中值作为集合M,M的中值(me)和原始集合A的中值非常近似。BFPRT算法用到的一个小技巧是递归地在集合M上使用BFPRT算法。编码这个算法是非常有意思的(例4-6是我们的实现,在这里我们通过递归地寻找固定距离、间隔的元素,最少化了元素交换操作)。注意到A中元素的3n/8个是显而易见的小于或者等于me,2n/8个也是显而易见的大于或者等于me。因此我们能够保证切分的递归调用在左子数组不会坏于37.5%,在右子数组不会坏于75%。这样就保证了BFPRT的总体最坏性能是O(n)。
在这里插入图片描述
下面是BFPRT算法的C语言实现:

#include<stdio.h>
#include<stdlib.h>#define SWAP(a,p1,p2,type){\type tmp_=a[p1];\a[p1]=a[p2];\a[p2] = tmp_;\
}int cmp(const void* x, const void* y)
{int standard;//这里比较时也是需要做强制类型转换standard = *(int*)x - *(int*)y;return standard;
}int partition(void** ar, int(*cmp)(const void*, const void*), int left, int right, int pivotIndex)
{int idx;int store;void* pivot = ar[pivotIndex];//将中枢值移动到数组结尾void* tmp = ar[right];ar[right] = ar[pivotIndex];ar[pivotIndex] = tmp;//将所有小于等于中枢值的元素都会移动到数组的前面,然后将中枢值插在它们后面store = left;for (idx = left;idx < right;idx++){if (cmp(ar[idx], pivot) <= 0){tmp = ar[idx];ar[idx] = ar[store];ar[store] = tmp;store++;}}//又一次交换,中枢值插在后面tmp = ar[right];ar[right] = ar[store];ar[store] = tmp;//返回中值的位置return store;
}//寻找在数组ar[left]、ar[left + gap]、ar[left + gap * 2]、ar[left + gap * 3]的4个元素的中值, 在结束的时候确保ar[left + gap * 2]包含有中值。
static void medianOffour(void** ar, int left, int gap, int(*cmp)(const void*, const void*))
{int pos1 = left;int pos2;int pos3;int pos4;void* a1 = ar[pos1];void* a2 = ar[pos2 = pos1 + gap];void* a3 = ar[pos3 = pos2 + gap];void* a4 = ar[pos4 = pos3 + gap];if (cmp(a1, a2) <= 0){if (cmp(a2, a3) <= 0){if (cmp(a3, a4) > 0){SWAP(ar, pos3, pos4, void *);}else{SWAP(ar, pos2, pos3, void*);}}else{if (cmp(a1, a4) <= 0){if (cmp(a2, a4) <= 0){SWAP(ar, pos2, pos3, void*);}else{SWAP(ar, pos3, pos4, void*);}}else{if (cmp(a1, a3) <= 0){if (cmp(a3, a4) <= 0){if (cmp(a2, a4) <= 0){SWAP(ar, pos2, pos3, void*);}else{SWAP(ar, pos3, pos4, void*);}}}else{if (cmp(a1, a4) <= 0){if (cmp(a2, a4) <= 0){SWAP(ar, pos2, pos3, void*);}else{SWAP(ar, pos1, pos3, void*);}}else{SWAP(ar, pos1, pos3, void*);}}}}}else{if (cmp(a1, a3) <= 0){if (cmp(a1, a4) <= 0){if (cmp(a3, a4) > 0){SWAP(ar, pos3, pos4, void*);}}else{;}}else{if (cmp(a2, a3) <= 0){if (cmp(a3, a4) <= 0){if (cmp(a1, a4) <= 0){SWAP(ar, pos1, pos3, void*);}else {SWAP(ar, pos3, pos4, void*);}}else {SWAP(ar, pos2, pos3, void*);}}}}  
}//使用间隔距离,对元素进行特定的插入排序
static void insertion(void** ar, int(*cmp)(const void*, const void*), int low, int right, int gap)
{int loc;for (loc = low + gap;loc <= right;loc += gap){int i = loc - gap;void* value = ar[loc];while (i >= low && cmp(ar[i], value) > 0){ar[i + gap] = ar[i];i -= gap;}ar[i + gap] = value;}
}/*
为ar[left,right]寻找合适的pivotIndex。考虑大小为b的集合,在这个代码中,b=4。
原始的BFPRT算法中b=5。
1.将元素分成floor(n/b)个集合,每个集合包含b个元素,在每一个集合中寻找中值元素。
然后将这些中值元素组成集合M。
2.如果|M|>b,那么递归地执行上一步,直到分成少于或者等于b个集合。
3.在递归的基础情况中,简单地使用插入排序来排序小于等于b个中值,
然后选择出这些中值中的中值。
*/static int medianOfMedians(void** ar, int(*cmp)(const void*, const void*), int left, int right, int gap)
{int s;int num;int span = 4 * gap;//元素不够组成集合?对这些元素插入排序,然后返回中值num = (right - left + 1) / span;if (num == 0){insertion(ar, cmp, left, right, gap);num = (right - left + 1) / gap;return left + gap * (num - 1) / 2;}//得到所有集合的中值for (s = left;s + span < right;s += span){medianOffour(ar, s, gap, cmp);}//如果仍然有足够的集合,那么增加gap的值,递归的应用在子数组[left,s-1]上,否则的话执行插入排序,返回中值if (num < 4){//基础情况insertion(ar, cmp, left + span / 2, right, span);return medianOfMedians(ar, cmp, left + span / 2, s - 1,span);}else{return medianOfMedians(ar, cmp, left + span / 2, s - 1, span);}}//在ar[left,right]中寻找中值的最坏情况为线性的算法,比较函数cmp用来比较元素。
int selectMedian(void** ar, int(*cmp)(const void*, const void*), int left, int right)
{int k = (right - left + 1) / 2;while (k > 0){//选择切分的时候应该根据元素的索引int idx = medianOfMedians(ar, cmp, left, right, 1);/*根据中值集合的中值x切分输入数组。如果能够找到第k大的元素,返回其素引,否则在A[left,pivotIndex-1]寻找第k大元素或者在A[pivotIndex+1,right]中寻找(k-p)大元素在A[Pivot  Index+1,  right]中。 */int pivotIndex = partition(ar, cmp, left, right, idx);//注意0<=k<=right-left,返回值pivotIndex是left<=pivotIndex<=rightint p = left + k;if (p == pivotIndex){return pivotIndex;}else if(p<pivotIndex){right = pivotIndex - 1;}else{k = k - (pivotIndex - left + 1);left = pivotIndex + 1;}}//如果执行到这里,那么left=right,所以仅仅只需要返回两个中的一个作为中值。return left;
}// 打印数组  
void printArray(int* ar, int size) {for (int i = 0; i < size; i++) {printf("%d ", ar[i]);}printf("\n");
}int main() 
{// 示例整数数组  int arr[] = { 5, 7, 4, 6, 3, 8, 1,0,9,2 };int size = sizeof(arr) / sizeof(arr[0]);// 创建指向整数的指针数组  void** ptrArr = (void**)malloc(size * sizeof(void*));for (int i = 0; i < size; i++) {ptrArr[i] = &arr[i];}// 找到中值的索引  int medianIndex = selectMedian(ptrArr, cmp, 0, size - 1);// 打印结果  printf("中值索引: %d\n", medianIndex);printf("中值元素: %d\n", arr[medianIndex]);// 释放动态分配的内存  free(ptrArr);return 0;
}

总而言之,中值排序的优化可以从以下几个方面入手:

一、枢轴选择的优化

三数取中法:
在选择枢轴时,不直接选择子数组的第一个、最后一个或中间元素,而是从子数组的首部、尾部和中间位置各取一个元素,然后取这三个数的中值作为枢轴。这种方法可以减少最坏情况发生的概率,即避免划分后的两个子数组极度不平衡。
随机选择枢轴:
通过随机选择一个元素作为枢轴,可以进一步降低最坏情况发生的概率,使算法的平均性能更加稳定。随机选择枢轴可以通过生成一个随机数,然后将其映射到子数组的索引范围内来实现。

二、划分过程的优化

双向扫描法:
在划分过程中,可以使用两个指针(哨兵)分别从数组的两端向中间扫描,一个指针寻找大于枢轴的元素,另一个指针寻找小于枢轴的元素,然后交换这两个元素的位置。这种方法可以减少不必要的元素移动,提高算法的效率。
尾递归优化:
在递归调用中,如果可能的话,优先对较小的子数组进行递归,而对较大的子数组进行迭代处理。这可以减少递归调用的深度,从而降低栈空间的使用。

三、处理小数组的优化

切换到其他算法>排序算法
当子数组的大小小于某个阈值时,可以切换到其他算法>排序算法,如插入排序或选择排序。这些算法在处理小规模数据时具有较高的效率。
使用内存块移动:
在插入排序等算法中,可以通过使用内存块移动而不是单个元素的交换来改善性能。内存块移动可以减少内存访问的次数,从而提高算法的效率。

四、并行化与分布式处理

多线程/多进程并行:
如果硬件支持多线程或多进程并行处理,可以将排序任务分解为多个子任务,并在不同的线程或进程中并行执行。这可以显著提高排序的速度,但需要注意线程或进程之间的同步和通信问题。
分布式排序:
在处理大规模数据集时,可以考虑使用分布式算法>排序算法。这些算法将数据分布在多个节点上进行处理,然后通过网络通信来协调各个节点的操作。分布式排序可以充分利用集群的计算资源,提高排序的效率。

五、其他优化策略

减少内存使用:
在实现算法>排序算法时,应尽量减少内存的使用。例如,可以通过原地排序的方式来减少额外的内存开销。
优化比较函数:
如果排序的对象是自定义的数据类型,应确保比较函数尽可能高效。比较函数的性能会直接影响算法>排序算法的整体性能。
避免不必要的元素复制:
在排序过程中,应尽量避免不必要的元素复制。例如,在划分过程中,可以直接交换元素的位置,而不是先复制到一个临时数组中再交换回来。

六、引用及参考文献

算法设计手册》


http://www.ppmy.cn/devtools/128107.html

相关文章

docker配置mysql8报错 ERROR 2002 (HY000)

通过docker启动的mysql&#xff0c;发现navicat无法连接&#xff0c;后来进入容器内部也是无法连接&#xff0c;产生以下错误 root9f3b90339a14:/var/run/mysqld# mysql -u root -p Enter password: ERROR 2002 (HY000): Cant connect to local MySQL server through socket …

《利用合成数据从临床数据仓库中自动检测脑部T1加权磁共振图像中的运动伪影》|文献速递-基于生成模型的数据增强与疾病监测应用

Title 题目 Automatic motion artefact detection in brain T1-weighted magnetic resonance images from a clinical data warehouse using synthetic data 《利用合成数据从临床数据仓库中自动检测脑部T1加权磁共振图像中的运动伪影》 Background 背景 近年来&#xff0…

校园电气火灾的精准防控“智”胜未来

在知识的殿堂里&#xff0c;每一缕光明都承载着未来的希望&#xff0c;而电力的稳定与安全&#xff0c;则是这希望之光的坚实基石。近年来&#xff0c;随着高校规模的不断扩大与电气化设备的日益增多&#xff0c;电力系统的安全保障成为了不容忽视的重大课题。电气火灾&#xf…

使用DQL命令查询数据(一)

DQL DQL(Data Query Language&#xff0c;数据查询语言)&#xff1a; 查询数据库数据&#xff0c;如SELECT语句。 简单的单表查询或多表的复杂查询和嵌套查询。 数据库语言中最核心、最重要的语句。 使用频率最高的语句。 SELECT语句&#xff1a; SELECT [ALL|DISTINCT] { *…

云电脑的真实使用体验

最近这几年&#xff0c;关于云电脑的宣传越来越多。 小枣君之前曾经给大家介绍过云电脑&#xff08;链接&#xff09;。简单来说&#xff0c;它属于云计算的一个应用。通过在云端虚拟出一些虚拟电脑&#xff0c;然后让用户可以远程使用&#xff08;仍然需要借助本地电脑&#x…

UDP/TCP协议

网络层只负责将数据包送达至目标主机&#xff0c;并不负责将数据包上交给上层的哪一个应用程序&#xff0c;这是传输层需要干的事&#xff0c;传输层通过端口来区分不同的应用程序。传输层协议主要分为UDP&#xff08;用户数据报协议&#xff09;和TCP&#xff08;传输控制协议…

C++学习路线(十九)

函数返回值指针 #include <iostream> using namespace std;int* add(int x, int y) {// 定义一个指针int* sum NULL;// 让指针指向堆内存 也就是sum的值是堆的地址sum new int;*sum x y;// 返回指针 以拷贝的方式返回// 也就是 外部的sum指针指向的地址和堆内存的地…

三维重建新范式对比与发展趋势

1.概述 本文重点对比三维视觉的新范式&#xff0c;主要是NeRF与 3D gausslain splatting在3维重建发展的趋势进行对比与说明。 2.NeRF趋势 3.3D GS趋势 4.动态场景重建的趋势