Найти бегущую медиану из потока целых чисел


223

Возможный дубликат:
алгоритм скользящей медианы в C

Учитывая, что целые числа читаются из потока данных. Найдите медиану прочитанных элементов эффективным способом.

Решение, которое я прочитал: мы можем использовать максимальную кучу на левой стороне для представления элементов, которые меньше эффективной медианы, и минимальную кучу на правой стороне для представления элементов, которые превышают эффективную медиану.

После обработки входящего элемента количество элементов в кучах отличается не более чем на 1 элемент. Когда обе кучи содержат одинаковое количество элементов, мы находим среднее значение корневых данных кучи как эффективную медиану. Когда кучи не сбалансированы, мы выбираем эффективную медиану из корня кучи, содержащей больше элементов.

Но как мы построим максимальную кучу и минимальную кучу, то есть как мы узнаем эффективную медиану здесь? Я думаю, что мы вставили бы 1 элемент в max-heap, а затем следующий 1 элемент в min-heap и так далее для всех элементов. Поправь меня, если я здесь не прав.


10
Умный алгоритм, использующий кучи. Из названия я не мог сразу придумать решение.
Mooing Duck

1
Решение vizier выглядит хорошо для меня, за исключением того, что я предполагал (хотя вы не заявили), что этот поток может быть произвольно длинным, поэтому вы не можете хранить все в памяти. Это тот случай?
Дикий бег

2
@RunningWild Для произвольно длинных потоков вы можете получить медиану последних N элементов, используя кучи Фибоначчи (таким образом, вы получаете удаления log (N)) и сохраняя указатели на вставленные элементы по порядку (например, в deque), затем удаляя самые старые элемент на каждом шаге, как только кучи заполнены (возможно, также перемещая вещи из одной кучи в другую). Вы можете получить несколько лучше, чем N, сохранив количество повторяющихся элементов (если имеется много повторов), но в целом, я думаю, вам нужно сделать какие-то предположения о распределении, если вы хотите получить медиану всего потока.
Дугал

2
Вы можете начать с обеих пустых кучи. Первый int идет в одну кучу; второй идет либо в другой, либо вы перемещаете первый элемент в другую кучу, а затем вставляете. Это обобщается так: «не позволяйте одной куче становиться больше, чем другая +1», и никакой специальный регистр не требуется («корневое значение» пустой кучи может быть определено как 0)
Джон Уотт,

Я только что получил этот вопрос на интервью MSFT. Спасибо за публикацию
R Claven

Ответы:


383

Существует несколько различных решений для определения медианы бега по потоковым данным, я кратко расскажу о них в самом конце ответа.

Вопрос о деталях конкретного решения (макс. Кучи / мин. Кучи) и о том, как работает решение на основе кучи, объясняется ниже:

Для первых двух элементов добавьте меньший к maxHeap слева, а больший к minHeap справа. Затем обработайте данные потока по одному,

Step 1: Add next item to one of the heaps

   if next item is smaller than maxHeap root add it to maxHeap,
   else add it to minHeap

Step 2: Balance the heaps (after this step heaps will be either balanced or
   one of them will contain 1 more item)

   if number of elements in one of the heaps is greater than the other by
   more than 1, remove the root element from the one containing more elements and
   add to the other one

Тогда в любой момент времени вы можете рассчитать медиану так:

   If the heaps contain equal amount of elements;
     median = (root of maxHeap + root of minHeap)/2
   Else
     median = root of the heap with more elements

Теперь я расскажу о проблеме в целом, как и было обещано в начале ответа. Найти медиану из потока данных - сложная задача, и найти точное решение с эффективными ограничениями памяти, вероятно, невозможно в общем случае. С другой стороны, если данные имеют некоторые характеристики, которые мы можем использовать, мы можем разработать эффективные специализированные решения. Например, если мы знаем, что данные являются целочисленным типом, то мы можем использовать счетную сортировку, который может дать вам постоянную память постоянного алгоритма времени. Решение на основе кучи является более общим решением, поскольку оно может использоваться и для других типов данных (удваивается). И, наконец, если точная медиана не требуется и достаточно приближения, вы можете просто попытаться оценить функцию плотности вероятности для данных и оценить медиану, используя ее.


6
Эти кучи растут без ограничений (т. Е. Окно из 100 элементов, скользящее по 10 миллионам элементов, потребовало бы хранения 10 миллионов элементов в памяти). Ниже приведен другой ответ с использованием индексируемых пропусков, в котором требуется, чтобы в памяти хранились только последние 100 элементов.
Рэймонд Хеттингер

1
Вы можете иметь ограниченное решение для памяти, используя кучи также, как объяснено в одном из комментариев к самому вопросу.
Hakan Serce

1
Вы можете найти реализацию решения на основе кучи в c здесь.
AShelly

1
Ничего себе, это помогло мне не только решить эту конкретную проблему, но и помогло мне изучить кучу. Вот моя основная реализация в python: github.com/PythonAlgo/DataStruct
swati saoji

