重复计算百分位数的快速算法?

Posted

技术标签:

【中文标题】重复计算百分位数的快速算法?【英文标题】:Fast algorithm for repeated calculation of percentile? 【发布时间】:2011-04-13 21:26:28 【问题描述】:

在算法中,每当我添加一个值时,我都必须计算数据集的75th percentile。现在我正在这样做:

    获取值x 在后面已排序的数组中插入x 向下交换 x 直到数组排序 读取位置array[array.size * 3/4]的元素

第 3 点是 O(n),其余是 O(1),但这仍然很慢,尤其是当数组变大时。有什么办法可以优化吗?

更新

感谢尼基塔!因为我使用的是 C++,所以这是最容易实现的解决方案。代码如下:

template<class T>
class IterativePercentile 
public:
  /// Percentile has to be in range [0, 1(
  IterativePercentile(double percentile)
    : _percentile(percentile)
   

  // Adds a number in O(log(n))
  void add(const T& x) 
    if (_lower.empty() || x <= _lower.front()) 
      _lower.push_back(x);
      std::push_heap(_lower.begin(), _lower.end(), std::less<T>());
     else 
      _upper.push_back(x);
      std::push_heap(_upper.begin(), _upper.end(), std::greater<T>());
    

    unsigned size_lower = (unsigned)((_lower.size() + _upper.size()) * _percentile) + 1;
    if (_lower.size() > size_lower) 
      // lower to upper
      std::pop_heap(_lower.begin(), _lower.end(), std::less<T>());
      _upper.push_back(_lower.back());
      std::push_heap(_upper.begin(), _upper.end(), std::greater<T>());
      _lower.pop_back();
     else if (_lower.size() < size_lower) 
      // upper to lower
      std::pop_heap(_upper.begin(), _upper.end(), std::greater<T>());
      _lower.push_back(_upper.back());
      std::push_heap(_lower.begin(), _lower.end(), std::less<T>());
      _upper.pop_back();
                
  

  /// Access the percentile in O(1)
  const T& get() const 
    return _lower.front();
  

  void clear() 
    _lower.clear();
    _upper.clear();
  

private:
  double _percentile;
  std::vector<T> _lower;
  std::vector<T> _upper;
;

【问题讨论】:

很好,我最近在一次采访中遇到了类似的问题。 Nikita 已经给出了我的答案。 @Alexandru: Similar != Same :-) 我相信这里不需要堆解决方案。它可能适用于此:***.com/questions/2213707/…,但我认为这是一个误用。 我认为在:if (_lower.empty() || x &lt;= _lower.front()) 中存在未定义的行为,因为未定义评估顺序。 @davide 评估的顺序是明确定义的,如果_lower.empty() 返回true,则不评估右侧。 @martinus 没错,运算符&amp;&amp;|| 是一个例外,因为它们保证了评估的顺序。需要注意的是,它们的重载对应物会反转或不保证评估的顺序,这取决于它们是否被定义为方法,但这里不是这种情况。我会在这个主题上引用this excellent answer on SO。 【参考方案1】:

您可以使用两个heaps 来完成。不确定是否有更“人为”的解决方案,但这个解决方案提供了O(logn) 时间复杂度,并且堆也包含在大多数编程语言的标准库中。

第一个堆(堆 A)包含最小的 75% 元素,另一个堆(堆 B)- 其余的(最大的 25%)。第一个在顶部有最大的元素,第二个 - 最小。

    添加元素。

查看新元素 x 是否为 max(A)。如果是,则将其添加到堆 A,否则 - 将其添加到堆 B。 现在,如果我们将x 添加到堆 A 并且它变得太大(包含超过 75% 的元素),我们需要从 A 中删除最大元素(O(logn))并将其添加到堆 B(也O(logn))。 如果堆 B 变得太大,则类似。

    寻找“0.75 中位数”

只需从 A 中取出最大的元素(或从 B 中取出最小的元素)。需要 O(logn) 或 O(1) 时间,具体取决于堆实现。

编辑 正如 Dolphin 所指出的,我们需要精确地指定每个 n 的每个堆应该有多大(如果我们想要精确的答案)。例如,如果size(A) = floor(n * 0.75)size(B) 是其余的,那么对于每个n &gt; 0array[array.size * 3/4] = min(B)

【讨论】:

