在算法中,每当我添加一个值时,我都必须计算数据集的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;
};
答案 0 :(得分:30)
你可以用两个heaps来做。不确定是否有一个不太“设计”的解决方案,但是这个解决方案提供了O(logn)
时间复杂度,并且堆也包含在大多数编程语言的标准库中。
第一个堆(堆A)包含最小的75%元素,另一个堆(堆B) - 其余(最大25%)。第一个是最重要的元素,第二个是最小元素。
查看新元素x
是否为&lt; = max(A)
。如果是,请将其添加到堆A
,否则 - 添加到堆B
现在,如果我们将x
添加到堆A并且它变得太大(占据超过75%的元素),我们需要从A
(O(logn))中删除最大元素并将其添加到堆B(也是O(logn))
类似的,如果堆B变得太大了。
从A中取出最大的元素(或从B中取出最小的元素)。需要O(logn)或O(1)时间,具体取决于堆实现。
修改强>
正如 Dolphin 所指出的那样,我们需要精确指定每个堆的每个堆的大小(如果我们想要精确的答案)。例如,如果其余为size(A) = floor(n * 0.75)
和size(B)
,那么,对于每个n > 0
,array[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而不是数组。