“分治算法 求第k小元素 O(n) & O(nlog2^n)”

容易想到的算法是采用一种排序算法先将数组按不降的次序排好,然后从排好序的数组中捡出第k个元素。这样的算法在最坏情况下时间复杂度是O(nlog2^n)。

实际上,我们可以设计出在最坏情况下的时间复杂度为O(n)的算法。

利用分治算法并结合快排思想,很容易达到O(n)的时间复杂度。其核心思想在于快排中基准的选取。(根据严蔚敏版教材,一般直接选取第一个元素作为快排基准。但求第k小元素,则依赖于一种中值选取法,以加速剪枝)。

阅读以下内容时,需要先学习快排算法,可以看看这篇文章《[Data Structure]九大内部排序算法》。

下面举个例子,如何达到O(n)选取第k小的元素。

问题:如何在O(n)内,确定A[17]中第k小的元素? A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

这里给出一个分治算法,选取基准的求解过程。

step1 设置一个值r,将A[15]分为长度为r的几个组。(假设r = 5)

A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

                 G1               G2                   G3                  G4

分组结果,G1[5] = {0, 1, 2, 3, 4} ;G2[5] = {5, 6, 7, 8, 9};G3[5] = {10, 11, 12, 13, 14};G4[2] = {15, 16}。这里注意第四组,只有2个元素。

step2 分别求取G1~4组别中的中值,G1 = 2, G2 = 7, G3 = 12, G4 = 15。

并由这四个数再组成一个数组G[4] = {2, 7, 12, 15}。

step3 求得G[4] = {2, 7, 12, 15}数组中的中值,mid = 7。

那么,mid = 7便是最终选取出来的快排划分基准。

以下程序中,partition函数(第13行)以上部分即为上述基准选取过程。这里需要说明的是,求得中值后,程序中并没有开辟额外空间,而是在原有A[]基础上进行操作的,将中值放在最前面,故需要swap以保证数据信息不丢失。

//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
    int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,总共分为r_group个组
    //计算每个分组中值,存于A[]最前面
    for (int i = 1; i <= r_group; ++i) {
        sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
        swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
    }
    //获得每个组的中值的中值(并置于A[low]位置,方便调用快排划分函数)
    sort(&A[low], &A[low + r_group]);
    swap(A[low], A[low+r_group / 2]);
    int cur = partition(A, low, high);
    if (cur == k-1){
        return A[cur];
    }
    else if (cur < k){
        return select_rank_k(A, cur + 1, high, k);
    }
    else{
        return select_rank_k(A, low, cur - 1, k);
    }

}

程序中其它执行步的时间复杂度都至多是n的倍数。如果用T(n)表示算法在数组长度为n的时间复杂度,则当n≥24时,有递归关系

其中c是常数。从上述递推关系式出发,用数学归纳法可以证明,

所以,在最坏情况下,select_rank_k算法的时间复杂度是O(n)。

最后,对整个问题抽象以下,并给出完整DEMO。问题:已知n元数组A[1..n],试确定其中第k小的元素。

补充:36行需修正为swap(A[low], A[low+r_group / 2]);

#include <stdio.h>
#include <algorithm>
#include <math.h>
using namespace std;

//划分——每次划分唯一确定一个元素位置

int partition(int A[], int low, int high)
{
    int pivot = A[low];    //一般采用严蔚敏教材版本,以第1个位置为基准
    while (low < high){
        while (low < high && A[high] >= pivot){
            --high;
        }
        A[low] = A[high];  //将比基准小的元素移动到左端
        while (low < high && A[low] <= pivot){
            ++low;
        }
        A[high] = A[low];  //将比基准小的元素移动到右端
    }
    A[low] = pivot;
    return low;
}

int r = 5;
//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
    int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,总共分为r_group个组
    //计算每个分组中值,存于A[]最前面
    for (int i = 1; i <= r_group; ++i) {
        sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
        swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
    }
    //获得每个组的中值的中值(并置于A[low]位置,方便调用快排划分函数)
    sort(&A[low], &A[low + r_group]);
    swap(A[low], A[r_group / 2]);
    int cur = partition(A, low, high);
    if (cur == k-1){
        return A[cur];
    }
    else if (cur < k){
        return select_rank_k(A, cur + 1, high, k);
    }
    else{
        return select_rank_k(A, low, cur - 1, k);
    }
}
int main(void)
{
    int A[15] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
    printf("%d\n", select_rank_k(A, 0, 3, 2));
    return 0;
}

从此dalao 博客上转载:https://blog.csdn.net/qingdujun/article/details/78605575

若您觉得此篇博客写得不错,请别忘了关注我哦 >_<

猜你喜欢

转载自blog.csdn.net/cza0927/article/details/81414477