美文网首页
线性时间选择

线性时间选择

作者: ZakWind | 来源:发表于2019-01-26 15:02 被阅读0次

    一、概念

    给定线性序集中n个元素和一个整数k,1\leq k\leq n,要求找出这n个元素中第k小的元素。

    二、特殊情况(堆排序)

    k\leq n/\log nk\geq n-n\log n时,
    使用堆排序可以在O(n+k\log n)=O(n)时间内找出第k小元素。

    注:算法中log是以2为底的

    三、一般情况

    1.思路

    模仿快速排序算法,对输入数组进行递归划分。与快速排序不同的是,它只对划分出的子数组之一进行递归处理。

    2.缺点

    在最坏情况下,随机划分需要\Omega(n^2)时间计算。

    3.改进思路

    如果能在线性时间内找到一个划分基准,使得按这个基准所划分出的两个子数组长度都至少为原数组长度的\varepsilon倍(0<\varepsilon<1是某个正常数),那么在最坏情况下用O(n)时间就可以完成选择任务。例如,若\varepsilon=9/10,算法递归调用所产生的子数组的长度至少缩短1/10。所以,在最坏情况下,算法所需的计算时间T(n)满足递归式T(n)\leq T(9n/10)+O(n)。由此可得T(n)=O(n)

    4.改进方法

    (1)将n个输入元素划分成\lceil n/5\rceil个组,每组5个元素,除可能有一个组不是5个元素外。用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共\lceil n/5\rceil个。
    (2)递归调用Select找出这\lceil n/5\rceil个元素的中位数。如果\lceil n/5\rceil是偶数,就找它的两个中位数中较大的一个。然后以这个元素作为划分基准。

    5.C++代码

    #include <iostream>
    
    using namespace std;
    
    template<class Type>
    void Swap(Type &a, Type &b) {
        Type c = a;
        a = b;
        b = c;
    }
    
    //选择排序
    template<class Type>
    void SelectSort(Type a[], int p, int r) {
        for (int i = p; i < r; ++i) {
            int index = i;
            for (int j = i + 1; j <= r; ++j) {
                if (a[j] < a[index])
                    index = j;
            }
            Swap(a[i], a[index]);
        }
    }
    
    //按x划分,返回划分基准下标
    template<class Type>
    int Partition(Type a[], int p, int r, Type x) {
        int i = p - 1, j = r + 1;
        while (true) {
            while (a[++i] < x && i < r);
            while (a[--j] > x && j > p);
            if (i >= j)break;
            Swap(a[i], a[j]);
        }
        return j;
    }
    
    //找到中位数(用于找每组的5个数的中位数)
    template<class Type>
    int SearchMid(Type a[], int p, int r) {
        Type *b = new Type[r - p + 1];
        for (int i = p; i <= r; ++i) {
            b[i - p] = a[i];
        }
        SelectSort(b, 0, r - p);
        for (int i = p; i <= r; ++i) {
            if (a[i] == b[(r - p + 1) / 2])
                return i;
        }
        return 0;
    }
    
    //p第一个数下标,r最后一个数下标,k要找的第k个数
    template<class Type>
    Type Select(Type a[], int p, int r, int k) {
        //规模小于75时直接排序查找
        if (r - p < 75) {
            SelectSort(a, p, r);
            return a[p + k - 1];
        }
        //分成n/5组,每组5个;找到每组中位数,放置队首
        for (int i = 0; i <= (r - p - 4) / 5; ++i) {
            int mid = SearchMid(a, p + 5 * i, p + 5 * i + 4);
            Swap(a[mid], a[p + i]);
        }
        //找到所有中位数的中位数
        Type x = Select(a, p, p + (r - p - 4) / 5, (r - p - 4) / 10 + 1);
        //按中位数划分
        int 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);
    }
    
    int main() {
        return 0;
    }
    

    6.复杂度分析

    为了分析算法Select的计算时间复杂性,设n=r-p+1,即n为输入数组的长度。

    • 算法的递归调用只有在n\geq75时才执行。因此,当n<75时,算法Select所用的计算时间不超过一个常数C1
    • 找到中位数的中位数x后,算法Select以x为划分基准调用函数Partition对数组a[p:r]进行划分,这需要O(n)时间。
    • 算法Select的for循环体共执行n/5次,每一次需要O(1)时间。因此,执行for循环共需O(n)时间。

    设对n个元素的数组调用Select需要T(n)时间,那么找中位数的中位数x至多用T(n/5)时间。现已证明,按照算法所选的基准x进行划分所得到的两个子数组分别至多有3n/4个元素。所以无论对哪一个子数组调用Select都至多用T(3n/4)时间。
    总之,可以得到关于T(n)的递归式T(n)\leq\begin{cases}C1&n<75\\C2n+T(n/5)+T(3n/4)&n\geq75\end{cases}
    解此递归式可得T(n)=O(n)

    7.其他

    • 由于算法将每一组的大小定位5,并选取75作为是否进行递归调用的分界点。这两点保证了T(n)的递归式中两个自变量之和n/5+3n/4=19n/20=\alpha n0<\alpha<1。这是使T(n)=O(n)的关键之处。当然,除了5和75之外,还有其他选择。
    • 在算法Select中,假设所有元素互不相等,这是为了保证在以x为划分基准调用函数Paritition对数组a[p:r]进行划分之后,所得到的两个子数组的长度都不超过原数组长度的3/4。当元素可能相等时,应在划分之后加一条语句,将所有与基准x相等的元素集中在一起,如果这样的元素的个数m\geq1,而且j\leq k\leq j+m-1时,就不必再递归调用,只要返回a[i]即可。否则最后一行改为调用Select(i+m+1, r, k-j-m)

    相关文章

      网友评论

          本文标题:线性时间选择

          本文链接:https://www.haomeiwen.com/subject/ffdijqtx.html