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

300

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

Я смог дать только решение методом грубой силы, которое должно было отсортировать массив за O (nlogn) сложность времени и взять последние 100 чисел.

Arrays.sort(array);

Интервьюер искал лучшую временную сложность, я попробовал несколько других решений, но не смог ответить на него. Есть ли лучшее решение сложности времени?

userx
источник
70
Возможно, проблема в том, что это был не вопрос сортировки , а вопрос поиска .
Geomagas
11
С технической точки зрения, сортировка может быть не лучшим способом решения проблемы, но я не думаю, что это грубая сила - я могу придумать гораздо худшие способы сделать это.
Бернхард Баркер
88
Я просто подумал о еще более глупом методе грубой силы ... Найдите все возможные комбинации из 100 элементов из массива в 1 миллиард элементов и посмотрите, какая из этих комбинаций имеет наибольшую сумму.
Шашанк
10
Обратите внимание, что все детерминированные (и правильные) алгоритмы O(1)в этом случае, потому что нет увеличения размера. Интервьюер должен был спросить «Как найти m самых больших элементов из массива n с n >> m?».
Бакуриу
3
Возможный дубликат получения 100 лучших номеров из ста миллионов номеров
Адриан Маккарти

Ответы:

328

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

РЕДАКТИРОВАТЬ: как отметил 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 (logn). В первый раз у вас все получилось (упорядоченная очередь или очередь с приоритетами), пока Skizz не заставил вас вторую догадаться.
Дев
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
Я согласен, задание вопроса интервьюеру действительно имеет большое значение. Фактически, такой вопрос, как вы ограничены вычислительной мощностью или нет, также может помочь вам распараллелить решение, используя несколько вычислительных узлов.
Sumit Nigam
1
@R_G Нет необходимости просматривать весь список. Достаточно выбрать небольшую часть (например, миллион) случайных членов списка, чтобы получить полезную статистику.
Итамар
Для тех, кто не задумывался об этом решении, я бы рекомендовал прочитать о счетной сортировке en.wikipedia.org/wiki/Counting_sort . На самом деле это довольно распространенный вопрос интервью: можете ли вы отсортировать массив лучше, чем O (nlogn). Этот вопрос является лишь продолжением.
Максим Шерами
69

Вы можете перебирать числа, которые занимают O (n)

Всякий раз, когда вы найдете значение, превышающее текущий минимум, добавьте новое значение в круговую очередь размером 100.

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

Regenschein
источник
3
Это не работает например, найти топ 2 из {1, 100, 2, 99} даст {100,1} как топ 2.
Skizz
7
Вы не можете обойтись, чтобы держать очередь отсортированной. (если вы не хотите каждый раз искать в очереди дыр следующий наименьший элемент)
MrSmith42
3
@ MrSmith42 Частичная сортировка, как в куче, достаточно. Смотрите ответ Рона Теллера.
Кристофер Кройциг
1
Да, я молча предположил, что извлечение-min-очередь реализовано в виде кучи.
Regenschein
Вместо циклической очереди используйте min heap размера 100, сверху будет минимум сто номеров. Это займет только O (log n) для вставки по сравнению с o (n) в случае очереди
techExplorer
33

Я понял, что это помечено как «алгоритм», но выбрасывает некоторые другие варианты, поскольку, вероятно, также следует пометить «интервью».

Каков источник 1 миллиарда чисел? Если это база данных, тогда «выбор значения из порядка таблиц по значению desc limit 100» вполне бы сработал - могут быть различия в диалектах.

Это одноразовое или что-то, что будет повторяться? Если повторяется, как часто? Если это одноразовый файл и данные находятся в файле, то 'cat srcfile | сортировать (варианты по необходимости) | head -100 'позволит вам быстро выполнять продуктивную работу, за которую вам платят, в то время как компьютер справляется с этой тривиальной работой.

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

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

Удачи на следующем интервью.

Фред Митчелл
источник
2
Исключительный ответ. Все остальные сосредоточились на технической стороне вопроса, в то время как этот ответ затрагивает деловую социальную часть вопроса.
13
2
Я никогда не думал, что вы могли бы сказать спасибо и оставить интервью, а не ждать его окончания. Спасибо, что открыли мой разум.
УрсулРосу
1
Почему мы не можем создать кучу из миллиарда элементов и извлечь 100 самых больших элементов. Этот путь стоимость = O (млрд) + 100 * O (лог (млрд)) ??
Мохит Шах
17

Моей непосредственной реакцией на это было бы использование кучи, но есть способ использовать QuickSelect, не сохраняя все входные значения под рукой одновременно.

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

