bfprt 算法 (数组中第K 小问题问题)

本文深入探讨了BFPRT算法,一种高效的TOP-K问题解决方案,详细介绍了算法的背景、过程和代码实现,对比了快速排序和堆排序,强调了BFPRT算法在最坏情况下的时间复杂度优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

在首次接触 TOP-K 问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前 k 即可,但是这么做有两个问题:
快速排序的平均复杂度为 (nlogn),但最坏时间复杂度为(n^2),不能始终保证较好的复杂度;
我们只需要前 k 大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。

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

那是否还存在更有效的方法呢?我们来看下 BFPRT 算法的做法。

在快速排序的基础上,首先通过判断主元位置与 k 的大小使递归的规模变小,其次通过修改快速排序中主元的选取方法来降低快速排序在最坏情况下的时间复杂度。

下面先来简单回顾下快速排序的过程,以升序为例:
选取主元;
以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
分别对左边和右边进行递归,重复上述过程。

二:算法过程及代码

BFPRT 算法步骤如下:
选取主元;
1.1. 将 n 个元素按顺序分为 [5 / n]个组,每组 5 个元素,若有剩余,舍去;
1.2. 对于这[5 / n]个组中的每一组使用插入排序找到它们各自的中位数;
1.3. 对于 1.2 中找到的所有中位数,调用 BFPRT 算法求出它们的中位数,作为主元;
以 1.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 GetPivotIndex(int array[], int left, int right);
int Partition(int array[], int left, int right, int pivot_index);
int BFPRT(int array[], int left, int right, int k);

int main()
{
    int k = 8; // 1 <= k <= array.size
    int array[20] = { 11,9,10,1,13,8,15,0,16,2,17,5,14,3,6,18,12,7,19,4 };

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

    // 因为是以 k 为划分,所以还可以求出第 k 小值
    cout << "第 " << k << " 小值为:" << array[BFPRT(array, 0, 19, k)] << endl;

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

    return 0;
}

/**
 * 对数组 array[left, right] 进行插入排序,并返回 [left, right]
 * 的中位数。
 */
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;
}

/**
 * 数组 array[left, right] 每五个元素作为一组,并计算每组的中位数,
 * 最后返回这些中位数的中位数下标(即主元下标)。
 *
 * @attention 末尾返回语句最后一个参数多加一个 1 的作用其实就是向上取整的意思,
 * 这样可以始终保持 k 大于 0。
 */
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]);
    }

    // 利用 BFPRT 得到这些中位数的中位数下标(即主元下标)
    return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1);
}

/**
 * 利用主元下标 pivot_index 进行对数组 array[left, right] 划分,并返回
 * 划分后的分界线下标。
 */
int Partition(int array[], int left, int right, int pivot_index)
{
    swap(array[pivot_index], array[right]); // 把主元放置于末尾

    int partition_index = left; // 跟踪划分的分界线
    for (int i = left; i < right; i++)
    {
        if (array[i] < array[right])
        {
            swap(array[partition_index++], array[i]); // 比主元小的都放在左侧
        }
    }

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

    return partition_index;
}

/**
 * 返回数组 array[left, right] 的第 k 小数的下标
 */
int BFPRT(int array[], int left, int right, int k)
{
    int pivot_index = GetPivotIndex(array, left, right); // 得到中位数的中位数下标(即主元下标)
    int partition_index = Partition(array, left, right, pivot_index); // 进行划分,返回划分边界
    int num = partition_index - left + 1;

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

运行如下:

原数组:11 9 10 1 13 8 15 0 16 2 17 5 14 3 6 18 12 7 19 4
第 8 小值为:7
变换后的数组:4 0 1 3 2 5 6 7 8 9 10 12 13 14 17 15 16 11 18 19

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值