Esse é um problema clássico que teve alguma ressonância em 1986, quando Donald Knuth implementou uma solução rápida com tentativas de hash em um programa de 8 páginas para ilustrar sua técnica de programação, enquanto Doug McIlroy, o padrinho dos pipes Unix, respondeu com uma one-liner, que não foi tão rápido, mas fez o trabalho:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Obviamente, a solução da McIlroy possui complexidade de tempo O (N log N), onde N é um número total de palavras. Existem soluções muito mais rápidas. Por exemplo:
Aqui está uma implementação C ++ com a complexidade de tempo limite superior O ((N + k) log k), normalmente - quase linear.
Abaixo está uma implementação rápida do Python usando dicionários de hash e heap com complexidade de tempo O (N + k log Q), onde Q é um número de palavras exclusivas:
import collections, re, sys
filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv)>2 else 10
text = open(filename).read()
counts = collections.Counter(re.findall('[a-z]+', text.lower()))
for i, w in counts.most_common(k):
print(i, w)
Comparação de tempo de CPU (em segundos):
bible32 bible256
C++ (prefix tree + heap) 5.659 44.730
Python (Counter) 10.314 100.487
Sheharyar (AWK + sort) 30.864 251.301
McIlroy (tr + sort + uniq) 60.531 690.906
Notas:
- bible32 é a Bíblia concatenada consigo mesma 32 vezes (135 MB), bible256 - 256 vezes respectivamente (1,1 GB).
- A desaceleração não linear dos scripts Python é causada puramente pelo fato de processar arquivos completamente na memória, de modo que as despesas gerais estão aumentando para arquivos enormes.
- Se houvesse uma ferramenta Unix que pudesse construir uma pilha e selecionar n elementos da parte superior da pilha, a solução AWK poderia atingir uma complexidade de tempo quase linear, enquanto atualmente ela é O (N + Q log Q).