zoukankan      html  css  js  c++  java
  • BFPRT 算法 (TOP-K 问题)——本质就是在利用分组中位数的中位数来找到较快排更合适的pivot元素

    先说快排最坏情况下的时间复杂度为n^2。

    正常情况:

     

    最坏的情况下,待排序的记录序列正序或逆序,每次划分只能得到一个比上一次划分少一个记录的子序列,(另一个子序列为空)。此时,必须经过n-1次递归调用才能把所有记录定位,而且第i趟划分需要经过n-i次比较才能找个才能找到第i个记录的位置,因此时间复杂度为

     
    所以BFPRT本质上是在寻找正确的pivot元素!!!避免这种最坏情况出现。
     
     

    在BFPTR算法中,仅仅是改变了快速排序Partion中的pivot值的选取,在快速排序中,我们始终选择第一个元素或者最后一个元素作为pivot,而在BFPTR算法中,每次选择五分中位数的中位数作为pivot,这样做的目的就是使得划分比较合理,从而避免了最坏情况的发生。算法步骤如下:

    1. 将 n 个元素划为 lfloor n/5
floor 组,每组5个,至多只有一组由 nmod5 个元素组成。 
    2. 寻找这 lceil n/5
ceil 个组中每一个组的中位数,这个过程可以用插入排序。 
    3. 对步骤2中的 lceil n/5
ceil 个中位数,重复步骤1和步骤2,递归下去,直到剩下一个数字。
    4. 最终剩下的数字即为pivot,把大于它的数全放左边,小于等于它的数全放右边。 
    5. 判断pivot的位置与k的大小,有选择的对左边或右边递归。

    当你看到本质上后,至于”首先把数组按5个数为一组进行分组,最后不足5个的忽略。 ” 、“偶数个元素的中位数取中间2个中较小的一个。”这些小细节都是无关紧要的了!!!

    下面为代码实现,其所求为前 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

    性能分析:

    划分时以5个元素为一组求取中位数,共得到n/5个中位数,再递归求取中位数,复杂度为T(n/5)。

    得到的中位数x作为主元进行划分,在n/5个中位数中,主元x大于其中1/2*n/5=n/10的中位数,而每个中位数在其本来的5个数的小组中又大于或等于其中的3个数,所以主元x至少大于所有数中的n/10*3=3/10*n个。同理,主元x至少小于所有数中的3/10*n个。即划分之后,任意一边的长度至少为3/10,在最坏情况下,每次选择都选到了7/10的那一部分,则递归的复杂度为T(7/10*n)。

    在每5个数求中位数和划分的函数中,进行若干个次线性的扫描,其时间复杂度为c*n,其中c为常数。其总的时间复杂度满足 T(n) <= T(n/5) + T(7/10*n) + c * n。

    我们假设T(n)=x*n,其中x不一定是常数(比如x可以为n的倍数,则对应的T(n)=O(n^2))。则有 x*n <= x*n/5 + x*7/10*n + c*n,得到 x<=10*c。于是可以知道x与n无关,T(n)<=10*c*n,为线性时间复杂度算法。而这又是最坏情况下的分析,故BFPRT可以在最坏情况下以线性时间求得n个数中的第k个数。

    时间复杂度为O(n)的原因

    enter description here

    我们选取的x可以在每次递归的时候最少淘汰(n/10-2)x3的数据量

    详细的时间复杂度分析见:https://blog.csdn.net/LaoJiu_/article/details/54986553

  • 相关阅读:
    【转】VC 线程间通信的三种方式
    【转】MFC对话框和控件
    美国政府、部门构成及其运作
    贝叶斯推理(Bayes Reasoning)、独立与因式分解
    贝叶斯推理(Bayes Reasoning)、独立与因式分解
    机器学习:利用卷积神经网络实现图像风格迁移 (三)
    TBB、OpenCV混合编程
    VS编译环境中TBB配置和C++中lambda表达式
    概率图模型(PGM) —— 贝叶斯网络(Bayesian Network)
    概率图模型(PGM) —— 贝叶斯网络(Bayesian Network)
  • 原文地址:https://www.cnblogs.com/bonelee/p/10248206.html
Copyright © 2011-2022 走看看