Напишіть програму, щоб знайти 100 найбільших чисел із масиву в 1 мільярд чисел


300

Нещодавно я взяв участь в інтерв'ю, де мене попросили "написати програму, щоб знайти 100 найбільших чисел з масиву в 1 мільярд чисел".

Мені вдалося лише дати грубе рішення, яке повинно було сортувати масив за часовою складністю O (nlogn) та взяти останні 100 чисел.

Arrays.sort(array);

Інтерв'юер шукав кращої складності в часі, я спробував пару інших рішень, але не зміг відповісти на нього. Чи є краще рішення щодо складності часу?


70
Можливо, проблема полягає в тому, що це було не сортування , а шукальне .
geomagas

11
Як технічна примітка, сортування може бути не найкращим способом вирішення проблеми, але я не думаю, що це жорстока сила - я можу придумати набагато гірші способи її вирішення.
Бернхард Баркер

88
Я просто подумав про ще більш дурний метод грубої сили ... Знайдіть усі можливі комбінації 100 елементів з масиву елементів 1 мільярда і подивіться, яка з цих комбінацій має найбільшу суму.
Шашанк

10
Зауважте, що всі детерміновані (і правильні) алгоритми є O(1)в цьому випадку, оскільки збільшення розмірів не відбувається. Інтерв'юер повинен був запитати "Як знайти m найбільших елементів з масиву n з n >> m?".
Бакуріу

Відповіді:


328

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

EDIT: як зазначав Dev, з чергою пріоритетів, реалізованою разом із купою, складність вставки до черги становитьO(logN)

У гіршому випадку ви отримуєте кращу, ніжbillionlog2(100)billionlog2(billion)

Взагалі, якщо вам потрібні найбільші числа K із набору N чисел, складність O(NlogK)швидше, ніж O(NlogN)це, це може бути дуже суттєво, коли K дуже малий порівняно з N.

EDIT2:

Очікуваний час цього алгоритму досить цікавий, оскільки в кожній ітерації вставка може відбуватися або не мати місце. Імовірність введення i-го числа до черги - це ймовірність того, що випадкова величина буде більшою, ніж принаймні i-Kвипадкових змінних з того ж розподілу (перші k числа автоматично додаються до черги). Ми можемо використовувати статистику замовлень (див. Посилання ) для обчислення цієї ймовірності. Наприклад, припустимо, що числа були вибрані випадковим чином рівномірно {0, 1}, очікуване значення (iK) -го числа (з i чисел) є (i-k)/i, а шанс випадкової величини буде більшим, ніж це значення 1-[(i-k)/i] = k/i.

Таким чином, очікувана кількість вставок становить:

введіть тут опис зображення

І очікуваний час роботи можна виразити так:

введіть тут опис зображення

( kчас для генерації черги з першими kелементами, потім n-kпорівняння та очікувана кількість вставок, як описано вище, кожна займає середній log(k)/2час)

Зауважте, що коли Nдуже великий порівняно з Kцим, цей вираз є набагато ближчим n, ніж NlogK. Це дещо інтуїтивно, тому що у випадку запитання, навіть після 10000 повторень (що дуже мало порівняно з мільярдним), шанс ввести номер у чергу дуже малий.


6
Це фактично лише O (100) для кожної вставки.
MrSmith42

8
@RonTeller Не можна ефективно виконувати двійковий пошук пов’язаного списку, тому черговість пріоритетів зазвичай реалізується за допомогою купи. Час вставки, як описано, - O (n), а не O (вхід). У вас це було правильно в перший раз (замовлена ​​черга чи пріоритетна черга), поки Skizz не змусив вас вдруге здогадатися.
Dev

17
@ThomasJungblut мільярд також є постійною, тому якщо це так, це O (1): P
Рон Теллер

9
@RonTeller: зазвичай подібні питання стосуються того, щоб знайти 10 найпопулярніших сторінок із мільярдів результатів пошуку Google, або 50 найпоширеніших слів за слово хмара, або 10 найпопулярніших пісень на MTV тощо. Отже, я вважаю, у звичайних обставинах це можна вважати k постійним і малим порівняно з n. Хоча завжди слід пам’ятати про ці «нормальні обставини».
подруга

5
Оскільки у вас є елементи 1G, відбирайте 1000 елементів випадковим чином і вибирайте найбільші 100. Це повинно уникати вироджених випадків (відсортованих, зворотно відсортованих, в основному відсортованих), значно зменшивши кількість вставок.
ChuckCottrill

136

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

Опис досить загальний, тому, можливо, ви можете запитати його про діапазон чи значення цих чисел, щоб зрозуміти проблему. Це може вразити інтерв'юера. Якщо, наприклад, ці цифри означають вік людей у ​​країні (наприклад, Китай), то це набагато простіша проблема. З обґрунтованим припущенням, що ніхто з живих не старше 200 років, ви можете використовувати масив int розміром 200 (можливо, 201), щоб підрахувати кількість людей з тим самим віком лише за одну ітерацію. Тут індекс означає вік. Після цього це шматок пирога, щоб знайти 100 найбільшої кількості. До речі цей альго називається лічильним сортом .

У будь-якому випадку, зробити питання більш конкретним та зрозумілим, це добре для вас в інтерв'ю.