В конце у вас есть лучшие 100 значений. Для значений N вы запустили QuickSelect примерно N / 100 раз. Стоимость каждого Quickselect примерно в 200 раз превышает некоторую постоянную, поэтому общая стоимость в 2N раза превышает постоянную. Это выглядит линейно по размеру входных данных для меня, независимо от размера параметра, который я собираюсь установить равным 100 в этом объяснении.

mcdowella
источник
10
Вы можете добавить небольшую, но, возможно, важную оптимизацию: после запуска QuickSelect для разбиения массива размера 200 известен минимум из первых 100 элементов. Затем, при переборе всего набора данных, заполняйте только нижние 100 значений, если текущее значение больше текущего минимума. Простая реализация этого алгоритма в C ++ наравне с тем, что libstdc ++ partial_sortработает непосредственно с набором данных из 200 миллионов 32-битных данных int(созданным через MT19937, равномерно распределенным).
13
1
Хорошая идея - не влияет на анализ наихудшего случая, но выглядит хорошо, стоит сделать.
McDowella
@mcdowella Стоит попробовать, и я сделаю это, спасибо!
userx
8
Это именно то, что Ordering.greatestOf(Iterable, int) делает Гуава . Это абсолютно линейное и однопроходное время, и это очень симпатичный алгоритм. К тому же, у нас также есть некоторые фактические критерии: его постоянные коэффициенты на несколько медленнее, чем традиционная очередь с приоритетами в среднем случае, но эта реализация намного более устойчива к «худшему» вводу (например, строго восходящий ввод).
Луи Вассерман
15

Вы можете использовать алгоритм быстрого выбора, чтобы найти число по индексу (по порядку) [billion-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) (средняя производительность по случаю)

Второй вариант, предложенный Томасом Юнгблутом :

Использование кучного строительство MAX кучи будет принимать O (N), то топ 100 Макс число будет находиться в верхней части кучи, все , что вам нужно , это получить их из кучи (100 XO (Log (N)).

Время этого алгоритма: O (N) + 100 XO (Log (N)) = O (N)

One Man Crew
источник
8
Вы работаете по всему списку три раза. 1 био целые числа примерно 4 ГБ, что бы вы сделали, если бы не поместили их в память? quickselect - худший из возможных вариантов в этом случае. Итерация один раз и сохранение кучи из топ-100 элементов - ИМХО самое эффективное решение в O (n) (обратите внимание, что вы можете отключить O (log n) вставок кучи, так как n в куче равно 100 = константа = очень крошечная ).
Томас Юнгблут
3
Несмотря на то, что это все еще происходит O(N), выполнение двух быстрых выборок и еще одного линейного сканирования требует больше затрат, чем необходимо.
Кевин
Это код PSEUDO, все решения здесь займут больше времени (O (NLOG (N) или 100 * O (N))
One Man Crew
1
100*O(N)(если это правильный синтаксис) = O(100*N)= O(N)(предположительно, 100 может быть переменной, если это так, это не совсем верно). Да, и Quickselect имеет худшую производительность O (N ^ 2) (ой). И если он не помещается в память, вы будете дважды перезагружать данные с диска, что намного хуже, чем один раз (это узкое место).
Бернхард Баркер
Существует проблема , что , как ожидается , время работы, а не худший случай, но с использованием стратегии приличной выбора поворота (например , выбрать 21 элементов в случайном порядке, и выбрать медиану этих 21 в качестве оси), то число сравнений может быть гарантируется с высокой вероятностью не более (2 + c) n для сколь угодно малой постоянной c.
One Man Crew
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, содержащего случайные показатели

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

MRIP
источник
4
Ваш последний параграф - ключевой момент: с миллиардом чисел невозможно хранить все данные в памяти или обмениваться элементами. (По крайней мере, так я бы истолковал проблему, учитывая, что это был вопрос для интервью.)
Тед Хопп,
14
В любом алгоритмическом вопросе, если чтение данных является проблемой, это должно быть упомянуто в вопросе. Вопрос гласит: «дан массив», а не «дан массив на диске, который не помещается в память и не может управляться в соответствии с моделью фон Неймана, которая является стандартом в анализе алгоритмов». В эти дни вы можете получить ноутбук с 8 гигабайтами оперативной памяти. Я не уверен, откуда возникла идея провести миллиард чисел в памяти. На моей рабочей станции прямо сейчас хранится несколько миллиардов номеров.
Mrip
К вашему сведению, в наихудшем случае выполнения быстрого выбора является O (n ^ 2) (см. En.wikipedia.org/wiki/Quickselect ), и оно также изменяет порядок элементов во входном массиве. Возможно наихудшее решение O (n) с очень большой константой ( en.wikipedia.org/wiki/Median_of_medians ).
оч
Наихудший случай быстрого выбора вряд ли возможен по экспоненте, что означает, что для практических целей это не имеет значения. Легко изменить быстрый выбор, так что с высокой вероятностью число сравнений будет (2 + c) n + o (n) для сколь угодно малого c.
13
«Факт остается фактом, что quickselect найдет решение быстрее, чем использование очереди размером 100» - Нет. Решение для кучи требует сравнения N + Klog (N) и среднего значения 2N для быстрого выбора и 2,95 для медианы медиан. Это явно быстрее для данного К.
Нил Г
5

возьмите первые 100 номеров миллиарда и рассортируйте их. Теперь просто переберите миллиард, если номер источника больше, чем наименьшее из 100, вставьте в порядке сортировки. То, что вы в итоге получите, будет намного ближе к O (n) по размеру набора.

Сэмюэл Терстон
источник
3
упс не видел более подробный ответ, чем мой собственный.
Сэмюэль Терстон
Возьмите первые 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. Задание на карте: подсчитайте частоту или время каждого элемента. Уменьшить: получить верхний элемент К.

Обычно я бы дал рекрутеру два ответа. Дайте им все, что они хотят. Конечно, кодирование с уменьшением карты будет трудоемким, потому что вы должны знать все точные параметры. Не вредно практиковать это. Удачи.

Крис Су
источник
+1 за MapReduce, я не могу поверить, что ты был единственным, кто упомянул Hadoop на миллиард номеров. Что, если интервьюер попросил 1 000 000 000 номеров? Вы заслуживаете большего количества голосов по моему мнению.
Сильвиу Бурча
@Silviu Burcea Большое спасибо. Я тоже ценю MapReduce. :)
Крис Су
Хотя размер 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)
Крис Су
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)?
miracle173
1