2
@HakanSerce Не могли бы вы объяснить, почему мы сделали то, что сделали? Я имею в виду, я вижу, как это работает, но я не могу понять это интуитивно.
Шива

51

Если вы не можете держать все элементы в памяти одновременно, эта проблема становится намного сложнее. Решение для кучи требует, чтобы вы держали все элементы в памяти одновременно. Это невозможно в большинстве реальных применений этой проблемы.

Вместо этого, как вы видите номера, следить за кол количества раз вы видите каждое целое число. Предполагая 4-байтовые целые, это 2 ^ 32 сегмента, или не более 2 ^ 33 целых (ключ и число для каждого целого), что составляет 2 ^ 35 байт или 32 ГБ. Вероятно, это будет намного меньше, чем это, потому что вам не нужно хранить ключ или счетчик для тех записей, которые равны 0 (т.е. как defaultdict в python). Требуется постоянное время для вставки каждого нового целого числа.

Затем в любой момент, чтобы найти медиану, просто используйте количество, чтобы определить, какое целое число является средним элементом. Это требует постоянного времени (хотя и большой постоянной, но тем не менее постоянной).


3
Если почти все числа видны один раз, то разреженный список займет еще больше памяти. И кажется весьма вероятным, что если у вас так много чисел, они не помещаются в число, что большинство чисел появятся один раз. Несмотря на это, это умное решение для массового подсчета чисел.
Mooing Duck

1
Для разреженного списка, я согласен, это хуже с точки зрения памяти. Хотя, если целые числа распределены случайным образом, вы начнете получать дубликаты намного раньше, чем предполагает интуиция. См. Mathworld.wolfram.com/BirthdayProblem.html . Поэтому я уверен, что это вступит в силу, как только у вас будет несколько ГБ данных.
Андрей С

4
@AndrewC можете ли вы объяснить, как потребуется постоянное время, чтобы найти медиану. Если я видел n различных видов целых чисел, то в худшем случае последний элемент может быть медианой. Это делает медиану обнаружения O (n) активностью.
Шшнк

@shshnk Разве не n общее число элементов, которое в данном случае составляет >>> 2 ^ 35?
ВишАмди

@shshnk Вы правы, что по-прежнему линейно по числу различных целых чисел, которые вы видели, как сказал ВишАмди, я предполагаю, что для этого решения я предполагаю, что n - это число чисел, которое вы видели, что значительно больше чем 2 ^ 33. Если вы не видите столько цифр, решение maxheap определенно лучше.
Андрей C

49

Если дисперсия входных данных является статистически распределенной (например, нормальная, логарифмическая и т. Д.), То выборка из резервуара является разумным способом оценки процентилей / медиан из произвольно длинного потока чисел.

int n = 0;  // Running count of elements observed so far  
#define SIZE 10000
int reservoir[SIZE];  

while(streamHasData())
{
  int x = readNumberFromStream();

  if (n < SIZE)
  {
       reservoir[n++] = x;
  }         
  else 
  {
      int p = random(++n); // Choose a random number 0 >= p < n
      if (p < SIZE)
      {
           reservoir[p] = x;
      }
  }
}

«Резервуар» - это бегущая, равномерная (справедливая) выборка всех входных данных - независимо от размера. Поиск медианы (или любого процентиля) - это прямая задача сортировки резервуара и опроса интересующей точки.

Поскольку резервуар имеет фиксированный размер, сортировка может считаться эффективной O (1) - и этот метод работает с постоянным временем и потреблением памяти.


из любопытства, зачем вам дисперсия?
LazyCat

Поток может возвращать меньше, чем элементы SIZE, оставляя резервуар наполовину пустым. Это следует учитывать при расчете медианы.
Алекс

Есть ли способ сделать это быстрее, рассчитав разницу вместо медианы? Достаточно ли для этого удаленного и добавленного образца и предыдущей медианы?
inf3rno

30

Самый эффективный способ вычисления процентиля потока, который я нашел, - это алгоритм P²: Радж Джейн, Имрих Хламтак: Алгоритм P² для динамического вычисления квантилей и гистограмм без сохранения наблюдений. Commun. ACM 28 (10): 1076-1085 (1985)

Алгоритм прост в реализации и работает очень хорошо. Это оценка, однако, имейте это в виду. Из аннотации:

Предложен эвристический алгоритм для динамического расчета медианы и других квантилей. Оценки производятся динамически по мере генерации наблюдений. Наблюдения не сохраняются; следовательно, алгоритм имеет очень маленькое и фиксированное требование к памяти независимо от количества наблюдений. Это делает его идеальным для реализации в квантовом чипе, который может использоваться в промышленных контроллерах и рекордерах. Алгоритм далее распространяется на построение гистограммы. Точность алгоритма анализируется.


2
Count-Min Sketch лучше P ^ 2 в том смысле, что он также дает оценку ошибки, в то время как последняя - нет.
sinoTrinity

1
Также рассмотрим «Эффективные в пространстве онлайн-вычисления квантильных сумм» Гринвальда и Ханны, которые также дают границы ошибок и имеют хорошие требования к памяти.
Павел Чернох