26
Дуже хороші бали. Ніхто інший нічого не запитував і не вказував про розподіл цих номерів - це може змінити значення, як підійти до проблеми.
NealB

13
Мені б хотілося, щоб ця відповідь була достатньою, щоб продовжити її. Прочитайте цифри один раз, щоб отримати мінімальні / максимальні значення, щоб можна було припустити їх розподіл. Потім візьміть один із двох варіантів. Якщо діапазон досить малий, побудуйте масив, де ви можете просто перевірити номери в міру їх виникнення. Якщо діапазон занадто великий, використовуйте відсортований алгоритм нагромадження, обговорений вище .... Просто думка.
Richard_G

2
Я погоджуюсь, що запитання назад інтерв'юеру справді має велике значення. Насправді, таке питання, як ви обмежені потужністю обчислень чи ні, також може допомогти вам паралелізувати рішення за допомогою кількох обчислювальних вузлів.
Суміт Нігам

1
@R_G Не потрібно переглядати весь список. Досить відібрати невелику частку (наприклад, мільйон) випадкових членів списку, щоб отримати корисну статистику.
Ітамар

Для тих, хто не замислювався б над цим рішенням, рекомендую почитати про підрахунок сортування en.wikipedia.org/wiki/Counting_sort . Це насправді досить поширене питання інтерв'ю: чи можете ви відібрати масив краще, ніж O (nlogn). Це питання - лише розширення.
Maxime Chéramy

69

Ви можете перебирати числа, які приймають O (n)

Щоразу, коли ви знайдете значення, що перевищує поточний мінімум, додайте нове значення до кругової черги розміром 100.

Мінімум цієї кругової черги - це ваше нове значення порівняння. Продовжуйте додавати до цієї черги. Якщо повна, витягніть мінімум із черги.


3
Це не працює. напр., знайдіть топ-2 із {1, 100, 2, 99}, як топ-2 вийде {100,1}
Skizz

