Отримайте 100 найвищих цифр із нескінченного списку


53

Одному моєму другові було задано це питання інтерв'ю -

"Існує постійний потік чисел, що надходять з якогось нескінченного списку чисел, з яких вам потрібно підтримувати структуру даних, щоб повернути топ-100 найвищих чисел у будь-який момент часу. Припустимо, що всі числа є лише цілими числами."

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

Потім питання було продовжено -

"Чи можете ви переконатися, що наказ про вставку повинен бути O (1)? Чи це можливо?"

Наскільки я знав, навіть якщо ви додасте нове число до списку та ще раз сортуєте його за допомогою будь-якого алгоритму сортування, найкраще це буде O (logn) для quicksort (я думаю). Тож мій друг сказав, що це неможливо. Але він не був переконаний, він попросив підтримувати будь-яку іншу структуру даних, а не список.

Я подумав про врівноважене Бінарне дерево, але навіть там ви не отримаєте вставлення з порядком 1. Отже, те саме питання у мене є і зараз. Хотів дізнатись, чи існує така структура даних, яка може робити вставку в Порядок 1 для вищезазначеної проблеми або це взагалі неможливо.


19
Можливо, це я просто нерозумію питання, але навіщо вам вести відсортований список? Чому б просто не відслідковувати найменше число, а якщо зустрічається число, яке вище, ніж таке, видаліть найменше число та введіть нове число, не зберігаючи список відсортованим. Це дало б вам О (1).
EdoDodo

36
@EdoDodo - і після цієї операції, як ви знаєте, що таке нове найменше число?
Damien_The_Unbeliever

19
Сортуйте список [O (100 * журнал (100)) = O (1)] або виконайте лінійний пошук через нього як мінімум [O (100) = O (1)], щоб отримати нове найменше число. Ваш список має постійний розмір, тому всі ці операції також є постійним часом.
Випадково832

6
Вам не доведеться проводити сортування всього списку. Вам байдуже, що найбільше чи 2-е за версією. Вам просто потрібно знати, що таке найнижчий. Тож після того, як ви вставите нове число, ви просто переходите 100 чисел і бачите, який зараз найнижчий. Це постійний час.
Том Зіч

27
Асимптотический порядок операції цікавий тільки тоді , коли розмір проблеми може рости необмежено. З вашого питання дуже незрозуміло, яка кількість зростає без обмежень; здається, ви запитуєте, що таке асимптотичний порядок для проблеми, розмір якої обмежений 100; це навіть не розумне запитання; щось має рости без зв’язків. Якщо питання "чи можете ви зробити це для збереження верхньої n, а не топ-100, в O (1) час?" тоді питання є розумним.
Ерік Ліпперт

Відповіді:


35

Скажімо, k - це кількість найвищих цифр, які ви хочете знати (100 у вашому прикладі). Потім ви можете додати нове число, в O(k)якому також є O(1). Тому що O(k*g) = O(g) if k is not zero and constant.


6
O (50) - це O (n), а не O (1). Вставлення до списку довжини N в O (1) час означає, що час не залежить від значення N. Це означає, що якщо 100 стає 10000, 50 НЕ повинно ставати 5000.

18
@hamstergene - але у випадку з цим питанням, чи Nрозмір відсортованого списку чи кількість оброблених до цього часу елементів? Якщо ви обробляєте 10000 предметів і зберігаєте в списку 100 найпопулярніших елементів, або ви обробляєте 1000000000 елементів і зберігаєте топ-100 елементів у відсортованому списку, витрати на вставку у цьому списку залишаються однаковими.
Damien_The_Unbeliever

6
@hamstergene: У такому випадку ви неправильно зрозуміли основи. У вашому посиланню вікіпедії є властивість ( «Множення на константу») O(k*g) = O(g) if k not zero and constant. => O(50*1) = O(1).
duedl0r

