Один из моих знакомых спросил с вопросом
Получение максимальных 100 номеров из 100 миллионов номеров
в недавнем интервью о работе. У вас есть идея придумать эффективный способ его решения?
Один из моих знакомых спросил с вопросом
Получение максимальных 100 номеров из 100 миллионов номеров
в недавнем интервью о работе. У вас есть идея придумать эффективный способ его решения?
Запустите их через min-heap размера 100: для каждого номера входа k
замените текущий min m
на max(k, m)
. После этого куча содержит 100 самых больших входов.
Поисковая система, такая как Lucene, может использовать этот метод с уточнениями для выбора наиболее релевантных поисковых ответов.
Изменить: Я не могу провести собеседование - я дважды получил детали (после того, как сделал это раньше, на производстве). Здесь код для проверки; он почти такой же, как стандарт Python heapq.nlargest()
:
import heapq
def funnel(n, numbers):
if n == 0: return []
heap = numbers[:n]
heapq.heapify(heap)
for k in numbers[n:]:
if heap[0] < k:
heapq.heapreplace(heap, k)
return heap
>>> funnel(4, [3,1,4,1,5,9,2,6,5,3,5,8])
[5, 8, 6, 9]
Хорошо, вот действительно глупый ответ, но он действительный:
Рассуждение:
Это хорошее решение для какой-то одноразовой операции. Он будет сосать его x раз в секунду или что-то в этом роде. Но тогда нам нужно больше контекста - поскольку mclientk также имел свой простой оператор SQL - предполагая, что 100 миллионов номеров не существует в памяти, это вопрос, который может быть осуществлен из-за того, что... они могут поступать из базы данных и большую часть времени будут при разговоре о соответствующих бизнес-номерах.
Как таковой, вопрос действительно трудно ответить - эффективность должна быть определена.
Мергосор в партиях по 100, а затем сохранить только 100.
Кстати, вы можете масштабировать его во всех направлениях, в том числе одновременно.
Если данные уже находятся в массиве, который вы можете изменить, вы можете использовать вариант алгоритма Hoare Select, который (в свою очередь) является вариантом Quicksort.
Основная идея довольно проста. В Quicksort вы разбиваете массив на две части: один из элементов больше, чем опорный, а другой - меньше. Затем вы рекурсивно сортируете каждый раздел.
В алгоритме Select вы делаете шаг секционирования точно так же, как раньше - но вместо рекурсивной сортировки обоих разделов вы смотрите, какой раздел содержит нужные вам элементы, и рекурсивно выбирайте ТОЛЬКО в этом разделе. Например, предположив, что ваш 100 миллионов элементов разделяется почти наполовину, первые несколько итераций вы будете смотреть только на верхний раздел.
В конце концов, вы, вероятно, достигнете точки, где часть, которую вы хотите "мосты" двух разделов - например, у вас есть раздел из 150 чисел, а когда вы разбиваете, вы получаете две части ~ 75 кусок. В этот момент изменяется только одна незначительная деталь: вместо отказа от одного раздела и продолжения работы только другой, вы принимаете верхний раздел из 75 элементов, а затем продолжаете искать верхние 25 в нижнем разделе.
Если вы делаете это на С++, вы можете сделать это с помощью std::nth_element
(который обычно будет реализован примерно так, как описано выше). В среднем, это имеет линейную сложность, которая, как я считаю, так же хороша, как вы можете надеяться (без какого-либо существующего порядка, я не вижу никакого способа найти верхние элементы N, не глядя на все элементы).
Если данные уже не находятся в массиве, и вы (например) читаете данные из файла, вы обычно хотите использовать кучу. Вы в основном читаете элемент, вставляете его в кучу, и если куча больше вашей цели (в этом случае 100 единиц), вы удаляете один и повторно heapify.
Что, вероятно, не так очевидно (но на самом деле верно) заключается в том, что вы обычно не хотите использовать максимальную кучу для этой задачи. На первый взгляд это кажется довольно очевидным: если вы хотите получить максимальные элементы, вы должны использовать максимальную кучу.
Проще, однако, думать в терминах предметов, которые вы "удаляете" из кучи. Массивная куча позволяет быстро найти один самый большой элемент в куче. Однако он не оптимизирован для поиска наименьшего элемента в куче.
В этом случае нас интересует прежде всего наименьший элемент в куче. В частности, когда мы читаем каждый элемент из файла, мы хотим сравнить его с наименьшим элементом в куче. Если (и только если) оно больше, чем наименьший элемент в куче, мы хотим заменить этот наименьший элемент, находящийся в куче, с новым элементом. Поскольку это (по определению) больше, чем существующее, нам нужно будет просеять это в правильное положение в куче.
Но обратите внимание: если элементы в файле упорядочены произвольно, когда мы читаем файл, мы довольно быстро достигаем точки, в которой большинство элементов, которые мы читаем в файле, будет меньше, чем самый маленький элемент в нашей куче. Поскольку у нас есть легкий доступ к наименьшему элементу в куче, это довольно быстро и легко сделать это сравнение, а для небольших предметов никогда не вставлять в кучу вообще.
Под TOP 100
, вы имеете в виду 100 крупнейших? Если да:
SELECT TOP 100 Number FROM RidiculouslyLargeTable ORDER BY Number DESC
Убедитесь, что вы сообщите интервьюеру, что вы считаете, что таблица правильно проиндексирована.
Нет причин сортировать весь список. Это должно выполняться в O (n) времени. В псевдокоде:
List top = new List
for each num in entireList
for i = 0 to top.Length
if num > top[i] then
top.InsertBefore(num, i)
if top.Length > 100 then
top.Remove(top.Length - 1)
end if
exit for
else
if i = top.Length - 1 and i < 100 then
top.Add(num)
end if
end if
next
next
@darius можно на самом деле улучшить!!!
Посредством "обрезки" или отсрочки операции замены кучи по мере необходимости
Предположим, что a = 1000 в верхней части кучи
Это c, b братья и сестры
Мы знаем, что c, b > 1000
a=1000
+-----|-----+
b>a c>a
We now read the next number x=1035
Since x>a we should discard a.
Instead we store (x=1035, a=1000) at the root
We do not (yet) bubble down the new value of 1035
Note that we still know that b,c<a but possibly b,c>x
Now, we get the next number y
when y<a<x then obviously we can discard it
when y>x>a then we replace x with y (the root now has (y, a=1000))
=> we saved log(m) steps here, since x will never have to bubble down
when a>y>x then we need to bubble down y recursively as required
Worst run time is still O(n log m)
But average run time i think might be O(n log log m) or something
In any case, it is obviously a faster implementation
Измените массив в O (n). Затем вытащите 100 лучших элементов.
Я храню первые 100 номеров в Max -Heap размером 100.
На последнем уровне я отслеживаю минимальное количество и новый номер, который я вставляю и проверяю с минимальным номером. Если входящий номер является кандидатом на 100.
- Снова я призываю reheapify, чтобы у меня всегда была максимальная куча 100 лучших.
Таким образом, его сложность - O (nlogn).
int numbers[100000000000] = {...};
int result[100] = {0};
for( int i = 0 ; i < 100000000000 ; i++ )
{
for( int j = 0 ; j < 100 ; j++ )
{
if( numbers[i] > result[j] )
{
if( j < 99 )
{
memcpy(result+j+1, result+j, (100-j)*sizeof(int));
}
result[j] = numbers[i];
break;
}
}
}
Первая итерация:
Быстросортировать, принимать 100 лучших. O (n log n). Простой, простой в использовании. Очень очевидно.
лучше? Мы работаем с числами, делаем сортировку радикса (линейное время), занимая 100 лучших. Я бы ожидал, что это то, что ищет интервьюер.
Любые другие соображения? Ну, миллион номеров - это не так много памяти, но если вы хотите свести к минимуму объем памяти, вы до сих пор сохраняете до 100 номеров, а затем просто просматриваете номера. Каким будет лучший способ?
Некоторые упомянули кучу, но немного лучшее решение может быть двусвязным списком, где вы держите указатель на минимум 100 лучших, найденных до сих пор. Если вы столкнулись с номером a, который больше текущего наименьшего в списке, по сравнению со следующим элементом и переместите число рядом с текущим, пока не найдете место для нового номера. (Это в основном просто специализированная куча для ситуации). При некоторой настройке (если число больше текущего минимума, сравните с текущим максимумом, чтобы увидеть, в каком направлении находится список прохода, чтобы найти точку вставки), это будет относительно эффективно и займет всего 1,5 тыс. Памяти.
Предположим, что mylist - это список из сотни миллионов данных. поэтому мы можем отсортировать список и взять последние сотни данных из списка.
mylist.sort()
MyList [-100:]
Второй способ:
import heapq
heapq.nlargest(100, mylist)