Как найти высокочастотные слова в книге в среде с нехваткой памяти? - PullRequest
5 голосов
/ 12 апреля 2009

Недавно в техническом интервью меня попросили написать программу для поиска высокочастотных слов (слов, которые встречаются максимальное количество раз) в учебнике. Программа должна быть разработана таким образом, чтобы обрабатывать весь учебник с минимальным объемом памяти. Производительность не является проблемой. Я смог запрограммировать, чтобы найти частоту слов, но это заняло много памяти.

Как сделать эту операцию менее трудоемкой? Какие стратегии / решения?

-Snehal

Ответы [ 12 ]

5 голосов
/ 12 апреля 2009

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

Другой конец спектра - посмотреть на первое слово, а затем просмотреть всю книгу, чтобы узнать, сколько раз встречается это слово. Это требует минимальной памяти. Затем вы делаете то же самое для следующего слова и просматриваете всю книгу. Если это слово встречается несколько раз, вы добавляете его как верхнее слово (или верхнее N слов). Конечно, это крайне неэффективно - если первое и третье слова совпадают, вы в конечном итоге пройдете всю книгу снова, даже если вы только что сделали то же самое для первого слова.

4 голосов
/ 12 апреля 2009

Мне, вероятно, за это проголосуют ...

Если текст английский, и вы просто хотите найти 5 самых популярных слов, вот ваша программа:

print "1. the\n";
print "2. of\n";
print "3. and\n";
print "4. a\n";
print "5. to\n";

Работает быстро и потребляет минимум памяти!

4 голосов
/ 12 апреля 2009

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

  • парсинг чанка, достаточно маленького для ограничения памяти,
  • пропуск произвольное количество текста,
  • повторить, объединяя накопленные результаты.
  • Остановитесь, когда список удовлетворительно сходится.

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

Примечание : Это предполагает, что наиболее часто встречающиеся слова встречаются чаще всего по всему тексту, а не только в одном месте текста. Для английского текста это предположение верно, поскольку во всем мире часто встречаются такие слова, как «и т. Д.». Если вас беспокоит это требование, попросите алгоритм выполнить хотя бы один проход всего текста.

4 голосов
/ 12 апреля 2009

ОК, если вас интересуют только слова с наибольшим числом n, один из способов сделать это - два прохода, причем первый проход основан на модифицированном Блум-фильтре . Вместо того, чтобы использовать битовую карту для отслеживания вхождений хеша, используйте вместо этого целочисленный массив - байтовый, 16-битный, 32-битный или даже 64-битный в зависимости от вашего размера ввода. Там, где фильтр Блума просто устанавливает бит, соответствующий каждому из значений хеша слова, вы увеличиваете счет по хеш индексу в массиве.

Проблема этого подхода состоит в том, что два слова, вероятно, дадут одинаковые значения хеш-функции. Таким образом, вам нужно сделать второй проход, где вы игнорируете слова, если их хэш-суммы не превышают определенного порога, что уменьшает объем памяти, который вам нужно выделить для точного подсчета.

Так что просто создайте битовую карту с битами, установленными для самых высоких значений хэша. Затем во втором проходе слов, если у слова есть «совпадения» в битовой карте для его хэшей, найдите его или добавьте в хэш-таблицу и увеличьте его счетчик. Это минимизирует использование памяти, создавая хеш-таблицу только из самых часто встречающихся слов.

3 голосов
/ 12 апреля 2009

Если производительность действительно не имеет значения, вы можете просто просмотреть каждое слово по очереди, проверить, находится ли оно в вашей «верхней N», и, если это не так, подсчитать все его вхождения. Таким образом, вы храните только N значений. Конечно, вы будете считать одни и те же слова много раз, но, как вы сказали, производительность не является проблемой - и код будет тривиальным (что обычно предпочтительнее - при прочих равных условиях).

2 голосов
/ 12 апреля 2009

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

Я при условии, что текст где-то хранится в автономном режиме, но есть способ перебирать каждое слово в тексте без загрузки всего текста в память.

