分治法——线性时间选择

分治法——线性时间选择

问题:给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素,算法的复杂度为O(n)。

思路分析:

首先,假如我们要找最大或者最小的元素,那么只需遍历一遍序列即可,复杂度为O(n)。假如要找第k大的元素,k<=n/logn或者k>=n-nlogn时,利用堆排序,时间复杂度仍然可以达到O(n),具体为什么还没搞懂。无论如何,如果对于任意的k,(最坏)时间复杂度要想达到O(n),对整个序列进行排序都是不可行的(复杂度至少为nlogn)

那么如何将复杂度降低到O(n)呢?由于我们求的仅仅是第k大的数字,那么可以不用整个对整个序列进行排序,而仅仅保证在第k大的数字前面的数字都小于这个第k大数即可。借助快排的思想,每次找一个哨兵将序列分为大于哨兵和小于哨兵的两部分,根据需要求的数是第k大选择一个子序列递归进行排序即可,而非对所有的序列都要排序。

假如能够做到在任意情况下,所选的哨兵将序列等比地分为两部分,就可以做到线性地得到结果:设子序列的长度分别为ne和n(1-e),复杂度递推公式为T(n) = T(ne)+O(n),时间复杂度为O(n)。

如何保证在任意情况下哨兵都会做到等比分割序列呢?传统的快速排序算法在选取哨兵时采用序列的第一个元素,这样在极端情况下会退化为等差递减的递归,也就是冒泡;随机快速排序算法对此进行了一些优化,每次随机选取哨兵,但是本质上也是一样。将序列预划分可以有效地解决这一问题:

1.将序列划分成n/5个子序列,每个子序列有5个元素,最后一个不足5个元素的子序列扔掉不要(因为目的只是为了找一个差不多的基准哨兵),任选一种排序方法对每个子序列进行排序,对每个子序列取中位数

2.取出中位数后,对n/5个中位数再取中位数,这里其实转化为中位数中求解第k大的数的子问题

3.以确定的中位数作为哨兵,进行划分序列,可以证明,当n>=75时,此时子序列近似划分为n/4和3n/4两个部分

复杂度递推公式为T(n)=T(n/5)+T(3n/4)+O(n),n>=75;T(n)=O(1),n<75

代码:

// 将序列a[]重排为mid左边的元素全部小于mid,mid右边的元素全部大于mid
// 输入:序列a,序列起始下标l,终止下标r,哨兵大小mid
// 输出:mid在序列a中的位置j,据此可确定划分完成的两个子序列
int part(int a[],int l,int r,int mid){
    
    int i = l,j = r;
    while(i<=r && j>=l){
        while(a[i]<=mid){i++;}
        while(a[j]>=mid){j--;}
        if(i>=j){
            break;
        }else{
            swap(a[i],a[j]);
            i++;
            j--;
        }
    }
    return j;
}

// 选取序列a中序号从l到r中第k小的元素
// 输入:序列a,序列起始下标l,序列终止下标r,所求的元素是第k小
// 输出:第k小的元素大小
int select(int a[],int l,int r,int k){
    // 处理最小子问题:n<75
    if(r-l<75){
        sort(a+l, a+r+1);
        return a[l+k-1];
    }else{
        // 选出n/5个组中的中位数,每次将中位数和序列开始的位置交换,
        // 将中位数聚集在一起,比额外开辟内存空间然后记录下标和值要好
        for(int i = 0;i <= (r-l-4)/5;i++){
            sort(a+l+5*i,a+l+5*i+4);
            swap(a[l+i],a[l+5*i+2]);
        }
        // 获取中位数的中位数
        int mid = select(a, l, l+(r-l-4)/5, ((r-l-4)/5+1)/2);
        // 以mid为基准进行子序列的划分
        int mid_id = part(a, l, r, mid);
        int mid_rank = mid_id - l + 1;
        // 判断应该取哪个子序列进行递归
        if(k == mid_rank)
            return a[mid_id];
        else if(k > mid_rank)
            return select(a, mid_id+1, r, k-mid_rank);
        else
            return select(a, l, mid_id-1, k);
        
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值