第k小元素(顺序统计量)

在一个含有n个元素的集合中,有时我们需要找到第该集合中第 k 个小的元素,这也被称作第k个顺序统计量(order statistic)。在前面我们已学过排序, 我们可以将集合进行排序后,直接输出第 k 个元素。但是我们目前我们知道的排序中最好的时间复杂度就是O(nlog(n)). 接下来我们要使用两种方法找出第k小元素,其在某些过程用到排序,但我们并不会对整个集合进行排序,也不会使得最终的集合是顺序的。这两中方法一个是期望时间复杂度是 O(n) , 而另一个实际时间复杂度就是 O(n) .
这里写图片描述

1. 期望时间的选择算法

算法思想

这里我们要用到快速排序的思想,快速排序是将 1 关键字pivot_key排到整个集合中的正确位置,并将整个集合分成两个部分,左边都不比它大,而右边的都不比它小,如果我们现在将这个关键字pivot_key所在位置pivot_pos和我们的k(所要求的第 k 个顺序统计量)比较,若相等则返回;若小于则访问pivot_key左边的第k个顺序统计量;否则访问pivot_key右边的第 kpivot_pos 个顺序统计量(不包括pivot_key)。

算法实现

下面我们直接通过代码来说明问题:

int randomized_Partition(int *arr, int from, int to) {
    int pivot_key = arr[from];

    int low = from;
    int high = to;

    while (low < high) {
        while (low < high && pivot_key <= arr[high]) {
            --high;
        }
        arr[low] = arr[high];

        while (low < high && pivot_key >= arr[low]) {
            ++low;
        }
        arr[high] = arr[low];
    }
    arr[high] = pivot_key;
    return high;
}

上面的代码我们很容易就是快速排序将集合分成两部分,并放回pivot_key, 正确的位置。

int randomized_select(int *arr, int from , int to, int i) {
    if (from == to) {
        return arr[from];
    }

    // 算出arr[from] 所在的 pivot 位置
    int q = randomized_Partition(arr, from, to);

    // arr[from] 所在序列中第几个元素
    int k = q -from + 1;
    int result;
    if (i == k) {
        result = arr[q];
    } else if (i < k) {
        result = randomized_select(arr, from, q - 1, i);
    } else {
        result = randomized_select(arr, q + 1, to, i - k);
    }
    return result;
}

这剩下的这一部分代码就是我们上述提到的核心思想。

算法分析

z这里我们主要分析它为什么期望时间是 O(n) , 我们在这里假设 Xk 表示有k个元素的事件,且经过一次randomized_Partition我们要找的第 i 个顺序统计量都在Xk(即pivot_key的左边部分或右边部分)里,且 P(Xk=1)=1n ( Xk 这事件会发生的概率), 这样我们从上面的randomized_select可以分析到其时间复杂度 T() 满足如下关系:

T(n)k=1nXk(T(k)+O(n))

所以可得其期表达式:
E(T(n))E(k=1nXk(T(k)+O(n)))=k=1n(E(Xk)T(Xk))+O(n)=k=1n(1nT(Xk)+O(n))

而上述公式最终算出的期望值恰好为 T(n)=O(n)

2. 最坏情况为线性时间的选择算法

算法步骤

先假设我们这个算法函数为Select()
1) 将输入数组arr的 n 个元素划分为15, 每组 5 个元素,且之多只有一组剩下的nmod5个元素组成。
2) 找这 n5 组中每一组的中位数:首先对每组元素进行插入排序,然后确定每组有序元素的中位数。
3) 对第2步中找出的 n5 个中位数,递归调用Select()找出其中位数 x
4) 利用修改过的partition函数,找出这个x在整个数组arr中, 的正确位置 i ,这样就使得在数组arr中,左边的数都不比它大,而右边的数都不比它小。
5) 如果i=k, 则返回x。因此x就是第 k 小元素。如果i<k, 则在 x 的左边找第i小的元素。否则在 x 的右边(不包括x)找第 ik 小的元素。
t通过 上述算法描述,我们可以利用下面一张图进行形象化。
这里写图片描述

算法实现

插入排序

