重复元素的Top.K排序推演(朴素快排+BFPTR+三向切分最优熵)

日期:2020.11.10

作者:YJF


问题描述:重复元素的Top.K排序推演(朴素快排+BFPTR+三向切分最优熵)

朴素快排

快速排序是一种分治的算法,从思想上来说,它将一个数据切分成两部分,然后将两部分独立排序,这种方式正好与归并排序正好互补。

快排与归并的区别

简单来说,归并是将数组分成两个子数组,把子数组排好序后再归并为有序数组;快排是在将数组分为两个子数组的过程中就把子数组的顺序排好,达到数组有序的目的。

img

(该动图来自于菜鸟教程的快速排序)
### 伪码
快速排序的切分
//切分为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算法。

算法思路:

  1. 将数组分以每五个元素为一个单位的n/5个子数组。
  2. 用插入排序找到这些子数组中的中位数,并将它们从数组中提取出来,组成中位数数组。
  3. 从中位数数组中通过循环利用 2. 一样的方式,找到中位数的中位数 MidofMid。
  4. 利用 MidofMid 进行分割,类似于快排的 partion ,让 K 比分割的较小部分多一,那么Top.K就是当前的切分元素 num 了。
  5. 如果 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 。

三向切分最优熵

三向切分的思路和快排也很相似,但它是将数组分为三个子数组,在这里,中位数便代表一种子数组。

算法思路:

  1. 一个指针 lt 使得 a[lo…lt-1] 的元素都比切分元素 v 小
  2. 一个指针 gt 使得 a[gt+1…hi] 的元素都比切分元素 v 大
  3. 一个指针 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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中南大学苹果实验室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值