【问题标题】:C++ Fast Percentile CalculationC++ 快速百分位数计算
【发布时间】:2016-01-26 18:41:10
【问题描述】:

我正在尝试编写一个百分位数函数,该函数将 2 个向量作为输入,1 个向量作为输出。输入向量 (Distr) 之一将是随机数的分布。另一个输入向量(测试)将是我想从 Distr 计算百分位数的值向量。输出将是一个向量(与 Tests 大小相同),它返回 Tests 中每个值的百分位数。

以下是我想要的示例:

Input Distr = {3, 5, 8, 12}
Input Tests = {4, 9}
Output Percentile = {0.375, 0.8125}

以下是我在 C++ 中的实现:

vector<double> Percentile(vector<double> Distr, vector<double> Tests)
{
    double prevValue, nextValue;
    vector<double> result;
    unsigned distrSize = Distr.size();

    std::sort(Distr.begin(), Distr.end());

    for (vector<double>::iterator test = Tests.begin(); test != Tests.end(); test++)
    {

        if (*test <= Distr.front())
        {
            result.push_back((double) 1 / distrSize); // min percentile returned (not important)
        }
        else if (Distr.back() <= *test)
        {
            result.push_back(1); // max percentile returned (not important)
        }
        else
        {
            prevValue = Distr[0];
            for (unsigned sortedDistrIdx = 1; sortedDistrIdx < distrSize; sortedDistrIdx++)
            {
                nextValue = Distr[sortedDistrIdx];

                if (nextValue <= *test)
                {
                    prevValue = nextValue;
                }
                else
                {
                    // linear interpolation
                    result.push_back(((*test - prevValue) / (nextValue - prevValue) + sortedDistrIdx) / distrSize);
                    break;
                }
            }
        }
    }
    return result;
}

Distr 和 Tests 的大小都可以在 2,000 到 30,000 之间。

是否有任何现有的库可以计算如上所示(或类似)的百分位数?如果不是,我怎样才能使上面的代码更快?

【问题讨论】:

  • 如果您不使用push_back 而是预先分配所涉及的向量,这将有所帮助。
  • 一种方法是通过引用获取您的输入参数。目前正在无缘无故地复制大向量。
  • @JonathanPotter 因为 Distr 已排序,通过引用获取它会修改输入。此外,当两者都很大时,复制的线性成本只是总时间的一小部分。
  • 您说Distr 将是随机数的分布,但在您的示例中它是排序的,并且大多数答案假设 @ 987654325@ 已排序。请澄清Distr 是否最初排序。从您的代码来看,它似乎没有排序,因为您首先对其进行了排序。请注意,这种类型很可能是总成本的主要贡献者。
  • 了解test 是否可以假定为初始排序也会很有帮助。

标签: c++ vector percentile


【解决方案1】:

此答案与input 最初是随机的(未排序)且test.size() 小于input.size() 的情况有关,这是最常见的情况。

假设只有一个测试值。然后,您只需根据该值对input 进行分区并获取下(上)分区的上(下)界以计算相应的百分位数。这比对输入的完整排序(快速排序实现为分区的递归)快得多。

如果test.size()&gt;1,那么您首先对test 进行排序(理想情况下,test 已经排序,您可以跳过此步骤)然后按升序处理测试元素,每次仅将上部从上一个分区。由于我们还跟踪上分区的下限(以及下分区的上限),我们可以检测连续测试元素之间是否没有输入数据,并避免进行分区。

该算法应该接近最优,因为不会生成不必要的信息(就像使用完整的input 一样)。

如果随后的分区将输入大致分成两半,则该算法将是最优的。这可以通过不按test 的递增顺序进行近似,而是通过随后将test 减半,即从中间测试元素开始,然后是第一个和第三个四分位数等。

【讨论】:

  • 在两个输入的某个相对大小下,您是正确的。当test 是input 的日志大小时,我相信您已经超出了您的方法。在test(相对于input)的一些更小的尺寸上,你是正确的。
  • @JSF 如果 input 最初是随机的(如 OP 中所指定),那么我看不出如何获得更快的方法。
  • 如果连续的测试元素之间没有输入数据,你怎么能检测到不需要工作的事实?相反,你会做那些不需要的工作。现在您更改为对test 进行排序,您应该将input 递归地划分为test 的一半,而不是线性地划分。对于两者之间的随机相关性,这将大大改善您的方法。随着两种尺寸变得相似,它仍然会分解。
  • @JSF 你大概是对的。我将编辑问题。
  • 我意识到你的方法有进一步的改进,假设两个列表最初都是随机的:一个递归函数从两个输入随机开始并且都不排序:取Tests的第一个元素和分区两个 输入该值。这为您提供了第一个元素的答案。然后递归传递Tests 的第一个分区,只有Distr 的第一个分区(并且分别是第二个和第二个)。随机输入应该取 (N+M)logN