// 插入排序
void insertSorting(int *arr, int from, int to) {

    int i, j, t;
    int sc;
    for (i = from + 1; i <= to; ++i) {
        sc = arr[i];
        j = i - 1;
        while(j >= from) {
            // 找到一个不满足arr[j] <= sc
            if (arr[j] > sc) {
                --j;
            } else {
                break;
            }
        }
        // 将arr[j + 1, i - 1]向前移动一个位置
        // 即arr[j + 2, i]
        for (t = i; t > j; --t) {
            arr[t] = arr[t - 1];
        }
        arr[j + 1] = sc;
    }
}

改版的partition

// 讲指定位置pivot的元素作为主元
int partition(int *arr, int from, int to, int pivot) {
    int sc = arr[pivot];
    swap(arr + from, arr + pivot);
    int low = from;
    int high = to;
    while (low < high) {
        while (low < high && sc <= arr[high]) {
            --high;
        }
        arr[low] = arr[high];

        while (low < high && sc >= arr[low]) {
            ++low;
        }
        arr[high] = arr[low];
    }
    arr[low] = sc;
    return high;
}

上述算法核心就是BFPRT算法:

// 利用BFPRT 求解第k小元素
int BFPRT(int *arr, int from, int to, int ith) {
    // 当元素小于5个的时候直接插入并且返回
    if(to - from + 1 <= 5) {
        insertSorting(arr, from, to);
        return arr[from + ith - 1];
    }

    int t = from - 1;
    // 作为每5个数组的开始和结束的index
    int st, ed;
    // 至少有一组元素能进入这个循环, 也就是 to - from + 1 > 5
    for (st = from; (ed = st + 4) <= to; st += 5) {
        insertSorting(arr, st, ed);
        ++t;
        swap(arr + t, arr + st + 2);
    }

    // 关心的是中位数的位置,而不是中位数的值
    int pivot = (from + t) >> 1;
    BFPRT(arr, from, t, pivot - from + 1);

    int m = partition(arr, from, to, pivot);
    // pivot 为第几小
    int cur = m - from + 1;

    int res;
    if (cur == ith) {
        res = arr[m];
    } else if (cur < ith) {
        res = BFPRT(arr, m + 1, to, ith - cur);
    } else {
        res = BFPRT(arr, from, m - 1, ith);
    }
    return res;
}

算法分析

从上图分析,如果我们去掉数组arr最右边不足5个那一组,以及中位数 x 所在的那一组,我们可以判定x至少大于这么多数

3(12n52)3n106

也就是说,经过一次迭代或每一轮我们的就能确定至少有 3n10 x 小,则我们在最坏的情况,只需要和剩下的7n10+6个数进行新一轮迭代。
如果我们假设每次迭代都没找到我们的第 k 小元素,则我们会有如下关于时间复杂度的数学关系式:
f(n){O(1),T(n/5)+T(7n/10+6)+O(n),if n<140if n140

接下来我们着重分析 n140 的情况。我们的算法主要有 3 个耗时的步骤:

步骤 耗时
n/5个数组分别进行插入排序 O(n) n/5 个中位数查找他们之间的中位数 T(n/5) 最坏情况下,对剩下的元素查找第 k 小元素 T(7n/10+6)

f(n)=O(n) 就不详细数学公式证明(读者可以尝试使用 f(n)=an+b 来证明)。

讨论

  1. 期望为线性时间的选择算法和最坏情况为线性时间的选择算法有什么区别?
    期望为线性时间的选择算法每次都是选择的 arr[from...to] 中的第 1 个作为pivot_key,所以它下一次查找的子数组的个数是不确定,具有很大的随机性,所以它在概率下时间复杂度为O(n);而最坏情况为线性时间的选择算法它每次选择的pivot_key是特定的,这个特定的pivot_key使得至少有 3n106 个元素比其小或等于,在最坏情况下,我们只需要对剩下的 7n/10+6 个元素组成的子集进行查找我们需要的第 k <script type="math/tex" id="MathJax-Element-62">k</script>小元素。

致谢

本文是基于《算法导论》写的,最主要的是有本人大量的心得体会,感谢《算法导论》的那些作者Thomas H.Cormen、Charles E.Leiserson等 人。如果有错误的请留言,不甚感激。谢谢。

参考

《算法导论》Thomas H.Cormen、Charles E.Leiserson等 第三版 第9章 “中位数和顺序统计量”

源代码:

等上传完,附上

转载,请注明

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值