C’est un problème classique qui a trouvé un écho en 1986, lorsque Donald Knuth a implémenté une solution rapide avec essais de hachage dans un programme de 8 pages pour illustrer sa technique de programmation, alors que Doug McIlroy, parrain des tubes Unix, répondait par un one-line, ce n’était pas aussi rapide, mais le travail était fait:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Bien entendu, la solution de McIlroy a une complexité temporelle O (N log N), où N est un nombre total de mots. Il existe des solutions beaucoup plus rapides. Par exemple:
Voici une implémentation C ++ avec la complexité temporelle supérieure O ((N + k) log k), généralement - presque linéaire.
Ci-dessous, une implémentation rapide de Python utilisant des dictionnaires de hachage et des tas avec une complexité temporelle O (N + k log Q), où Q est un nombre de mots uniques:
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)
Voici une solution extrêmement rapide dans Rust par Anders Kaseorg.
Comparaison du temps de calcul (en secondes):
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
Remarques:
- bible32 est concaténé par la Bible 32 fois (135 Mo), bible256 - 256 fois (1,1 Go).
- Le ralentissement non linéaire des scripts Python est uniquement dû au fait qu'il traite les fichiers complètement en mémoire, de sorte que les frais généraux augmentent pour les fichiers volumineux.
- S'il existait un outil Unix capable de construire un segment de mémoire et de sélectionner n éléments dans la partie supérieure du segment, la solution AWK pourrait atteindre une complexité temporelle quasi linéaire, alors qu'actuellement il s'agit de O (N + Q log Q).