9
Я думаю, що duedl0r вірно. Давайте зменшимо проблему і скажемо, що вам потрібні лише мінімальні та максимальні значення. Це O (n), тому що мінімум і максимум 2? (n = 2). № 2 є частиною визначення проблеми. Це константа, тому ак в O (k * щось) еквівалентно O (щось)
xanatos

9
@hamstergene: про яку функцію ти говориш? значення 100 здається мені досить постійним ..
duedl0r

19

Зберігайте список несортованим. З'ясування, чи потрібно вставляти нове число, займе більше часу, але вставка буде O (1).


7
Я думаю, що це отримає нагороду smart-aleck, якщо нічого іншого. * 8 ')
Марк Бут

4
@Emilio, ти технічно правильний - і, звичайно, це найкращий вид правильних…
Гарет

1
Але ви також можете зберегти найнижчі зі своїх 100 чисел, тоді ви також можете вирішити, чи потрібно вставляти в O (1). Тоді лише коли ви вставляєте номер, вам потрібно шукати нове найнижче число. Але це відбувається рідше, ніж вирішити вставити чи ні, що відбувається з кожним новим номером.
Андрій Вайна II

12

Це легко. Розмір списку постійних, тому час сортування списку є постійним. Кажуть, що операція, яка виконується в постійний час, O (1). Тому сортування списку O (1) для списку фіксованого розміру.


9

Після того, як ви пройдете 100 чисел, максимальна вартість, яку ви коли-небудь понесете для наступного числа, - це вартість перевірити, чи є це число найвищим 100 номерів (давайте позначимо цей CheckTime ) плюс вартість ввести його в цей набір і витягнути найнижчий (назвемо EnterTime ), який є постійним часом (принаймні для обмежених чисел), або O (1) .

Worst = CheckTime + EnterTime

Далі, якщо розподіл чисел є випадковим, середня вартість зменшується тим більше у вас чисел. Наприклад, шанс, що вам доведеться ввести 101-е число в максимальний набір, становить 100/101, шанси для 1000-го числа становитимуть 1/10, а шанси на n-е число - 100 / n. Таким чином, наше рівняння середньої вартості буде:

Average = CheckTime + EnterTime / n

Таким чином, по мірі наближення n до нескінченності важливий лише CheckTime :

Average = CheckTime

Якщо числа зв'язані, CheckTime є постійним, і, таким чином, це час O (1) .

Якщо цифри не пов'язані, час перевірки зростатиме із збільшенням чисел. Теоретично це пов'язано з тим, що якщо найменше число в максимальному наборі стане досить великим, ваш час перевірки буде більшим, тому що вам доведеться враховувати більше біт. Звідси здається, що це буде трохи вище, ніж постійний час. Однак ви також можете стверджувати, що шанс, що наступне число знаходиться у найвищому наборі, наближається до нуля, оскільки n наближається до нескінченності, і тому шанс вам знадобиться врахувати більше бітів також підходить до 0, що було б аргументом для O (1) час.

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


За винятком того, що список є довільним, що робити, якщо це список постійно зростаючих чисел?
dan_waterworth

@dan_waterworth: Якщо нескінченний список є арбітражним і випадково коли-небудь збільшується (шанси на це становитимуть 1 / ∞!), це відповідало б гіршому сценарію CheckTime + EnterTimeкожного числа. Це має сенс лише в тому випадку, якщо числа без обмежень, і так, CheckTimeі EnterTimeвони збільшаться принаймні логарифмічно через збільшення розміру чисел.
Briguy37

1
Числа не випадкові, є довільні. Немає сенсу говорити про шанси.
dan_waterworth

@dan_waterworth: Ви вже двічі говорили, що числа довільні. Звідки ти це береш? Також я вважаю, що ви все ще можете застосовувати статистику до довільних чисел, починаючи з випадкового випадку, і покращувати їх точність, оскільки ви більше знаєте про арбітра. Наприклад, якби ти був арбітром, виявляється, є більше шансів вибрати постійно зростаючі числа, ніж якщо, скажімо, я був арбітром;)
Briguy37

7