7
Ви не можете обійти, щоб провести сортування черги. (якщо ви не хочете кожен раз шукати чергу в
отворах

3
@ MrSmith42 Часткового сортування, як у купі, достатньо. Дивіться відповідь Рона Теллера.
Крістофер Кройцгіг

1
Так, я мовчки припускав, що черга на екстракт-хв реалізується як купа.
Regenschein

Замість кругової черги використовуйте міні-купу розміром 100, це буде мати мінімум сто номерів вгорі. Для вставки знадобиться лише O (log n) порівняно з o (n) у випадку черги
techExplorer

33

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

Яке джерело 1 мільярда чисел? Якщо це база даних, то "вибрати значення з порядку таблиці за значенням desc limit 100" виконає цю роботу досить непогано - можуть бути діалектні відмінності.

Це одноразове чи щось, що повториться? Якщо повторюється, як часто? Якщо він одноразовий, а дані знаходяться у файлі, тоді 'cat srcfile | сортувати (потрібні варіанти) | head -100 'дозволить вам швидко виконати продуктивну роботу, яку вам платять, поки комп'ютер обробляє цю дрібницю.

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

Нарешті, є такий розгляд. Ви шукаєте роботу початкового рівня та співбесіду з придурливим менеджером чи майбутнім співробітником? Якщо так, то ви можете викинути всілякі підходи, що описують відносні технічні плюси та мінуси. Якщо ви шукаєте більш управлінську роботу, тоді підходите до неї так, як менеджер, стурбований витратами на розробку та технічне обслуговування рішення, і скажіть «дуже дякую» і залиште, якщо саме цей інтерв'юер хоче зосередитись на дрібницях CS . Він і ви навряд чи матимете великий потенціал просування там.

Пощастить у наступному інтерв’ю.


2
Виняткова відповідь. Всі інші зосередилися на технічній стороні питання, тоді як ця відповідь стосується ділової соціальної частини цього питання.
vbocan

2
Я ніколи не уявляв, що ти можеш сказати спасибі і залишити інтерв'ю, а не чекати, коли воно закінчиться. Дякую, що відкрили свою думку.
UrsulRosu

1
Чому ми не можемо створити купу мільярдів елементів і витягти 100 найбільших елементів. Таким чином вартість = O (мільярд) + 100 * O (log (мільярд)) ??
Мохіт Шах

17

Моєю негайною реакцією на це було б використання купи, але є спосіб використовувати QuickSelect, не зберігаючи всі вхідні значення під рукою в один момент.

Створіть масив розміром 200 і заповніть його першими 200 вхідними значеннями. Запустіть QuickSelect і відкиньте низький 100, залишивши вам 100 вільних місць. Прочитайте наступні 100 вхідних значень та запустіть QuickSelect знову. Продовжуйте, поки ви не запустите, хоча весь внесок партіями по 100.

Наприкінці ви маєте перші 100 значень. Для N значень ви запустили QuickSelect приблизно N / 100 разів. Кожен Quickselect коштує приблизно в 200 разів більше постійної, тому загальна вартість у 2N рази більша від постійної. Це виглядає лінійним за розміром вхідного сигналу для мене, незалежно від розміру параметра, який я вкладаю в цей пояснення, дорівнює 100.


10
Ви можете додати невелику, але можливо важливу оптимізацію: Після запуску QuickSelect для розділу масиву розміром 200, відомий мінімум із 100 найпопулярніших елементів. Потім при ітерації над усім набором даних заповнюйте лише нижчі 100 значень, якщо поточне значення перевищує поточний мінімум. Проста реалізація цього алгоритму в C ++ знаходиться нарівні з partial_sortзапуском libstdc ++ безпосередньо на наборі даних в 200 мільйонів 32-бітних int(створених за допомогою MT19937, рівномірно розподілених).
dyp

1
Гарна ідея - не впливає на аналіз найгіршого випадку, але, схоже, варто зробити це.
mcdowella

@mcdowella Варто спробувати, і я це зроблю, дякую!
користувачx

8
Це саме те , що Ordering.greatestOf(Iterable, int) робить Гуава . Це абсолютно лінійний час і однопрохідний, і це дуже милий алгоритм. У FWIW ми також маємо деякі фактичні орієнтири: його постійні чинники уповільнені, ніж у традиційної черги в середньому випадку, але ця реалізація набагато стійкіша до «гіршого випадку» (наприклад, суворо зростаючий внесок).
Луї Вассерман

15

Ви можете скористатися алгоритмом швидкого вибору, щоб знайти число в індексі (на замовлення) [мільярд-101], а потім перебирати цифри та знаходити числа, які більші від цього числа.

array={...the billion numbers...} 
result[100];

pivot=QuickSelect(array,billion-101);//O(N)

for(i=0;i<billion;i++)//O(N)
   if(array[i]>=pivot)
      result.add(array[i]);

Цей алгоритм Час: 2 XO (N) = O (N) (середня ефективність випадку)

Другий варіант, як Томас Юнгблут, пропонує:

Використовуйте Heap, будуючи кучу MAX, займе O (N), тоді найвищі 100 макс числа будуть у верхній частині Heap, все що вам потрібно - це дістати їх з купи (100 XO (Log (N))).

Цей алгоритм Час становить: O (N) + 100 XO (Log (N)) = O (N)


8
Ви три рази переглядаєте весь список. 1 біо. цілі числа - це приблизно 4 Гб, що б ви зробили, якщо не можете вписати їх у пам'ять? quickselect - найгірший можливий вибір у цьому випадку. Повторне повторення та збереження купи у топ-100 позицій - IMHO - найкраще рішення в O (n) (зауважте, що ви можете відрізати O (log n) вкладок купи, оскільки n в купі 100 = константа = дуже крихітна ).
Томас Юнгблут

3
Незважаючи на те, що це все ще є O(N), зробити два QuickSelects та ще одне лінійне сканування набагато більше, ніж потрібно.
Кевін

Це код PSEUDO, всі рішення тут знадобиться більше часу (O (NLOG (N) або 100 * O (N))
Екіпаж чоловіка

1
100*O(N)(якщо це дійсний синтаксис) = O(100*N)= O(N)(правда, 100 може бути змінною, якщо так, то це не зовсім вірно). О, і Quickselect має найгірший показник роботи O (N ^ 2) (ouch). І якщо це не вписується в пам'ять, ви будете перезавантажувати дані з диска двічі, що набагато гірше, ніж один раз (це вузьке місце).
Бернхард Баркер

Існує питання, що це очікується час роботи, і не в гіршому випадку, але, використовуючи гідну стратегію вибору стрижнів (наприклад, вибирайте навмання 21 елемент і вибирайте медіану з цих 21 як опорну), тоді кількість порівнянь може бути гарантовано з високою ймовірністю бути максимум (2 + c) n для довільно малої константи c.
Один чоловік Екіпаж

10

Хоча інше рішення швидкого вибору було скасовано, факт залишається фактом, що швидкий вибір знайде рішення швидше, ніж використання черги розміром 100. Швидкий вибір має очікуваний час роботи 2n + o (n) у порівнянні. Дуже просто реалізація була б

array = input array of length n
r = Quickselect(array,n-100)
result = array of length 100
for(i = 1 to n)
  if(array[i]>r)
     add array[i] to result

Це займе 3n + o (n) порівняння в середньому. Більше того, це може бути більш ефективним, використовуючи той факт, що швидкий вибір залишить найбільші 100 елементів у масиві у 100 найбільш правильних місцях. Тому насправді час роботи може бути покращений до 2n + o (n).

Існує питання, що це очікується час роботи, і не в гіршому випадку, але, використовуючи гідну стратегію вибору стрижнів (наприклад, вибирайте навмання 21 елемент і вибирайте медіану з цих 21 як опорну), тоді кількість порівнянь може бути гарантовано з високою ймовірністю бути максимум (2 + c) n для довільно малої константи c.

Насправді, використовуючи оптимізовану стратегію вибірки (наприклад, вибірки sqrt (n) елементів випадковим чином і вибираючи 99-й перцентиль), час роботи може бути скорочений до (1 + c) n + o (n) для довільно малих c (якщо припустити, що K, кількість елементів для вибору є o (n)).

З іншого боку, для використання черги розміром 100 буде потрібно порівняння O (log (100) n), а основа журналу 2 з 100 приблизно дорівнює 6,6.

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

У коментарях зазначалося, що рішення черги буде працювати у очікуваний час N + K log N на випадковому вході. Звичайно, припущення про випадковий вхід ніколи не є дійсним, якщо в питанні це прямо не зазначено. Рішення черги може бути зроблене для проходження масиву у випадковому порядку, але це спричинить додаткову вартість N викликів генератору випадкових чисел, а також перестановку всього вхідного масиву або виділення нового масиву довжиною N, що містить випадкові індекси.

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


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

14
У будь-якому алгоритмічному запитанні, якщо читання даних є проблемою, це повинно бути зазначено у запитанні. У запитанні зазначено "заданий масив", а не "заданий масив на диску, який не вміщується в пам'яті і не може маніпулювати відповідно до моделі фон Неймана, яка є стандартом при аналізі алгоритмів". У ці дні ви можете придбати ноутбук з 8 гігами оперативної пам’яті. Я не впевнений, звідки ідея зберігати мільярд чисел в пам’яті неможливо. На моєму робочому місці зараз є кілька мільярдів чисел.
mrip

Найгірший час виконання швидкого вибору FYI - це O (n ^ 2) (див. En.wikipedia.org/wiki/Quickselect ), а також він змінює порядок елементів у вхідному масиві. Можливе найгірше O (n) рішення з дуже великою константою ( en.wikipedia.org/wiki/Median_of_medians ).
пт

Найгірший випадок швидкого вибору навряд чи відбудеться експоненціально, а це означає, що для практичних цілей це не має значення. Легко змінити швидкий вибір таким чином, щоб з великою часткою ймовірності кількість порівнянь було (2 + c) n + o (n) для довільно малого c.
mrip

"факт залишається фактом, що швидкий вибір швидше знайде рішення, ніж використання черги розміром 100" - Nope. Купольний розчин займає приблизно порівняння N + Klog (N) порівняно з середнім значенням 2N для швидкого вибору та 2,95 для медіани медіанів. Це явно швидше для даного К.
Ніл G

5

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


3
oops не бачив більш детальної відповіді, ніж моя власна.
Самуель Турстон

Візьміть перші 500 або більше цифр і зупиніться лише на сортуванні (і викиньте низькі 400), коли список заповниться. (І само собою зрозуміло, що потім ви додасте до списку лише тоді, коли нове число> найнижче у вибраній 100.)
Hot Licks

4

Два варіанти:

(1) Куча (пріоритетна черга)

Підтримуйте міні-купу розміром 100. Прокладіть масив. Як тільки елемент менше, ніж перший елемент у купі, замініть його.

InSERT ELEMENT INTO HEAP: O(log100)
compare the first element: O(1)
There are n elements in the array, so the total would be O(nlog100), which is O(n)

(2) Модель зменшення карт.

Це дуже схоже на приклад підрахунку слів у hadoop. Завдання на карті: підраховуйте частоту кожного елемента або час, що з'явився. Скорочення: Отримайте верхній елемент K.

Зазвичай я б дав рекрутеру дві відповіді. Дайте їм все, що їм заманеться. Звичайно, зменшення кодування на карті буде трудовим, тому що ви повинні знати всі точні параметри. Без шкоди практикувати це. Щасти.


+1 для MapReduce, я не можу повірити, що ти єдиний згадував Hadoop на мільярд чисел. Що робити, якщо інтерв'юер запитав 1 мільярд мільярдів? На мою думку, ти заслужив більше голосів.
Сільвіу Бурса

@Silviu Burcea Дякую велике Я також ціную MapReduce. :)
Chris Su

Хоча розмір 100 є постійним у цьому прикладі, вам слід дійсно узагальнити це окремою змінною, тобто. к. Оскільки 100 є таким же постійним, як 1 мільярд, то чому ви даєте розміру великого набору чисел змінну величини n, а не для меншого набору чисел? Дійсно, ваша складність повинна бути O (nlogk), що не є O (n).
Том Херд

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

1
@TomHeard Добре. O (nlogk) Є лише один фактор, який впливатиме на результати. Це означає, що якщо n зростає більшим і більшим, "рівень результату" лінійно зростатиме. Або ми можемо сказати, навіть давши трильйонні числа, я все одно можу отримати 100 найбільших чисел. Однак ви не можете сказати: Зі збільшенням n k збільшується, так що k вплине на результат. Тому я використовую O (nlogk), але не O (nlogn)
Chris Su

4

Дуже простим рішенням було б повторити масив у 100 разів. Який є O(n).

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


1
Два недоліки - (1) Ви знищуєте вхід у процесі - цього бажано уникати. (2) Ви проходите масив кілька разів - якщо масив зберігається на диску і не може вміститися в пам'яті, це може бути майже в 100 разів повільніше, ніж прийнята відповідь. (Так, вони обоє O (n), але все ж)
Бернхард Баркер

Хороший дзвінок @Dukeling, я додав додаткові формулювання про те, як уникнути зміни вихідного вводу шляхом відстеження попередніх індексів відповідей. Що все-таки було б досить легко кодувати.
Джеймс Оравець

Яскравий приклад рішення O (n), яке набагато повільніше, ніж O (n log n). log2 (1 мільярд) - це лише 30 ...
gnasher729

@ gnasher729 Наскільки велика константа прихована в O (n log n)?
чудо173

1

Ось натхненний відповіддю телефону @ron, ось програма без голівок C, щоб робити те, що ви хочете.

#include <stdlib.h>
#include <stdio.h>

#define TOTAL_NUMBERS 1000000000
#define N_TOP_NUMBERS 100

int 
compare_function(const void *first, const void *second)
{
    int a = *((int *) first);
    int b = *((int *) second);
    if (a > b){
        return 1;
    }
    if (a < b){
        return -1;
    }
    return 0;
}

int 
main(int argc, char ** argv)
{
    if(argc != 2){
        printf("please supply a path to a binary file containing 1000000000"
               "integers of this machine's wordlength and endianness\n");
        exit(1);
    }
    FILE * f = fopen(argv[1], "r");
    if(!f){
        exit(1);
    }
    int top100[N_TOP_NUMBERS] = {0};
    int sorts = 0;
    for (int i = 0; i < TOTAL_NUMBERS; i++){
        int number;
        int ok;
        ok = fread(&number, sizeof(int), 1, f);
        if(!ok){
            printf("not enough numbers!\n");
            break;
        }
        if(number > top100[0]){
            sorts++;
            top100[0] = number;
            qsort(top100, N_TOP_NUMBERS, sizeof(int), compare_function);
        }

    }
    printf("%d sorts made\n"
    "the top 100 integers in %s are:\n",
    sorts, argv[1] );
    for (int i = 0; i < N_TOP_NUMBERS; i++){
        printf("%d\n", top100[i]);
    }
    fclose(f);
    exit(0);
}

На моїй машині (ядро i3 зі швидким SSD) це займає 25 секунд і 1724 сорти. Я створив двійковий файл з dd if=/dev/urandom/ count=1000000000 bs=1для цього запуску.

Очевидно, є проблеми з продуктивністю з читанням лише 4 байтів одночасно - з диска, але це, наприклад. З позитивного боку потрібно дуже мало пам’яті.


1

Найпростіше рішення - сканувати масив на мільярд чисел і зберегти 100 найбільших значень, знайдених на даний момент, в малому буфері масиву без будь-якого сортування і запам'ятати найменше значення цього буфера. Спочатку я подумав, що цей метод був запропонований fordprefect, але в коментарі він сказав, що він вважає, що структура даних 100 чисел реалізується як купа. Щоразу, коли буде знайдено нове число, яке перевищує мінімальне значення в буфері, переписується новим знайденим значенням, і знову буфер шукає поточний мінімум. Якщо числа в масиві числових масивів розподіляються випадковим чином, більшу частину часу значення великого масиву порівнюють з мінімальним малим масивом і відкидають. Тільки для дуже дуже невеликої частки числа значення має бути вставлено у малий масив. Тож різницею в маніпулюванні структурою даних, що містить невеликі числа, можна знехтувати. Для невеликої кількості елементів важко визначити, чи використання черги з пріоритетом насправді швидше, ніж використання мого наївного підходу.

Я хочу оцінити кількість вставок у невеликий буфер масиву 100 елементів під час сканування масиву 10 ^ 9 елементів. Програма сканує перші 1000 елементів цього великого масиву і має вставити щонайменше 1000 елементів у буфер. Буфер містить 100 елементів з 1000 сканованих елементів, тобто 0,1 відсканованого елемента. Отже, ми припускаємо, що ймовірність того, що значення з великого масиву більше, ніж поточний мінімум буфера, становить приблизно 0,1 Такий елемент повинен бути вставлений у буфер. Тепер програма сканує наступні 10 ^ 4 елементи з великого масиву. Тому що мінімум буфера буде збільшуватися щоразу, коли новий елемент буде вставлено. Ми підрахували, що співвідношення елементів, що перевищує наш поточний мінімум, становить приблизно 0,1, і для цього потрібно вставити 0,1 * 10 ^ 4 = 1000 елементів. Насправді очікувана кількість елементів, які вставляються в буфер, буде меншою. Після сканування цього 10 ^ 4 елементів частка чисел у буфері складе приблизно 0,01 від сканованих до цього часу елементів. Отже, під час сканування наступних 10 ^ 5 чисел ми припускаємо, що в буфер буде вставлено не більше 0,01 * 10 ^ 5 = 1000. Продовжуючи цю аргументацію, ми вставили близько 7000 значень після сканування 1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9 ~ 10 ^ 9 елементів великого масиву. Отже, під час сканування масиву з 10 ^ 9 елементами випадкового розміру ми очікуємо, що в буфер не буде більше 10 ^ 4 (= 7000 округлих) вставок. Після кожного вставки в буфер повинен бути знайдений новий мінімум. Якщо буфер - це простий масив, нам потрібно 100 порівняння, щоб знайти новий мінімум. Якщо буфер - це інша структура даних (наприклад, купа), нам знадобиться принаймні 1 порівняння, щоб знайти мінімум. Для порівняння елементів великого масиву нам потрібно 10 ^ 9 порівнянь. Таким чином, нам потрібно приблизно 10 ^ 9 + 100 * 10 ^ 4 = 1.001 * 10 ^ 9 порівнянь при використанні масиву в якості буфера і принаймні 1.000 * 10 ^ 9 порівнянь при використанні іншого типу структури даних (наприклад, купи) . Тож використання купи приносить лише прибуток 0,1%, якщо продуктивність визначається кількістю порівняння. Але яка різниця у часі виконання між вставкою елемента в купу елементів 100 і заміною елемента в масиві 100 елементів і знаходженням нового мінімуму? 000 * 10 ^ 9 порівнянь при використанні іншого типу структури даних (наприклад, купи). Тож використання купи приносить лише прибуток 0,1%, якщо продуктивність визначається кількістю порівняння. Але яка різниця у часі виконання між вставкою елемента в купу елементів 100 і заміною елемента в масиві 100 елементів і знаходженням нового мінімуму? 000 * 10 ^ 9 порівнянь при використанні іншого типу структури даних (наприклад, купи). Тож використання купи приносить лише прибуток 0,1%, якщо продуктивність визначається кількістю порівняння. Але яка різниця у часі виконання між вставкою елемента в купу елементів 100 і заміною елемента в масиві 100 елементів і знаходженням нового мінімуму?

  • На теоретичному рівні: скільки порівнянь потрібно для вставки в купу. Я знаю, що це O (log (n)), але наскільки великим є постійний коефіцієнт? Я

  • На рівні машини: Який вплив кешування та прогнозування гілок на час виконання вставки купи та лінійного пошуку в масиві.

  • На рівні реалізації: Які додаткові витрати приховуються в структурі даних купи, що надаються бібліотекою або компілятором?

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


1
Ось що робить купа.
Ніл Г

@Neil G: Що "це"?
чудо173

1
Верхівка купи є мінімальним елементом у купі, а нові елементи відкидаються одним порівнянням.
Ніл Г

1
Я розумію, що ви говорите, але навіть якщо ви йдете за абсолютною кількістю порівнянь, а не за асимптотичною кількістю порівнянь, масив все ще набагато повільніше, оскільки час "вставити новий елемент, відкинути старий мінімум і знайти новий мінімум" є 100, а не близько 7.
Ніл G

1
Добре, але ваша оцінка дуже кругла. Можна безпосередньо обчислити очікувану кількість вставок, що буде k (digamma (n) - digamma (k)), що менше klog (n). У будь-якому випадку, і купа, і рішення масиву проводять лише одне порівняння, щоб відкинути елемент. Єдина відмінність - кількість порівнянь для вставленого елемента 100 для вашого рішення проти 14 до купи (хоча середній випадок, ймовірно, набагато менший.)
Ніл G

1
 Although in this question we should search for top 100 numbers, I will 
 generalize things and write x. Still, I will treat x as constant value.

Алгоритм Найбільший х елементів з n:

Я подзвоню повертається значення LIST . Це набір з x елементів (на мою думку, слід пов’язати список)

  • Перші x елементи беруться з пулу "по мірі їх надходження" і сортуються у списку (це робиться в постійний час, оскільки х трактується як постійний - час (O (x log (x)))
  • Для кожного наступного елемента ми перевіряємо, чи він більший за найменший елемент у списку, і якщо ми вискакуємо найменший та вставляємо поточний елемент у СПИСОК. Оскільки це впорядкований список, кожен елемент повинен знайти своє місце в логарифмічному часі (двійковий пошук), а оскільки впорядкований введення списку не є проблемою. Кожен крок також робиться в постійний (O (log (x)) час).

Отже, який найгірший сценарій?

x log (x) + (nx) (log (x) +1) = nlog (x) + n - x

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

Можливі поліпшення

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

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

x log (x) + (nx) log (x) = nlog (x)

операції.

У цьому випадку використання я не бачу жодних вдосконалень. Але ви повинні запитати себе: що робити, якщо я повинен робити це більше, ніж журнал (n) разів та для різних x-es? Очевидно, ми би сортували цей масив в O (n log (n)) і беремо наш x елемент, коли вони нам потрібні.


1

На це запитання відповіли б складність N log (100) (замість N log N) лише одним рядком коду C ++.

 std::vector<int> myvector = ...; // Define your 1 billion numbers. 
                                 // Assumed integer just for concreteness 
 std::partial_sort (myvector.begin(), myvector.begin()+100, myvector.end());

Остаточною відповіддю буде вектор, де перші 100 елементів гарантовано становлять 100 найбільших чисел у вас масиві, а решта елементів не упорядковані

C ++ STL (стандартна бібліотека) досить зручна для подібних проблем.

Примітка. Я не кажу, що це оптимальне рішення, але це врятувало б ваше інтерв'ю.


1

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

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

Тож ми спочатку вибираємо скажімо 100 000 випадкових чисел з масиву. Щоб уникнути випадкового доступу, який може бути повільним, ми додамо, скажімо, 400 випадкових груп з 250 послідовних чисел. Завдяки цьому випадковому відбору ми можемо бути впевнені, що дуже мало решти цифр знаходяться в першій сотні, тому час виконання буде дуже близьким до часу простого циклу, порівнюючи мільярд чисел до деякого максимального значення.


1

Знайти топ-100 із мільярда чисел найкраще, використовуючи міні-купу з 100 елементів.

Спершу простежте міні-купу з наближеними першими 100 номерами. min-heap збереже найменше з перших 100 чисел у корені (вгорі).

Тепер, коли ви йдете по решті чисел, порівняйте їх лише з коренем (найменшим із 100).

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

У рамках вставки нового числа в min-heap найменше число в купі прийде до вершини (root).

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


0

Я написав просте рішення в Python на випадок, коли хтось зацікавлений. Він використовує bisectмодуль і тимчасовий список повернення, який він зберігає впорядкованому. Це схоже на виконання пріоритетної черги.

import bisect

def kLargest(A, k):
    '''returns list of k largest integers in A'''
    ret = []
    for i, a in enumerate(A):
        # For first k elements, simply construct sorted temp list
        # It is treated similarly to a priority queue
        if i < k:
            bisect.insort(ret, a) # properly inserts a into sorted list ret
        # Iterate over rest of array
        # Replace and update return array when more optimal element is found
        else:
            if a > ret[0]:
                del ret[0] # pop min element off queue
                bisect.insort(ret, a) # properly inserts a into sorted list ret
    return ret

Використання зі 100 000 000 елементів та найгіршим введенням, який є відсортованим списком:

>>> from so import kLargest
>>> kLargest(range(100000000), 100)
[99999900, 99999901, 99999902, 99999903, 99999904, 99999905, 99999906, 99999907,
 99999908, 99999909, 99999910, 99999911, 99999912, 99999913, 99999914, 99999915,
 99999916, 99999917, 99999918, 99999919, 99999920, 99999921, 99999922, 99999923,
 99999924, 99999925, 99999926, 99999927, 99999928, 99999929, 99999930, 99999931,
 99999932, 99999933, 99999934, 99999935, 99999936, 99999937, 99999938, 99999939,
 99999940, 99999941, 99999942, 99999943, 99999944, 99999945, 99999946, 99999947,
 99999948, 99999949, 99999950, 99999951, 99999952, 99999953, 99999954, 99999955,
 99999956, 99999957, 99999958, 99999959, 99999960, 99999961, 99999962, 99999963,
 99999964, 99999965, 99999966, 99999967, 99999968, 99999969, 99999970, 99999971,
 99999972, 99999973, 99999974, 99999975, 99999976, 99999977, 99999978, 99999979,
 99999980, 99999981, 99999982, 99999983, 99999984, 99999985, 99999986, 99999987,
 99999988, 99999989, 99999990, 99999991, 99999992, 99999993, 99999994, 99999995,
 99999996, 99999997, 99999998, 99999999]

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


0

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

Чи є відома інформація про природу цих чисел? Якщо це випадковий характер, то не йдіть далі і дивіться на інші відповіді. Ви не отримаєте кращих результатів, ніж вони.

Однак! Подивіться, чи будь-який механізм заповнення списку заповнює цей список у певному порядку. Чи вони у чітко визначеній схемі, де ви можете з упевненістю знати, що найбільша величина чисел знайдеться у певній області списку або на певному інтервалі? До цього може бути візерунок. Якщо це так, наприклад, якщо вони гарантовано перебувають у якомусь нормальному розподілі з характерним горбом посередині, завжди мають повторювані тенденції до зростання серед визначених підмножин, мають тривалий сплеск на деякий час Т в середині даних Якщо встановити, як, наприклад, частоту інсайдерської торгівлі чи збоїв обладнання, або, можливо, просто "спайк" кожного N-го числа, як при аналізі сил після катастрофи, ви можете зменшити кількість записів, які вам доведеться значно перевірити.

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


0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. Створіть порожній список із 100 порожніх слотів

  2. Для кожного номера в списку вхідних даних:

    • Якщо число менше першого, пропустіть

    • В іншому випадку замініть його цим номером

    • Потім натисніть число через сусідній своп; поки вона не буде меншою, ніж наступна

  3. Повернути список


Примітка: якщо log(input-list.size) + c < 100, то оптимальним способом є сортування списку вхідних даних, а потім розділити перші 100 елементів.


0

Складність становить O (N)

Спочатку створіть масив із 100 ints ініціалізуйте перший елемент цього масиву як перший елемент N значень, слідкуйте за індексом поточного елемента з іншою змінною, назвіть його CurrentBig

Ітерація, хоча N значень

if N[i] > M[CurrentBig] {

M[CurrentBig]=N[i]; ( overwrite the current value with the newly found larger number)

CurrentBig++;      ( go to the next position in the M array)

CurrentBig %= 100; ( modulo arithmetic saves you from using lists/hashes etc.)

M[CurrentBig]=N[i];    ( pick up the current value again to use it for the next Iteration of the N array)

} 

після закінчення надрукуйте масив M з CurrentBig 100 разів за модулем 100 :-) Для студента: переконайтеся, що останній рядок коду не перетворює дійсні дані безпосередньо перед виходом коду


0

Інший алгоритм O (n) -

Алгоритм знаходить найбільшу 100 за рахунок усунення

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

Основна булева операція може бути паралельно виконана на графічних процесорах


0

Я б дізнався, хто встиг покласти мільярд чисел у масив та звільнити його. Треба працювати уряду. Принаймні, якби у вас був зв'язаний список, ви можете вставити число в середину, не рухаючи півмільярда, щоб звільнити місце. Ще краще Btree дозволяє здійснювати двійковий пошук. Кожне порівняння виключає половину вашої загальної кількості. Хеш-алгоритм дозволить вам заповнити структуру даних як шахматну дошку, але не настільки хороша для розріджених даних. Оскільки найкраще зробити, щоб мати масив рішення у 100 цілих чисел і відслідковувати найменше число у вашому масиві рішення, щоб ви могли його замінити, коли ви зустрінете більшу кількість у вихідному масиві. Вам доведеться переглянути кожен елемент у вихідному масиві, припускаючи, що він не відсортований для початку.


0

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


1
Цей підхід майже ідентичний як найбільш-так і другому-найбільш обґрунтованим відповідям на це питання.
Бернхард Баркер

0

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


-1 quicksort - це O (n log n), що саме те, що зробила ОП, і просить покращити. Не потрібно керувати окремим списком, лише список зі 100 номерів. Ваша пропозиція також має небажаний побічний ефект від зміни оригінального списку або його копіювання. Ось пам’яті 4GiB або близько того, пішло.

0
  1. Використовуйте n-й елемент, щоб отримати 100-й елемент O (n)
  2. Ітераціюйте вдруге, але лише один раз, і виведіть кожен елемент, що перевищує цей конкретний елемент.

Зверніть увагу на esp. другий крок може бути простим для обчислення паралельно! І це також буде ефективно, коли вам знадобиться мільйон найбільших елементів.


0

Це питання від Google чи інших гігантів галузі. Можливо, наступний код - це правильна відповідь, яку очікує ваш інтерв'юер. Вартість часу та витрати на простір залежать від максимальної кількості вхідного масиву. Для введення 32-бітового int масиву максимальна вартість місця становить 4 * 125 М байт, час - 5 * мільярдів.

public class TopNumber {
    public static void main(String[] args) {
        final int input[] = {2389,8922,3382,6982,5231,8934
                            ,4322,7922,6892,5224,4829,3829
                            ,6892,6872,4682,6723,8923,3492};
        //One int(4 bytes) hold 32 = 2^5 value,
        //About 4 * 125M Bytes
        //int sort[] = new int[1 << (32 - 5)];
        //Allocate small array for local test
        int sort[] = new int[1000];
        //Set all bit to 0
        for(int index = 0; index < sort.length; index++){
            sort[index] = 0;
        }
        for(int number : input){
            sort[number >>> 5] |= (1 << (number % 32));
        }
        int topNum = 0;
        outer:
        for(int index = sort.length - 1; index >= 0; index--){
            if(0 != sort[index]){
                for(int bit = 31; bit >= 0; bit--){
                    if(0 != (sort[index] & (1 << bit))){
                        System.out.println((index << 5) + bit);
                        topNum++;
                        if(topNum >= 3){
                            break outer;
                        }
                    }
                }
            }
        }
    }
}

0

Я зробив свій власний код, не впевнений, що це те, що "інтерв'юер" він шукає

private static final int MAX=100;
 PriorityQueue<Integer> queue = new PriorityQueue<>(MAX);
        queue.add(array[0]);
        for (int i=1;i<array.length;i++)
        {

            if(queue.peek()<array[i])
            {
                if(queue.size() >=MAX)
                {
                    queue.poll();
                }
                queue.add(array[i]);

            }

        }

0

Можливі поліпшення.

Якщо файл містить 1 мільярд число, його читання може бути дуже довгим ...

Щоб покращити цю роботу, ви можете:

  • Розділіть файл на n частин, Створіть n ниток, зробіть по n потоків кожну на 100 найбільших чисел у своїй частині файлу (використовуючи чергу пріоритету), і нарешті отримайте 100 найбільших чисел усіх потоків виводу.
  • Використовуйте кластер, щоб зробити таке завдання, з таким рішенням, як hadoop. Тут ви можете розділити файл ще більше і швидше вивести файл на 1 мільярд (або 10 ^ 12) чисел.

0

Спочатку візьміть 1000 елементів і додайте їх у максимум купи. Тепер вийміть перші максимум 100 елементів і зберігайте їх кудись. Тепер виберіть наступні 900 елементів із файлу та додайте їх у купу разом із останнім 100 найвищим елементом.

Продовжуйте повторювати цей процес, збираючи 100 елементів із купи та додаючи 900 елементів із файлу.

Остаточний вибір 100 елементів дасть нам максимум 100 елементів із мільярда чисел.


-1

Проблема: Знайдіть m найбільших елементів з n елементів, де n >>> m

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

потім роздрукуємо останні n елементів масиву.

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

Оцінка часу виконання - O (m * n). Найкращі відповіді на даний момент - O (n log (m)), тому для малих m це рішення не суттєво дорожче.

Я не кажу, що цього не вдається покращити, але це далеко не найпростіше рішення.


1
Немає зовнішніх структур даних? Що з масивом мільярдів для сортування? Масив такого розміру - це величезні накладні витрати як для заповнення, так і для зберігання. Що робити, якщо всі "великі" числа опинилися в неправильному кінці масиву? Вам знадобиться на замовлення 100 мільярдів свопів, щоб "перекинути" їх на місце - ще одна велика накладні витрати ... Нарешті, M N = 100 мільярдів проти M Log2 (N) = 6,64 мільярда, що майже на два порядки різниці. Можливо, подумайте це. Сканування за один прохід при збереженні структури даних найбільшої кількості буде значно виправдати цей підхід.
NealB
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.