Быстрый алгоритм повторного вычисления процентили?
В алгоритме я должен вычислить 75-й процентиль набора данных всякий раз, когда добавляю значение. Сейчас я делаю это:
- Получить значение
x
- Вставьте
x
в уже отсортированный массив на задней панели
- swap
x
вниз, пока массив не будет отсортирован
- Прочитайте элемент в позиции
array[array.size * 3/4]
Точка 3 - O (n), а остальное - O (1), но это все еще довольно медленно, особенно если массив становится больше. Есть ли способ оптимизировать это?
UPDATE
Спасибо Никите! Поскольку я использую С++, это решение проще всего реализовать. Вот код:
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;
};
Ответы
Ответ 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), в зависимости от реализации кучи.
изменить
Как отметил Дельфин, нам нужно точно указать, насколько велика каждая куча для каждого n (если мы хотим получить точный ответ). Например, если size(A) = floor(n * 0.75)
и size(B)
- это остальное, то для каждого n > 0
, array[array.size * 3/4] = min(B)
.
Ответ 2
Для этого достаточно простого Статистика статистики заказов.
Сбалансированная версия этого дерева поддерживает O (logn) время вставки/удаления и доступа по рангу. Таким образом, вы не только получаете 75% процентиля, но и 66% или 50% или все, что вам нужно без изменения кода.
Если вы часто обращаетесь к 75% процентилям, но только вставляете реже, вы всегда можете кэшировать элемент 75% процентиля во время операции вставки/удаления.
Большинство стандартных реализаций (таких как Java TreeMap) являются статистическими деревьями заказов.
Ответ 3
Вот решение 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);
Ответ 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 вместо массива, если значения не подтверждают эти ограничения.