Знайти запущену медіану з потоку цілих чисел


223

Можливий дублікат:
середній алгоритм прокатки в С

Враховуючи, що цілі числа читаються з потоку даних. Знайдіть медіану елементів, прочитаних на сьогодні, ефективно.

Прочитане нами рішення: Ми можемо використовувати максимальну купу на лівій стороні для представлення елементів, менших за ефективну медіану, і міні-купу на правій стороні, щоб представити елементи, що перевищують ефективну медіану.

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

Але як би ми побудували максимальну купу і міні-купу, тобто як ми могли б знати ефективну медіану тут? Я думаю, що ми б вставили 1 елемент у max-heap, а потім наступний 1 елемент у min-heap і так далі для всіх елементів. Виправте мене, якщо я тут помиляюся.


10
Розумний алгоритм, використовуючи купи. З назви я не міг одразу придумати рішення.
Mooing Duck

1
Рішення vezier мені добре виглядає, за винятком того, що я припускав (хоча ви не заявляли), що цей потік може бути довільно довгим, тому ви не змогли зберегти все в пам'яті. Це так?
Запуск Диких

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

2
Почати можна з обох кущ пустими. Перший інт йде в одну купу; другий переходить або в інший, або ви переміщаєте перший елемент в іншу купу і потім вставляєте. Це узагальнює "не дозволяти одній купі збільшити більше, ніж інший +1", і спеціальний кожух не потрібен ("кореневе значення" порожньої купи можна визначити як 0)
Джон Ватт,

Я СПОСОБ отримав це питання на співбесіді з MSFT. Дякую за публікацію
R Клавен

Відповіді:


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
Ви також можете мати обмежене рішення пам'яті, використовуючи купи, як це пояснено в одному з коментарів до самого питання.
Хакан Серце

1
Ви можете знайти реалізацію розчину на основі купи c тут.
AShelly

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

2
@HakanSerce Чи можете ви поясніть, чому ми зробили те, що ми зробили? Я маю на увазі, що я бачу це, але це не вдається зрозуміти інтуїтивно.
шива

51

Якщо ви не можете втримати всі предмети в пам'яті одразу, ця проблема стає набагато складнішою. Рішення купи вимагає, щоб ви зберегли всі елементи в пам'яті одразу. Це неможливо в більшості реальних застосувань цієї проблеми.

Натомість, як ви бачите числа, слідкуйте за підрахунком кількості разів, коли ви бачите кожне ціле число. Якщо припустити 4 байтові цілі числа, це 2 ^ 32 відра, або щонайбільше 2 ^ 33 цілих числа (ключ і підрахунок для кожного int), що становить 2 ^ 35 байт або 32GB. Ймовірно, це буде набагато менше, ніж це, тому що вам не потрібно зберігати ключ або рахувати ті записи, які дорівнюють 0 (наприклад, як вирок за замовчуванням у python). Це потрібно постійний час для вставки кожного нового цілого числа.

Тоді в будь-якій точці, щоб знайти медіану, просто використовуйте підрахунки, щоб визначити, яке ціле число є середнім елементом. Це займає постійний час (хоч і велика константа, але все-таки константа).


3
Якщо майже всі номери будуть переглянуті один раз, то розріджений список займе ще більше пам’яті. І здається досить ймовірним, що якщо у вас так багато номерів, вони не відповідають кількості, що більшість номерів з’являться один раз. Незважаючи на це, це розумне рішення для масового підрахунку чисел.
Mooing Duck

1
Я погоджуюся, що для рідкого списку це гірше щодо пам’яті. Хоча якщо цілі числа розподілені випадковим чином, ви почнете отримувати дублікати набагато раніше, ніж передбачає інтуїція. Див. Mathworld.wolfram.com/BirthdayProblem.html . Тож я впевнений, що це стане ефективним, як тільки ви отримаєте навіть кілька ГБ даних.
Ендрю С

4
@AndrewC чи можете ви пояснити, як буде потрібно постійний час, щоб знайти медіану. Якщо я бачив п різних видів цілих чисел, то в гіршому випадку останнім елементом може бути медіана. Це робить медіану знаходження O (n) активності.
шшнк

@shshnk Чи не n загальна кількість елементів, яка >>> 2 ^ 35 в цьому випадку?
VishAmdi

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

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² для динамічного обчислення квантолів та гістограм без збереження спостережень. Комун. ACM 28 (10): 1076-1085 (1985)

Алгоритм прямо реалізований та працює надзвичайно добре. Це, однак, оцінка, тому майте це на увазі. З реферату:

Запропоновано евристичний алгоритм для динамічного обчислення qf медіани та інших квантилів. Оцінки виробляються динамічно в міру формування спостережень. Спостереження не зберігаються; отже, алгоритм має дуже малу та фіксовану вимогу зберігання незалежно від кількості спостережень. Це робить його ідеальним для впровадження в кількісний чіп, який можна використовувати в промислових контролерах та рекордерах. Алгоритм далі поширюється на побудову гістограми. Проаналізовано точність алгоритму.


2
Ескіз Count-Min є кращим за 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 елементів.
Ганс-Пітер Стрер

8
Термін "працює медіана" зазвичай використовується для позначення медіани підмножини даних. В ОП використовується загальний термін нестандартним способом.
Рейчел Хеттінгер

18

Інтуїтивно зрозумілим способом є те, що якби у вас було повне збалансоване дерево бінарного пошуку, корінь був би серединним елементом, оскільки там було б стільки ж менших і більших елементів. Тепер, якщо дерево не заповнене, це буде не зовсім так, оскільки будуть відсутні елементи з останнього рівня.

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

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


Як ти будеш це робити? "ми видаляємо min елемент правого дерева"
Hengameh

2
Я мав на увазі дерева бінарного пошуку, тому елемент min залишається повністю від кореня.
Ірен Папаконстантіну

7

Ефективне - це слово, яке залежить від контексту. Вирішення цієї проблеми залежить від кількості виконаних запитів щодо кількості вставок. Припустимо, ви вставляєте N чисел і K разів до кінця, що вас зацікавив медіану. Складність алгоритму на основі купи буде O (N log N + K).

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

Тепер, якщо K достатньо мало (нечасті запити), останній алгоритм насправді є більш ефективним і навпаки.


1
У прикладі купи, пошук - це постійний час, тому я думаю, що це повинен бути O (N log N + K), але ваша точка все-таки має місце.
Ендрю С

Так, хороший момент, це відредагує. Ви маєте рацію N log N все ще є провідним терміном.
Петерсіс

-2

Хіба ти не можеш це зробити лише однією групою? Оновлення: ні. Дивіться коментар.

Інваріант: Після зчитування 2*nвходів міні-купа тримає nнайбільший з них.

Петля: Прочитайте 2 входи. Додайте їх обоє до купи та зніміть хв. Це відновлює інваріант.

Отже, коли 2nдані були прочитані, мінус купи - п’ятий за величиною. Потрібно мати трохи додаткових ускладнень, щоб оцінити два елементи навколо медіанної позиції та обробити запити після непарної кількості входів.


1
Не працює: ви можете кинути речі, які згодом виявляються біля верху. Наприклад, спробуйте свій алгоритм цифрами від 1 до 100, але у зворотному порядку: 100, 99, ..., 1.
zellyn

Спасибі, зеллін. Дурне мене, щоб переконати себе в інваріанті, було відновлено.
Дарій Бекон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.