WordCount: насколько неэффективно решение McIlroy?
Короче говоря: в 1986 году интервьюер попросил Дональда Кнута написать программу, в которой вводятся текст и число N, а также перечисляются N наиболее часто используемых слов, отсортированных по частоте. Кнут создал 10-страничную программу Pascal, на которую Дуглас Макилрой ответил следующим 6-строчным сценарием оболочки:
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
Прочитайте полную историю в http://www.leancrew.com/all-this/2011/12/more-shell-less-egg/.
Конечно, у них были совершенно разные цели: Кнут демонстрировал свои концепции грамотного программирования и строил все с нуля, в то время как Макилрой использовал несколько распространенных утилит UNIX для получения кратчайшего исходного кода.
Мой вопрос: насколько это плохо?
(Чисто с точки зрения скорости выполнения, так как я уверен, что все мы согласны с тем, что 6 строк кода легче понять / поддерживать, чем 10 страниц, грамотное программирование или нет.)
я могу понять, что sort -rn | sed ${1}q
может быть не самый эффективный способ извлечь общие слова, но что не так с tr -sc A-za-z '\n' | tr A-Z a-z
? Это выглядит довольно хорошо для меня. Около sort | uniq -c
Это ужасно медленный способ определения частот?
Несколько соображений:
tr
должно быть линейное время (?)sort
Я не уверен, но я предполагаю, что это не так уж плохоuniq
должно быть линейное время тоже- нерестовые процессы должны быть линейными по времени (по количеству процессов)
1 ответ
Unix
Скрипт имеет несколько линейных операций и 2 вида. Это будет порядок расчета O(n log(n))
,
Алгоритм Кнута для взятия только верхних N: http://en.wikipedia.org/wiki/Selection_algorithm Где вы можете иметь несколько вариантов во временной и пространственной сложности алгоритма, но теоретически они могут быть быстрее для некоторых типичных примеров с большое количество (разных) слов.
Так что Кнут может быть быстрее. Конечно, потому что английский словарь имеет ограниченный размер. Это может превратить log(n)
в некоторой большой константе, хотя, возможно, потребляет много памяти.
Но, возможно, этот вопрос лучше подходит для https://cstheory.stackexchange.com/
Решение Дуга Макилроя имеет временную сложность O(T log T), где T - общее количество слов. Это связано с первымsort
.
Для сравнения, вот четыре более быстрых решения той же проблемы:
Вот реализация на C++ с верхней границей временной сложности O((T + N) log N), но практически - почти линейной, близкой к O(T + N log N).
Ниже представлена быстрая реализация Python. Внутри он использует хэш-словарь и кучу с временной сложностью O(T + N log Q), где Q - количество уникальных слов:
import collections, re, sys
filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv)>2 else 10
reg = re.compile('[a-z]+')
counts = collections.Counter()
for line in open(filename):
counts.update(reg.findall(line.lower()))
for i, w in counts.most_common(k):
print(i, w)
И еще одно решение оболочки Unix с использованием AWK. Он имеет временную сложность O(T + Q log Q):
awk -v FS="[^a-zA-Z]+" '
{
for (i=1; i<=NF; i++)
freq[tolower($i)]++;
}
END {
for (word in freq)
print(freq[word] " " word)
}
' | sort -rn | head -10
Вот это очень быстро раствор в Rust Андерс Kaseorg.
Сравнение времени процессора (в секундах):
bible32 bible256 Asymptotical
Rust (prefix tree) 0.632 5.284 O(?)
C++ (prefix tree + heap) 4.838 38.587 O((T + N) log N)
Python (Counter) 14.366 115.855 O(T + N log Q)
AWK + sort 21.548 176.411 O(T + Q log Q)
McIlroy (tr + sort + uniq) 60.531 690.906 O(T log T)
Заметки:
- T> = Q, обычно Q >> N (N - небольшая константа)
- bible32 состоит из Библии 32 раза (135 МБ), bible256 - 256 раз соответственно (1,1 ГБ)
- Время AWK может сильно различаться в зависимости от того, какую версию AWK вы используете (gawk, nawk или mawk).
Как видите, решение Макилроя работает примерно в 100 раз медленнее, чем самая быстрая из известных программ! Однако его решение по-прежнему очень элегантно, легко отлаживается и, в конце концов, не так уж и ужасно по производительности, если только вы не начнете использовать его для гигабайтных файлов. Плохая реализация более сложных алгоритмов на C/C++ или Haskell могла легко работать намного медленнее, чем его конвейер (я был свидетелем этого).