但是如何确定堆 A 是否变得太大? @Raze2dust 堆 A 应该包含大约 75% 的元素。如果它的大小超过这个,它就变得太大了。 @Raze2dust 如果你的意思是“如何获得堆大小”,这是一个 O(1) 操作:) 我认为这个想法会奏效,但我认为有必要进行一些更改。首先,其中一个堆应该始终有您要查找的项目。通过这种方式,您可以确定对于给定数量的元素heap A=floor(n*.75) and heap B=ceil(n*.25)(在这种情况下)每个堆的大小。接下来,当您添加项目时,确定需要增长的堆。如果堆 A 需要增长并且项目小于 B 的顶部,则将其添加到 A。否则删除 B 的顶部,将其添加到 A,然后将新项目添加到 B。(删除然后添加将是作为修改更有效)。 @Nikita - 不,只是一些调整。定义应该增长的堆使添加操作稍微简单一些(您的添加可以执行 3 个 O(logn) 操作(添加、删除、添加)。我的建议是最坏情况下的两个(修改、添加)。这并不重要您选择哪个堆,但选择小堆以始终拥有该项目将使堆的大小更接近,从而获得(可能微不足道的)性能增益。【参考方案2】:

一个简单的Order Statistics Tree 就足够了。

此树的平衡版本支持 O(logn) 时间插入/删除和按 Rank 访问。因此,您不仅可以获得 75% 的百分位数,而且还可以获得 66% 或 50% 或任何您需要的值,而无需更改代码。

如果您经常访问 75% 的百分位,但插入的频率较低,则您始终可以在插入/删除操作期间缓存 75% 的百分位元素。

大多数标准实现(如 Java 的 TreeMap)都是顺序统计树。

【讨论】:

+1 表示有用的技术。但是您有一个错误:Java 的 TreeSet(或 Map)不会为您提供从树根向下迭代到叶子所需的工具。 IIRC,STL版本也是。您必须编写自己的平衡树或破解其他人的代码。几乎没有乐趣。 +1 - 但是您不能按等级索引 Java TreeSet。如果值不会重复,您可以使用 Java 的 TreeSet;您只需要跟踪您当前的第 75 个百分位数以及左侧和右侧的项目数。添加某些内容时,将其放入集合中并更新左/右数字。如果你现在右边的太多了,用higher获取下一个;如果左边太多,使用lower获取上一个;如果你没事,就不要做任何事。如果值重复,您必须创建一个从键到某个集合(列表?)的映射,然后类似的技巧会起作用。 @Nikita:我相信 TreeMap 有它!看看这个答案的 cmets:***.com/questions/3071497/…。 @Rex,我说的是 TreeMap。当然我有一段时间没用过Java了。 但是 Rex 的想法应该可行(虽然实现起来不是很简单) @Nikita:我并不是说你必须自己遍历树。我声称该数据结构提供了用于按位置访问/插入/删除的 API。无论如何,我现在对 TreeMap 不太确定......【参考方案3】:

如果您可以使用近似答案,您可以使用直方图而不是将整个值保存在内存中。

对于每个新值,将其添加到相应的 bin。 通过遍历 bin 和求和计数来计算第 75 个百分位数,直到达到人口规模的 75%。百分位值介于 bin(您在此停止)的下限和上限之间。

这将提供 O(B) 复杂度,其中 B 是 bin 的计数,即range_size/bin_size。 (使用适合您的用户案例的bin_size)。

我已经在 J​​VM 库中实现了这个逻辑:https://github.com/IBM/HBPE,您可以将其用作参考。

【讨论】:

【参考方案4】:

您可以使用二分搜索在 O(log n) 中找到正确的位置。但是,将数组向上移动仍然是 O(n)。

【讨论】:

【参考方案5】:

如果你有一组已知的值,那么下面会非常快:

创建一个大型整数数组(偶数字节也可以),其中元素的数量等于数据的最大值。 例如,如果 t 的最大值为 100,000,则创建一个数组

int[] index = new int[100000]; // 400kb

现在遍历整个值集,如

for each (int t : set_of_values) 
  index[t]++;


// You can do a try catch on ArrayOutOfBounds just in case :)

现在计算百分位数为

int sum = 0, i = 0;
while (sum < 0.9*set_of_values.length) 
  sum += index[i++];


return i;

如果值不符合这些限制,您也可以考虑使用 TreeMap 而不是数组。

【讨论】:

这使得插入 O(1),但它使得找到第 75 个百分位元素 O(M),其中 M 是最大值。 M 可能比 N 大得多。(另外,请注意 OP 使用的是双精度浮点值,因此没有希望用合理大小的位图(或重复计数数组)来表示它们)。因此,对于每个部分列表中第 75 个百分位数的列表,总体时间复杂度为 O(NM)。如果可能值的范围非常小,这将很有趣,但在这里没有帮助。不过,与两堆技巧相比,我不会称其为“非常快”。 我没有得到这个答案的反对票。即使值是浮动的,如果它们的分布是已知的,仔细的分箱可以产生非常准确的结果。如果你能得到足够低的 $M$,那么与 O(n log(n)) 相比它会非常快,特别是考虑到操作非常简单和快速(浮点加法、索引)。此外,由于添加一个数字是 O(1),如果您不需要在每次添加一个数字时获取百分位数的更新值,您可以在堆上节省大量 log(n) 查找。由于 OP 正在寻找速度,因此值得考虑。【参考方案6】:

这是一个 javascript 解决方案。将其复制粘贴到浏览器控制台中即可。 $scores 包含分数列表,$percentile 给出列表的n-th percentile。所以第 75 个百分位是 76.8,第 99 个百分位是 87.9。

function get_percentile($percentile, $array) 
    $array = $array.sort();
    $index = ($percentile/100) * $array.length;
    if (Math.floor($index) === $index) 
         $result = ($array[$index-1] + $array[$index])/2;
    
    else 
        $result = $array[Math.floor($index)];
    
    return $result;


$scores = [22.3, 32.4, 12.1, 54.6, 76.8, 87.3, 54.6, 45.5, 87.9];

get_percentile(75, $scores);
get_percentile(90, $scores);

【讨论】:

以上是关于重复计算百分位数的快速算法?的主要内容,如果未能解决你的问题,请参考以下文章

C ++有效地计算运行中位数[重复]

重复大量输入的快速选择算法?

如何在 C++/Rcpp 中进行快速百分位数计算

不用排序怎样快速找到中位数,最好是一遍下来得到结果,求算法或者思路 谢谢!

模幂运算问题,使用朴素算法和重复-平方算法(快速幂+C#计算程序运行时间)

模幂运算问题,使用朴素算法和重复-平方算法(快速幂+C#计算程序运行时间)