це легко, якщо ви знаєте Бінарні кучі дерев . Бінарні купи підтримують введення в середній постійний час, O (1). І надати вам легкий доступ до перших x елементів.


Навіщо зберігати непотрібні вам елементи? (значення занадто низькі) Схоже, що власніший алгоритм є більш підходящим. Не кажучи, що ви не можете "не додати" значення, якщо вони не перевищують найнижчі.
Стівен Євріс

Я не знаю, моя інтуїція підказує мені, що купа (певного аромату) могла б дуже добре це зняти. Це не означає, що йому доведеться зберегти всі елементи для цього. Я не досліджував це, але це "добре" (TM).
Риг

3
Купу можна змінити, щоб відкинути що-небудь нижче деякого рівня mth (для двійкових купи і k = 100, m було б 7, оскільки кількість вузлів = 2 ^ m-1). Це уповільнить це, але все одно буде амортизовано постійний час.
Plutor

3
Якщо ви використовували двійковий min-heap (оскільки тоді верхній мінімум, який ви весь час перевіряєте), і ви знаходите нове число> min, тоді вам слід видалити верхній елемент, перш ніж ви зможете вставити новий . Видалення верхнього (хв) елемента буде O (logN), оскільки вам доведеться один раз пройти кожен рівень дерева. Тож єдино технічно вірно, що вставки є середніми O (1), оскільки на практиці це все-таки O (logN) кожного разу, коли ви знайдете число> min.
Скотт Уїтлок

1
@Plutor, ви припускаєте деякі гарантії, що бінарні купи не дають вам. Візуалізуючи це як двійкове дерево, можливо, кожен елемент у лівій гілці менший, ніж будь-який елемент правої гілки, але ви припускаєте, що найменші елементи найближчі до кореня.
Пітер Тейлор

6

Якщо запитання, яке інтерв'юер насправді мав намір задати, «чи можемо ми переконатися, що кожне вхідне число обробляється за постійний час», то стільки, про які вже вказувалося (наприклад, див. Відповідь @ duedl0r), рішення вашого друга вже є O (1), і було б так, навіть якби він використовував несортований список, або використовував сортування бульбашок чи що-небудь ще. У цьому випадку питання не має великого сенсу, якщо тільки це не було складним питанням чи ви його неправильно запам’ятали.

Я припускаю, що запитання інтерв'юера мало сенс, що він не запитував, як зробити щось, щоб бути O (1), що, очевидно, вже це.

Оскільки складність алгоритму опитування має сенс лише тоді, коли розмір вкладу нескінченно зростає, і єдиний вхід, який може зростати тут, становить 100 - розмір списку; Я припускаю, що справжнє питання було "чи можемо ми переконатися, що ми отримуємо час, що витрачається на верхній N (1) за число (не O (N), як у рішенні вашого друга)?"

Перше, що спадає на думку - це підрахунок сорту, який купуватиме складність часу O (1) за число для задачі Top-N для ціни використання простору O (m), де m - довжина діапазону вхідних чисел . Так що так, можливо.


4

Скористайтеся чергою з мінімальним пріоритетом, реалізованою з купою Фібоначчі , яка має постійний час вставки:

1. Insert first 100 elements into PQ
2. loop forever
       n = getNextNumber();
       if n > PQ.findMin() then
           PQ.deleteMin()
           PQ.insert(n)

4
"Операції видаляють і видаляють мінімальну роботу за O(log n)амортизований час" , тому це все одно призведе до того, O(log k)де kзберігається кількість предметів.
Стівен Євріс

1
Це не відрізняється від відповіді Еміліо, який отримав назву "премії smart-aleck", оскільки видалення min працює в O (log n) (згідно Вікіпедії).
Ніколь

@Renesis Відповідь Еміліо буде O (k), щоб знайти мінімум, моя - O (log k)
Гейб Мутхарт

1
@Gabe Справедливий, я просто маю на увазі в принципі. Іншими словами, якщо ви не вважаєте, що 100 є постійною, то ця відповідь також не є постійним часом.
Ніколь

