Este es un problema clásico que tuvo cierta resonancia en 1986, cuando Donald Knuth implementó una solución rápida con pruebas hash en un programa de 8 páginas para ilustrar su técnica de programación alfabetizada, mientras que Doug McIlroy, el padrino de las pipas de Unix, respondió con un one-liner, eso no fue tan rápido, pero hizo el trabajo:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Por supuesto, la solución de McIlroy tiene una complejidad de tiempo O (N log N), donde N es un número total de palabras. Hay soluciones mucho más rápidas. Por ejemplo:
Aquí hay una implementación de C ++ con la complejidad de tiempo límite superior O ((N + k) log k), típicamente, casi lineal.
A continuación se muestra una implementación rápida de Python usando diccionarios hash y montón con complejidad de tiempo O (N + k log Q), donde Q es una cantidad de palabras únicas:
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)
Comparación de tiempo de CPU (en 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 es una Biblia concatenada consigo misma 32 veces (135 MB), bible256 - 256 veces respectivamente (1.1 GB).
- La ralentización no lineal de los scripts de Python se debe únicamente al hecho de que procesa los archivos por completo en la memoria, por lo que los gastos generales son cada vez mayores para los archivos de gran tamaño.
- Si hubiera una herramienta Unix que pudiera construir un montón y seleccionar n elementos desde la parte superior del montón, la solución AWK podría lograr una complejidad temporal casi lineal, mientras que actualmente es O (N + Q log Q).