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

时间:2010-09-17 19:26:48

标签: algorithm optimization data-structures percentile

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

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

    点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;
    };
    

6 个答案:

答案 0 :(得分:30)

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

第一个堆(堆A)包含最小的75%元素,另一个堆(堆B) - 其余(最大25%)。第一个是最重要的元素,第二个是最小元素。

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

    1. 查找“0.75中位数”
    2. 从A中取出最大的元素(或从B中取出最小的元素)。需要O(logn)或O(1)时间,具体取决于堆实现。

      修改
      正如 Dolphin 所指出的那样,我们需要精确指定每个堆的每个堆的大小(如果我们想要精确的答案)。例如,如果其余为size(A) = floor(n * 0.75)size(B),那么,对于每个n > 0array[array.size * 3/4] = min(B)

答案 1 :(得分:14)

简单的Order Statistics Tree就足够了。

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

如果频繁访问75%百分位数,但只插入频率较低,则可以在插入/删除操作期间缓存75%百分位元素。

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

答案 2 :(得分:1)

如果您可以使用近似答案,则可以使用直方图来代替将整个值保留在内存中。

对于每个新值,将其添加到相应的bin中。 通过遍历垃圾箱并累加计数直到达到人口总数的75%来计算第75个百分位。百分数值介于bin的值(您在其处停止)的下限与上限之间。

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

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

答案 3 :(得分:-1)

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

答案 4 :(得分:-2)

这是一个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);

答案 5 :(得分:-3)

如果您有一组已知的值,则跟踪速度非常快:

创建一个大的整数数组(偶数字节将起作用),其元素数等于数据的最大值。 例如,如果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而不是数组。