[关闭]
@Zh1Cheung 2018-03-16T12:30:22.000000Z 字数 3386 阅读 973

BFPRT算法(TOP-K问题)

一:背景介绍

在一堆数中求其前k大或前k小的问题,简称TOP-K问题。而目前解决TOP-K问题最有效的算法即是"BFPRT算法",又称为"中位数的中位数算法",该算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最坏时间复杂度为

在首次接触TOP-K问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前k即可,但是这么做有两个问题:

除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为k的堆,时间复杂度为

那是否还存在更有效的方法呢?BFPRT算法的做法就是在快速排序的基础上,通过判断主元位置与k的大小使递归的规模变小,其次通过修改快速排序中主元的选取方法来降低快速排序在最坏情况下的时间复杂度

下面先来简单回顾下快速排序的过程,以升序为例:

(1):选取主元(数组中随机一个元素);
(2):以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
(3):分别对左边和右边进行递归,重复上述过程。

二:BFPRT算法过程及代码

BFPRT算法步骤如下:

(1):选取主元;
  (1.1):将n个元素划分为个组,每组5个元素,若有剩余,舍去;
  (1.2):使用插入排序找到个组中每一组的中位数;
  (1.3):对于(1.2)中找到的所有中位数,调用BFPRT算法求出它们的中位数,作为主元;
(2):以(1.3)选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
(3):判断主元的位置与k的大小,有选择的对左边或右边递归。

上面的描述可能并不易理解,先看下面这幅图:

BFPRT()调用GetPivotIndex()和Partition()来求解第k小,在这过程中,GetPivotIndex()也调用了BFPRT(),即GetPivotIndex)和BFPRT()为互递归的关系。

下面为代码实现,其所求为前K小的数

#include <iostream>
#include <algorithm>

using namespace std;

/* 插入排序,返回中位数下标 */
int InsertSort(int array[], int left, int right)
{
    int temp;
    int j;

    for (int i = left + 1; i <= right; i++)
    {
        temp = array[i];
        j = i - 1;
        while (j >= left && array[j] > temp)
            array[j + 1] = array[j--];
        array[j + 1] = temp;
    }

    return ((right - left) >> 1) + left;
}

/* 返回中位数的中位数下标 */

int BFPRT(int array[], int left, int right, const int & k);

int GetPivotIndex(int array[], int left, int right)
{
    if (right - left < 5)
        return InsertSort(array, left, right);

    int sub_right = left - 1;

    for (int i = left; i + 4 <= right; i += 5)
    {
        int index = InsertSort(array, i, i + 4);  // 找到五个元素的中位数的下标
        swap(array[++sub_right], array[index]);   // 依次放在左侧
    }

    return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1);
}

/* 利用中位数的中位数的下标进行划分,返回分界线下标 */
int Partition(int array[], int left, int right, int pivot_index)
{
    swap(array[pivot_index], array[right]);  // 把主元放置于末尾
    int divide_index = left;                 // 跟踪划分的分界线

    for (int i = left; i < right; i++)
    {
        if (array[i] < array[right])
            swap(array[divide_index++], array[i]);  // 比主元小的都放在左侧
    }

    swap(array[divide_index], array[right]);  // 最后把主元换回来

    return divide_index;
}

int BFPRT(int array[], int left, int right, const int & k)
{
    int pivot_index = GetPivotIndex(array, left, right);            // 得到中位数的中位数下标
    int divide_index = Partition(array, left, right, pivot_index);  // 进行划分,返回划分边界
    int num = divide_index - left + 1;

    if (num == k)
        return divide_index;
    else if (num > k)
        return BFPRT(array, left, divide_index - 1, k);
    else
        return BFPRT(array, divide_index + 1, right, k - num);
}

int main()
{
    int k = 5;
    int array[10] = { 1,1,2,3,1,5,-1,7,8,-10 };

    cout << "原数组:";
    for (int i = 0; i < 10; i++)
        cout << array[i] << " ";
    cout << endl;

    cout << "第" << k << "小值为:" << array[BFPRT(array, 0, 9, k)] << endl;

    cout << "变换后的数组:";
    for (int i = 0; i < 10; i++)
        cout << array[i] << " ";
    cout << endl;

    return 0;
}

运行如下:

三:时间复杂度分析

BFPRT算法在最坏情况下的时间复杂度是,下面予以证明。令为所求的时间复杂度,则有:

其中:

,其中t为未知,它可以是一个正常数,也可以是一个关于n的函数,代入上式:

其中c为一个正常数,故t也是一个正常数,即,因此,至此证明结束。

接下来我们再来探讨下BFPRT算法为何选5作为分组主元,而不是2, 3, 7, 9呢?

首先排除偶数,对于偶数我们很难取舍其中位数,而奇数很容易。再者对于3而言,会有,它本身还是操作了n个元素,与以5为主元的相比,其复杂度并没有减少。对于7,9,...而言,上式中的10c,其整体都会增加,所以与5相比,5更适合。

四:参考文献

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注