日期:2020.11.10
作者:YJF
问题描述:重复元素的Top.K排序推演(朴素快排+BFPTR+三向切分最优熵)
朴素快排
快速排序是一种分治的算法,从思想上来说,它将一个数据切分成两部分,然后将两部分独立排序,这种方式正好与归并排序正好互补。
快排与归并的区别
简单来说,归并是将数组分成两个子数组,把子数组排好序后再归并为有序数组;快排是在将数组分为两个子数组的过程中就把子数组的顺序排好,达到数组有序的目的。
快速排序的切分
//切分为a[lo...i-1],a[i],a[i+1...hi]
int partition(Comparable[] a, int lo, int hi){
//左右指针
int i = lo, j = hi + 1;
//切分元素
Comparable v = a[lo];
//左右同时扫描,并交换
while(true){
while(less(a[++i],v)) if(i == hi) break;
while(less(v,a[--j])) if(j == lo) break;
if(i >= j) break;
exch(a, i, j);
}
//把切分元素放到合适的位置
exch(a, lo, j);
return j;
}
快速排序
//快速排序
void sort(Comparable[] a, int lo, int hi){
if(lo >= hi) return;
//找到切分元素的下标
int v = partition(a, lo, hi);
//将左边部分排序
sort(a, lo, v-1);
//将右半部分排序
sort(a, v+1, hi);
}
性能分析
-
简洁性
内循环过程中用一个递增的索引将数组元素与切分元素比较,移动数据的次数少。
-
局限性:
在切分不平衡时,快排的效率极为低下,最优的情况是每次切分都正好将切分元素放在中间。
对于该局限性,可以采用让数组混乱的方式加速,也可以在子数组元素个数较小的时候切换为插入排序(元素个数较少时,大多数情况下,插入排序优于快速排序)。
void sort(Comparable[] a, int lo, int hi){ //M为一个常数 if(lo + M >= hi){ Insertion.sort(a, lo, hi); return; } //找到切分元素的下标 int v = partition(a, lo, hi); //将左边部分排序 sort(a, lo, v-1); //将右半部分排序 sort(a, v+1, hi); }
BFPTR:Top.K的经典解决方法
在我们寻找Top.K的数字时,可以知道对整个数组排序是没有必要的,因此需要对朴素快排进行优化,因此出现了BFPTR算法。
算法思路:
- 将数组分以每五个元素为一个单位的n/5个子数组。
- 用插入排序找到这些子数组中的中位数,并将它们从数组中提取出来,组成中位数数组。
- 从中位数数组中通过循环利用 2. 一样的方式,找到中位数的中位数 MidofMid。
- 利用 MidofMid 进行分割,类似于快排的 partion ,让 K 比分割的较小部分多一,那么Top.K就是当前的切分元素 num 了。
- 如果 K == num,则返回 num 的值,如果 K < num ,就对较小部分进行BFPTR,如果 K > num,就对较大部分进行BFPTR。
BFPTR与快排的区别
在对快排的性能分析过程,我们能够发现,快排的效率取决于数组的元素排序,为了避免最坏情况的发生,BFPTR便是再次利用分治的经典思想,利用对基准值的选择,达到该目的。
伪码
BFPTR 的 MidofMid 查找
//获得中位数的中位数
int getMidOfMid(Comparable[] a, int lo, int hi){
//获得中位数下标,M在此时等于5
if (hi - lo < 5)
return Insertion.sort(a, lo, hi);
//找到中位数数组的边界
int sub_hi = lo - 1;
//找到中位数,并将它们放在数组的一端,
//因为找的是Top.k小,所以我放在lo端
for (int i = lo; i + 4 <= hi; i += 5){
int index = Insertion.sort(a, i, i + 4);
exch(a, ++sub_hi, index);
}
return BFPRT(a, lo, sub_hi, ((sub_hi - lo + 1)/2) + 1);
}
BFPTR的切分(类似于Qsort.partion)
//以中位数为切分元素,进行切分
int partition(Comparable[] a, int lo, int hi, int MidOfMid){
exch(a, MidOfMid, hi);
int v = lo;
for (int i = lo; i < hi;; i++){
if (less(a, i, hi)
exch(a, v++, i);
}
exch(a, v, hi);
return v;
}
BFPTR
int BFPRT(Comparablep[] a, int lo, int hi, const int & k)
{
//得到中位数的中位数下标
int MidOfMid = getMidOfMid(a, lo, hi);
//进行划分,返回划分边界
int v = partition(a, lo, hi, MidOfMid);
//直到num==k即为Top.k
int num = v - lo + 1;
if (num == k)
return v;
else if (num > k)
return BFPRT(a, lo, v - 1, k);
else
return BFPRT(a, v + 1, hi, k - num);
}
性能分析
-
复杂度分析
对于一个规模为n的输入,其最坏时间复杂度为:
因此,BFPTR的最坏复杂度为:
-
局限性
当有重复元素时,BFPTR 算法在某种情况下便不再适用,当 K > num 时,如果 num 重复,则在对较大部分进行BFPTR时,重复的数字会占用一个位置,因此,此时的 num 只能代表下标,而不是 Top.K 。
三向切分最优熵
三向切分的思路和快排也很相似,但它是将数组分为三个子数组,在这里,中位数便代表一种子数组。
算法思路:
- 一个指针 lt 使得 a[lo…lt-1] 的元素都比切分元素 v 小
- 一个指针 gt 使得 a[gt+1…hi] 的元素都比切分元素 v 大
- 一个指针 i 使得与 v 一样的元素时,则放在 a[it…i-1] 中,因为此时 a[i…gt] 的元素还没有确定。
伪码
三向切分
void Quick3way(Comparable[] a, int lo, int hi){
if(lo >= hi) return;
//将数组分为三个子数组 a[lo...lt-1] a[it...gt] a[gt+1...hi]
int lt = lo, i = lo + 1, gt = hi;
Comparable v = a[lo];
while(i <= gt){
int cmp = a[i].comparaTo(v);
if(cmp < 0) exch(a, lt++, i++);
else if(cmp > 0) exch(a, i, gt--);
else i++;
}
Quick3way(a, lo, lt - 1);
Quick3way(a, gt + 1, hi);
}
性能分析
- 时间复杂度
对于包含大量重复元素的数组,它将排序时间从线性对数级别降低到了线性级别,三项切分的最坏情况应该是所有元素都不相同的时候。
- Top.K的三向切分算法
//外置一个全局数组用来储存结果即可
//实际上是为了得到vector的大小
vector<Comparable> res;
int Quick3way(Comparable[] a, int lo, int hi, const int &k){
//当没有找到Top.K时,将继续排序
if(res.size()!=k){
//每排好一组就把数据存入res中
if(lo >= hi){
res.push_back(a[lo]);
return 0;
}
//将数组分为三个子数组 a[lo...lt-1] a[it...gt] a[gt+1...hi]
int lt = lo, i = lo + 1, gt = hi;
Comparable v = a[lo];
while(i <= gt){
int cmp = a[i].comparaTo(v);
if(cmp < 0) exch(a, lt++, i++);
else if(cmp > 0) exch(a, i, gt--);
else i++;
}
Quick3way(a, lo, lt - 1, k);
Quick3way(a, gt + 1, hi, k);
}
return res.back();
}