【解决方案2】:

如果这两个元素都很大,则对 Tests 的每个元素进行 Distr 的线性搜索将是主要的时间量。

当 Distr 大得多时,进行二分搜索而不是线性搜索要快得多。 std 中有一个二进制搜索算法。你不需要写一个。

当 Tests 几乎与 Distr 一样大或更大时,执行 Tests 的索引排序然后将两个排序列表排序在一起存储结果,然后在下一次传递中输出存储的结果会更快。

编辑:我看到 Csaba Balint 的回答更详细地说明了我所说的“通过两个排序列表一起排序”的含义。

编辑:正在讨论的基本方法是:
1) 对两个列表进行排序,然后一起线性处理,时间 NlogN+MlogM
2) 只排序一个列表和二分查找,时间 (N+M)logM
3)只排序另一个列表和分区,时间我还没弄清楚,但在N和M相似的情况下,它必须大于方法1或2,并且在N足够小的情况下必须小于方法 1 或 2。

【讨论】:

  • bool std::binary_search(first, last, value) 如果在指定范围内找到等于 value 的元素,则返回 - 这里可能有什么帮助?在示例中,测试 4 不包含在输入分布中。
  • @Walter 我没有说 which 我的意思是 std 中的“二分查找”方法(因为我懒得查找详细信息)。有一个。我希望它是lower_bound。如果我的意思是 binary_search 而不是“二分搜索”,我会这么说。
【解决方案3】:

我会做类似的事情

vector<double> Percentile(vector<double> Distr, vector<double> Tests)
{
    double prevValue, nextValue;
    vector<double> result;
    unsigned distrSize = Distr.size();

    std::sort(Distr.begin(), Distr.end());

    for (vector<double>::iterator test = Tests.begin(); test != Tests.end(); test++)
    {
        if (*test <= Distr.front())
        {
            result.push_back((double) 1 / distrSize); // min percentile returned (not important)
        }
        else if (Distr.back() <= *test)
        {
            result.push_back(1); // max percentile returned (not important)
        }
        else
        {
            auto it = lower_bound(Distr.begin(), Distr.end(), *test);
            prevValue = *(it - 1);
            nextValue = *(it + 1);
            // linear interpolation
            result.push_back(((*test - prevValue) / (nextValue - prevValue) + (it - Distr.begin())) / distrSize);
        }
    }
    return result;
}

请注意,我不是在 Distr 上为每个测试进行线性搜索,而是利用 Distr 已排序的事实并进行二分搜索(使用 下界)。

【讨论】:

    【解决方案4】:

    您的问题有一个线性算法(两种大小的线性时间对数)。您需要对两个向量进行排序,然后让两个迭代器遍历每个向量(itDistr、itTest)。有三种可能:

    1。 *itDistr

    在这里,除了增加 itDistr 之外,您别无他法。

    2。 *itDistr >= *itTest

    当您发现 *itTest 是区间 [ *(itDistr-1), *itDistr ) 的元素时就是这种情况。所以你必须做你使用过的插值(线性),然后递增itTest。

    第三种可能性是其中任何一个到达其容器向量的末尾。您还必须定义在开始和结束时会发生什么,这取决于您如何从一系列数字中定义分布。

    是否有任何现有的库可以计算如上所示(或类似)的百分位数?

    可能,但它很容易实现,并且您可以很好地控制插值技术。

    【讨论】:

    • 你的方法显然是次优的,因为不需要完整的输入(随机)分布。
    • @Walter,对于两个输入向量都非常大的情况(尤其是在大小相似的情况下),在说这个答案中的方法是次优之前提出一个更好的方法。
    • @JSF,谢谢 :) 我不能(还)评论你的答案,但我认为你的意思是 std::lover_bound() (或上界)。
    猜你喜欢
    • 2011-04-13
    • 1970-01-01
    • 2013-01-31
    • 2011-04-16
    • 2011-12-29
    • 2013-06-20
    • 1970-01-01
    • 2017-09-28
    • 2016-07-28
    相关资源
    最近更新 更多