Тогда код F # ниже находит верхние N слов. Единственная структура данных - это отображение пар ключ-значение (слово, частота), и оно удерживает только верхние N из них, поэтому использование памяти равно O (N), что мало. Время выполнения равно O (numWordsInText ^ 2), что плохо, но приемлемо с учетом проблемных ограничений. Суть алгоритма проста: для каждого слова в тексте подсчитайте, сколько раз оно встречается, и, если оно находится в рабочем режиме best-N, добавьте его в список и удалите предыдущую минимальную запись.

Обратите внимание, что нижеприведенная программа загружает весь текст в память, просто для удобства изложения.

#light
// some boilerplate to grab a big piece of text off the web for testing
open System.IO 
open System.Net 
let HttpGet (url: string) = 
    let req = System.Net.WebRequest.Create(url) 
    let resp = req.GetResponse() 
    let stream = resp.GetResponseStream() 
    let reader = new StreamReader(stream) 
    let data = reader.ReadToEnd() 
    resp.Close() 
    data 
let text = HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1"
let words = text.Split([|' ';'\r';'\n'|], System.StringSplitOptions.RemoveEmptyEntries)
// perhaps 'words' isn't actually stored in memory, but so long as we can 
// 'foreach' over all the words in the text we're good
let N = 5  // how many 'top frequency' words we want to find
let FindMin map =
    // key-value pair with mininum value in a map
    let (Some(seed)) = Map.first (fun k v -> Some(k,v)) map
    map |> Map.fold_left 
        (fun (mk,mv) k v -> if v > mv then (mk,mv) else (k,v)) 
        seed
let Main() =
    let mutable freqCounts = Map.of_list [ ("",0) ]
    for word in words do
        let mutable count = 0
        for x in words do
            if x = word then
                count <- count + 1
        let minStr,minCount = FindMin freqCounts
        if count >= minCount then
            freqCounts <- Map.add word count freqCounts
        if Seq.length freqCounts > N then
            freqCounts <- Map.remove minStr freqCounts
    freqCounts 
    |> Seq.sort_by (fun (KeyValue(k,v)) -> -v) 
    |> Seq.iter (printfn "%A")
Main()

Выход:

[the, 75]
[to, 41]
[in, 34]
[a, 32]
[of, 29]
2 голосов
/ 12 апреля 2009

Возможное решение - использовать структуру данных trie для хранения всех слов, связанных с их числом вхождений.

Другие решения могут быть найдены в ответах на этот связанный вопрос: Пространственно-эффективная структура данных для хранения списка слов?

2 голосов
/ 12 апреля 2009

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

2 голосов
/ 12 апреля 2009

Один из способов - сначала отсортировать список.

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

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

1 голос
/ 27 сентября 2013

Вы можете использовать комбинацию внешней сортировки слиянием и очереди приоритетов . Сортировка слиянием обеспечит соблюдение пределов вашей памяти, а очередь с приоритетами будет поддерживать ваши лучшие K запросов. Очевидно, приоритетная очередь должна быть достаточно маленькой, чтобы помещаться в память.

  • Сначала разделите входные строки на чанки, отсортируйте каждый чанк и сохраните его во вторичном хранилище (внешняя сортировка) - O (n log n)
  • Чтение каждого фрагмента и внутри фрагмента, вычисление частоты слов, поэтому в конце этого шага каждый фрагмент сокращается до (уникальный счетчик частоты слов) внутри фрагмента. О (п)
  • Начните читать элементы по частям и агрегируйте для каждого слова. Поскольку куски отсортированы, вы можете сделать это за O (n)
  • Теперь сохраните кучу с минимальным приоритетом (верхняя часть кучи является минимальным элементом в куче) из K элементов. Заполните кучу приоритетов первыми K элементами, а затем для следующих (уникальное слово - конечный счетчик) , если его количество больше верхнего элемента в куче, всплывающее окно и нажмите текущее слово. O (n log k)

Таким образом, ваша окончательная сложность по времени составляет O (n (log k + log n)) -

...