1
Кроме того , для вероятностного подхода, посмотреть в блоге: research.neustar.biz/2013/09/16/... и бумагу , что она относится здесь: arxiv.org/pdf/1407.1121v1.pdf Это называется «Скромный Потоковое "
Павел Чернох

27

Если мы хотим найти медиану из n самых последних увиденных элементов, у этой задачи есть точное решение, для которого необходимо сохранить только n самых последних увиденных элементов в памяти. Это быстро и хорошо масштабируется.

An индексируемые skiplist опоры О (пер п) вставка, удаление и индексированный поиск произвольных элементов, сохраняя при этом порядок сортировки. В сочетании с очередью FIFO, которая отслеживает n-ю самую старую запись, решение простое:

class RunningMedian:
    'Fast running median with O(lg n) updates where n is the window size'

    def __init__(self, n, iterable):
        self.it = iter(iterable)
        self.queue = deque(islice(self.it, n))
        self.skiplist = IndexableSkiplist(n)
        for elem in self.queue:
            self.skiplist.insert(elem)

    def __iter__(self):
        queue = self.queue
        skiplist = self.skiplist
        midpoint = len(queue) // 2
        yield skiplist[midpoint]
        for newelem in self.it:
            oldelem = queue.popleft()
            skiplist.remove(oldelem)
            queue.append(newelem)
            skiplist.insert(newelem)
            yield skiplist[midpoint]

Вот ссылки на полный рабочий код (простая для понимания версия класса и оптимизированная версия генератора с индексируемым кодом пропускаемого списка):


7
Хотя, если я правильно понимаю, это даст вам медиану только из последних N элементов, а не всех элементов до этого момента. Это похоже на действительно удобное решение для этой операции.
Андрей С

16
Правильно. Ответ звучит так, как будто можно было найти медиану всех элементов, просто сохранив последние n элементов в памяти - это вообще невозможно. Алгоритм просто находит медиану последних n элементов.
Ганс-Петер Стёрр

8
Термин «текущая медиана» обычно используется для обозначения медианы подмножества данных. ОП используется общий термин нестандартным способом.
Рэйчел Хеттингер

18

Интуитивно понятный способ думать об этом заключается в том, что если бы у вас было полностью сбалансированное бинарное дерево поиска, то корнем был бы медианный элемент, поскольку там было бы одинаковое количество меньших и больших элементов. Теперь, если дерево не заполнено, это не совсем так, поскольку на последнем уровне будут отсутствовать элементы.

Поэтому вместо этого мы можем получить медиану и два сбалансированных бинарных дерева, одно для элементов, меньших, чем медиана, и одно для элементов, превышающих медиану. Два дерева должны быть одинакового размера.

Когда мы получаем новое целое число из потока данных, мы сравниваем его с медианой. Если оно больше медианы, мы добавляем его в нужное дерево. Если два размера дерева отличаются более чем на 1, мы удаляем элемент min правого дерева, делаем его новой медианой и помещаем старую медиану в левое дерево. Аналогично для меньших.


Как ты собираешься это сделать? «мы удаляем элемент min правого дерева»
Hengameh

2
Я имел в виду деревья бинарного поиска, поэтому элемент min полностью удален от корня.
Ирэн Папаконстантину

7

Эффективное - это слово, которое зависит от контекста. Решение этой проблемы зависит от количества выполненных запросов относительно количества вставок. Предположим, вы вводите N чисел и K раз к концу, который вас интересовал медианой. Сложность алгоритма на основе кучи будет O (N log N + K).

Рассмотрим следующую альтернативу. Вставьте числа в массив, и для каждого запроса запустите алгоритм линейного выбора (скажем, с помощью стержня быстрой сортировки). Теперь у вас есть алгоритм с временем выполнения O (KN).

Теперь, если K достаточно мало (редкие запросы), последний алгоритм на самом деле более эффективен, и наоборот.


1
В примере с кучей, поиск - это постоянное время, поэтому я думаю, что это должно быть O (N log N + K), но ваша точка зрения остается в силе.
Андрей С

Да, хорошая мысль, отредактирую это. Вы правы N log N по-прежнему главный термин.
Петерис

-2

Разве вы не можете сделать это только с одной кучей? Обновление: нет. Смотрите комментарий.

Инвариант: после прочтения 2*nвходных данных минимальная куча содержит nнаибольшее из них.

Цикл: прочитайте 2 входа. Добавьте их обоих в кучу и удалите мин кучи. Это восстанавливает инвариант.

Таким образом, когда 2nсчитаны входные данные, min кучи является n-м по величине. Для усреднения двух элементов вокруг средней позиции и обработки запросов после нечетного числа входных данных потребуется немного больше сложностей.


1
Не работает: вы можете выбросить вещи, которые позже окажутся ближе к вершине. Например, попробуйте свой алгоритм с номерами от 1 до 100, но в обратном порядке: 100, 99, ..., 1.
zellyn

Спасибо, Зеллин. Глупо с моей стороны убедить себя, что инвариант был восстановлен.
Дариус Бэкон
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.