Questo è un classico problema che risuonò nel 1986, quando Donald Knuth implementò una soluzione rapida con tentativi di hash in un programma di 8 pagine per illustrare la sua tecnica di programmazione letteraria, mentre Doug McIlroy, il padrino delle pipe Unix, rispose con un one-liner, non è stato così veloce, ma ha fatto il lavoro:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Naturalmente, la soluzione di McIlroy ha una complessità temporale O (N log N), dove N è un numero totale di parole. Ci sono soluzioni molto più veloci. Per esempio:
Ecco un'implementazione C ++ con la complessità del tempo limite superiore O ((N + k) log k), in genere - quasi lineare.
Di seguito è una veloce implementazione di Python che utilizza dizionari hash e heap con complessità temporale O (N + k log Q), dove Q è un numero di parole uniche:
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)
Ecco una soluzione estremamente veloce in Rust di Anders Kaseorg.
Confronto del tempo della CPU (in secondi):
bible32 bible256
Rust (prefix tree) 0.632 5.284
C++ (prefix tree + heap) 4.838 38.587
Python (Counter) 9.851 100.487
Sheharyar (AWK + sort) 30.071 251.301
McIlroy (tr + sort + uniq) 60.251 690.906
Gli appunti:
- bible32 è la Bibbia concatenata con se stessa 32 volte (135 MB), bibbia256 - 256 volte rispettivamente (1,1 GB).
- Il rallentamento non lineare degli script Python è causato esclusivamente dal fatto che elabora i file completamente in memoria, quindi le spese generali stanno diventando più grandi per i file di grandi dimensioni.
- Se esistesse uno strumento Unix in grado di costruire un heap e selezionare n elementi dall'alto dell'heap, la soluzione AWK potrebbe raggiungere una complessità temporale quasi lineare, mentre attualmente è O (N + Q log Q).