@Renesis Я видалив (неправильне) твердження з відповіді.
Гейб Мутхарт

2

Завдання чітко - знайти алгоритм, який є O (1) довжиною N необхідного списку чисел. Тож не має значення, чи потрібні вам топ-100 чи 10000 номерів, час вставки повинен бути O (1).

Хитрість тут полягає в тому, що хоча для вставки списку згадується вимога O (1), у питанні нічого не сказано про порядок пошуку часу у всьому просторі чисел, але виявляється, що це можна зробити O (1) так само. Тоді рішення наступне:

  1. Упорядкуйте хештел за допомогою цифр для ключів і пар пов'язаних списків покажчиків на значення. Кожна пара покажчиків - це початок і кінець пов'язаної послідовності списку. Зазвичай це буде лише один елемент, а потім наступний. Кожен елемент у зв'язаному списку йде поруч із елементом із наступним найвищим числом. Таким чином, пов'язаний список містить відсортовану послідовність необхідних чисел. Зберігайте запис про найменше число.

  2. Візьміть нове число x із випадкового потоку.

  3. Це вище останнього зафіксованого найменшого числа? Так => Крок 4, Ні => Крок 2

  4. Натисніть на хеш-таблицю із щойно взятим номером. Чи є запис? Так => Крок 5. Ні => Візьміть нове число x-1 і повторіть цей крок (це простий лінійний пошук вниз, просто несіть мене тут, це можна вдосконалити, і я поясню як)

  5. За допомогою елемента списку, щойно отриманого з хеш-таблиці, вставити нове число відразу після елемента у зв'язаному списку (та оновити хеш)

  6. Візьміть найменше записане число l (і видаліть його з хешу / списку).

  7. Натисніть на хеш-таблицю із щойно взятим номером Чи є запис? Так => Крок 8. Ні => Візьміть нове число l + 1 і повторіть цей крок (це простий лінійний пошук вгору)

  8. При позитивному хіті число стає новим найнижчим числом. Перейдіть до кроку 2

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

Вставкою тут є O (1). Згадані пошукові запити, я думаю, щось подібне, O (середня різниця між числами). Середня різниця збільшується з розміром простору чисел, але зменшується з необхідною довжиною списку чисел.

Отже, лінійна стратегія пошуку є досить поганою, якщо простір чисел великий (наприклад, для 4-байтного типу int, від 0 до 2 ^ 32-1) і N = 100. Щоб обійти цю проблему з продуктивністю, ви можете зберегти паралельні набори хештелів, де числа округляються до більших величин (наприклад, 1s, 10s, 100s, 1000s), щоб зробити відповідні ключі. Таким чином ви можете підняти зусилля вгору та вниз, щоб швидше виконати потрібні пошуки. Після цього продуктивність стає O (часовий діапазон журналу), я думаю, що це постійне, тобто O (1) також.

Щоб зробити це зрозумілішим, уявіть, що у вас є номер 197. Ви потрапили в хеш-таблицю 10s, на якій було написано '190', вона округляється до найближчої десятки. Що-небудь? Ні. Отже, ви спускаєтесь через 10 с, поки не натиснете скажімо 120. Потім ви можете почати з 129 у хештейлі 1s, потім спробуйте 128, 127, поки щось не вдарить. Тепер ви знайшли, куди у зв'язаному списку потрібно вставити число 197. Поки ви вводите його, ви також повинні оновити хешблет 1s із записом 197, хешблет 10s з номером 190, 100s зі 100 тощо. Найбільш кроки Ви коли-небудь повинні робити це в 10 разів більше журналу діапазону чисел.

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

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


1
Пошук повинен бути частиною функції вставки - вони не є незалежними функціями. Оскільки ваш пошук - O (n), функцією вставки також є O (n).
Кірк Бродхерст

Ні. Використовуючи описану вами стратегію, де більше хеш-таблиць використовується для швидшого переходу простору чисел, це O (1). Прочитайте ще раз мою відповідь.
Бенедикт