Вдохновленный ответом @ron teller, вот программа на Си, которая делает то, что вы хотите.

#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 элементов. Поэтому имеет смысл провести эксперимент и измерить реальную производительность.

miracle173
источник
1
Это то, что делает куча.
Нил Г
@ Нил Г: Что "это"?
чудо173
1
Верхняя часть кучи является минимальным элементом в куче, и новые элементы отклоняются при одном сравнении.
Нил Г
1
Я понимаю, что вы говорите, но даже если вы используете абсолютное количество сравнений, а не асимптотическое число сравнений, массив все равно намного медленнее, потому что время «вставить новый элемент, отбросить старый минимум и найти новый минимум» составляет 100, а не около 7.
Нил Г
1
Хорошо, но ваша оценка очень окольна. Вы можете напрямую рассчитать ожидаемое количество вставок, которое будет k (digamma (n) - digamma (k)), которое меньше, чем klog (n). В любом случае решение «куча» и «массив» проводят только одно сравнение для отбрасывания элемента. Единственное отличие состоит в том, что число сравнений для вставленного элемента составляет 100 для вашего решения против 14 для кучи (хотя средний случай, вероятно, намного меньше.)
Нил Г
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.

Алгоритм Самые большие x элементов из n:

Я назову возвращаемое значение LIST . Это набор элементов x (по моему мнению, это должен быть связанный список)

  • Первые элементы x берутся из пула «по мере их поступления» и сортируются в LIST (это делается за постоянное время, так как x рассматривается как постоянное время - O (x log (x)))
  • Для каждого следующего элемента мы проверяем, является ли он больше, чем наименьший элемент в LIST, и, если это, вынимаем наименьший элемент и вставляем текущий элемент в LIST. Поскольку это упорядоченный список, каждый элемент должен найти свое место в логарифмическом времени (бинарный поиск), и поскольку упорядоченный список не является проблемой, вставка не является проблемой. Каждый шаг также выполняется за постоянное время (O (log (x)).

Итак, каков наихудший сценарий?

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

Так что это O (N) время для худшего случая. +1 - это проверка, если число больше, чем наименьшее число в LIST. Ожидаемое время для среднего случая будет зависеть от математического распределения этих n элементов.

Возможные улучшения

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

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

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

операции.

Для этого варианта использования я не вижу дальнейших улучшений. И все же вы должны спросить себя - что, если мне придется делать это больше, чем log (n) раз и для разных x-es? Очевидно, что мы отсортировали бы этот массив в O (n log (n)) и взяли бы наш элемент x всякий раз, когда они нам нужны.

Rouz
источник
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 последовательных чисел. Благодаря этому случайному выбору мы можем быть совершенно уверены, что очень немногие из оставшихся чисел входят в первую сотню, поэтому время выполнения будет очень близко к времени простого цикла, сравнивающего миллиард чисел с некоторым максимальным значением.

gnasher729
источник
1

Поиск лучших 100 из миллиарда номеров лучше всего сделать с помощью min-heap из 100 элементов.

Сначала заполните мин-кучу первыми 100 встреченными числами. min-heap будет хранить наименьшее из первых 100 чисел в корне (вверху).

Теперь, когда вы идете вдоль остальных чисел, сравните их только с корнем (наименьшее из 100).

Если обнаруженное новое число больше, чем корень из min-heap, замените корень на это число, иначе проигнорируйте его.

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

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

imsaar
источник
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 миллиард. Чтобы быть справедливым, хотя, я кормил его вводом наихудшего случая (по иронии судьбы массив, который уже отсортирован).

Shashank
источник
0

Я вижу много O (N) обсуждений, поэтому я предлагаю что-то другое только для упражнения мысли.

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

Тем не мение! Посмотрите, заполняет ли какой-либо механизм заполнения списков этот список в определенном порядке. Находятся ли они в четко определенной схеме, в которой вы можете точно знать, что наибольшая величина чисел будет найдена в определенной области списка или в определенном интервале? Там может быть образец для этого. Если это так, например, если они гарантированно находятся в каком-то нормальном распределении с характерным горбом в середине, всегда имеют повторяющиеся восходящие тренды среди определенных подмножеств, имеют продолжительный всплеск в некоторый момент времени T в середине данных Например, это может быть случай инсайдерской торговли или отказа оборудования, или, может быть, просто иметь «всплеск» для каждого N-го числа, так как при анализе сил после катастрофы вы можете значительно сократить количество проверяемых записей.

В любом случае, есть пища для размышлений. Может быть, это поможет вам дать будущим интервьюерам вдумчивый ответ. Я знаю, что был бы впечатлен, если бы кто-то задал мне такой вопрос в ответ на такую ​​проблему - это бы сказало мне, что они думают об оптимизации. Просто признайте, что не всегда есть возможность оптимизировать.

djdanlib
источник
0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. Создать пустой список из 100 пустых слотов

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

    • Если число меньше первого, пропустите

    • В противном случае замените его на этот номер

    • Затем нажмите номер через смежный обмен; пока он не станет меньше следующего

  3. Вернуть список


Примечание: если log(input-list.size) + c < 100, то оптимальным способом является сортировка списка ввода, а затем разбить первые 100 элементов.

Khaled.K
источник
0

Сложность O (N)

Сначала создайте массив из 100 дюймов, инициализируйте первый элемент этого массива как первый элемент из 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 единиц, то остальные цифры с нулями исключают. Теперь из оставшихся чисел перейдем к следующему наиболее значимому биту. вести подсчет количества оставшихся чисел после исключения и продолжать до тех пор, пока это число больше 100.

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

Пандуранга Рао Садху
источник
0

Я бы выяснил, у кого было время собрать миллиард чисел в массив и уволить его. Должен работать на правительство. По крайней мере, если бы у вас был связанный список, вы могли бы вставить число в середину, не сдвигая полмиллиарда, чтобы освободить место. Еще лучше Btree позволяет бинарный поиск. Каждое сравнение устраняет половину вашей суммы. Алгоритм хеширования позволил бы вам заполнить структуру данных как шахматную доску, но не так хорошо для разреженных данных. Лучше всего иметь массив решений из 100 целых чисел и отслеживать минимальное число в массиве решений, чтобы вы могли заменить его, когда натолкнетесь на большее число в исходном массиве. Вам нужно будет посмотреть на каждый элемент в исходном массиве, предполагая, что он не отсортирован с самого начала.

Дэвид Аллан Хаузер-младший
источник
0

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

Джеймс Оравец
источник
1
Этот подход почти идентичен как наиболее, так и вторым по популярности ответам на этот вопрос.
Бернхард Баркер
0

Управление отдельным списком - это дополнительная работа, и вам придется перемещаться по всему списку каждый раз, когда вы найдете другую замену. Просто выполните сортировку и возьмите топ-100.

Крис Фокс
источник
-1 быстрая сортировка - это O (n log n), что именно то, что сделал OP и просит улучшить. Вам не нужно управлять отдельным списком, только список из 100 номеров. Ваше предложение также имеет нежелательный побочный эффект изменения исходного списка или его копирования. Это 4 ГБ или около того памяти, ушел.
0
  1. Используйте n-й элемент, чтобы получить 100-й элемент O (n)
  2. Повторяйте второй раз, но только один раз и выводите каждый элемент, который больше, чем этот конкретный элемент.

Пожалуйста, обратите внимание esp. второй шаг может быть легко вычислен параллельно! И это также будет эффективно, когда вам нужен миллион самых больших элементов.

математический
источник
0

Это вопрос от Google или других гигантов отрасли. Возможно, следующий код - правильный ответ, ожидаемый вашим интервьюером. Стоимость времени и стоимость пространства зависят от максимального числа во входном массиве. Для 32-битного ввода массива int, максимальная стоимость пространства составляет 4 * 125M байт, стоимость времени составляет 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 элементов из миллиарда чисел.

Juvenik
источник
-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