1
@ Бенедикт, у вашій відповіді чітко сказано, що він має лінійні пошуки на кроках 4 та 7. Лінійний пошук не є O (1).
Пітер Тейлор

Так, так, але я з цим розбираюся пізніше. Ви б не хотіли насправді прочитати решту, будь ласка. Якщо потрібно, я відредагую свою відповідь, щоб вона була чітко зрозумілою.
Бенедикт

@Benedict Ви правильні - виключаючи пошук, ваша відповідь - O (1). На жаль, це рішення не буде працювати без пошуку.
Кірк Бродхерст

1

Чи можна вважати, що номери мають фіксований тип даних, наприклад, цілий? Якщо так, то зберігайте підрахунок кожного доданого числа. Це операція O (1).

  1. Оголосіть масив з якомога більшою кількістю елементів:
  2. Прочитайте кожне число під час трансляції.
  3. Підрахуйте число. Ігноруйте це, якщо це число було помічено вже в 100 разів, оскільки воно вам ніколи не знадобиться. Це запобігає переповненню, щоб не зводити його нескінченну кількість разів.
  4. Повторіть з кроку 2.

Код VB.Net:

Const Capacity As Integer = 100

Dim Tally(Integer.MaxValue) As Integer ' Assume all elements = 0
Do
    Value = ReadValue()
    If Tally(Value) < Capacity Then Tally(Value) += 1
Loop

Після повернення списку ви можете тривати скільки завгодно. Просто перейдіть до кінця списку та створіть новий список найвищих 100 зафіксованих значень. Це операція O (n), але це не має значення.

Dim List(Capacity) As Integer
Dim ListCount As Integer = 0
Dim Value As Integer = Tally.Length - 1
Dim ValueCount As Integer = 0
Do Until ListCount = List.Length OrElse Value < 0
    If Tally(Value) > ValueCount Then
        List(ListCount) = Value
        ValueCount += 1
        ListCount += 1
    Else
        Value -= 1
        ValueCount = 0
    End If
Loop
Return List

Редагувати: насправді це не має значення, чи це фіксований тип даних. Зважаючи на відсутність обмежень щодо споживання пам'яті (або жорсткого диска), ви можете зробити цю роботу для будь-якого діапазону позитивних чисел.


1

Сто номерів легко зберігаються в масиві розміром 100. Будь-яке дерево, список або набір є надмірними, враховуючи завдання, що знаходиться під рукою.

Якщо вхідне число вище найнижчого (= останнього) в масиві, запустіть всі записи. Як тільки ви знайдете перший, який менший за ваш новий номер (для цього ви можете скористатися фантазійними пошуками), пробігте решту масиву, просуваючи кожен запис "вниз" по одному.

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


0

Ви можете використовувати Binary Max-Heap. Вам доведеться відслідковувати покажчик на мінімальний вузол (який може бути невідомим / null).

Ви починаєте, вставляючи перші 100 чисел у купу. Максимум буде вгорі. Після цього ви завжди будете зберігати 100 номерів.

Потім, коли ви отримаєте новий номер:

if(minimumNode == null)
{
    minimumNode = findMinimumNode();
}
if(newNumber > minimumNode.Value)
{
    heap.Remove(minimumNode);
    minimumNode = null;
    heap.Insert(newNumber);
}

На жаль, findMinimumNodeце O (n), і ви несете цю вартість один раз за вставку (але не під час вставки :). Видалення мінімального вузла та вставлення нового вузла в середньому є O (1), оскільки вони будуть прагнути до нижньої частини купи.

Йдучи в інший бік з Binary Min-Heap, хв знаходиться у верхній частині, що чудово підходить для пошуку хвилини для порівняння, але засихає, коли вам доведеться замінити мінімум на нове число, яке> min. Це тому, що вам потрібно видалити міні-вузол (завжди O (logN)), а потім вставити новий вузол (середній O (1)). Отже, у вас все ще є O (logN), який кращий, ніж Max-Heap, але не O (1).

Звичайно, якщо N постійний, то у вас завжди є O (1). :)

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