Dies ist vielleicht eine der klassischen Codierungsherausforderungen, die 1986 Anklang fanden, als der Kolumnist Jon Bentley Donald Knuth aufforderte, ein Programm zu schreiben, das k häufigste Wörter in einer Datei findet. Knuth implementierte eine schnelle Lösung mit Hash-Versuchen in einem 8-seitigen Programm, um seine literarische Programmiertechnik zu veranschaulichen. Douglas McIlroy von Bell Labs kritisierte Knuths Lösung als nicht einmal in der Lage, einen vollständigen Text der Bibel zu verarbeiten, und antwortete mit einem Einzeiler, der nicht so schnell sei, aber die Aufgabe erledige:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
1987 wurde ein Folgeartikel mit einer weiteren Lösung veröffentlicht, diesmal von einem Princeton-Professor. Aber es konnte nicht einmal ein Ergebnis für eine einzige Bibel liefern!
Problembeschreibung
Ursprüngliche Problembeschreibung:
Bei einer gegebenen Textdatei und einer Ganzzahl k müssen Sie die k häufigsten Wörter in der Datei (und die Anzahl ihrer Vorkommen) in abnehmender Häufigkeit drucken.
Zusätzliche Problemklärungen:
- Knuth definierte ein Wort als eine Folge lateinischer Buchstaben:
[A-Za-z]+
- Alle anderen Zeichen werden ignoriert
- Groß- und Kleinbuchstaben werden als gleichwertig angesehen (
WoRd
==word
) - Keine Beschränkung der Dateigröße oder der Wortlänge
- Abstände zwischen aufeinanderfolgenden Wörtern können beliebig groß sein
- Das schnellste Programm benötigt am wenigsten CPU-Zeit (Multithreading hilft wahrscheinlich nicht)
Beispiel-Testfälle
Test 1: Ulysses von James Joyce 64-mal verkettet (96 MB-Datei).
- Laden Sie Ulysses vom Projekt Gutenberg herunter :
wget http://www.gutenberg.org/files/4300/4300-0.txt
- Verketten Sie es 64 Mal:
for i in {1..64}; do cat 4300-0.txt >> ulysses64; done
- Das häufigste Wort ist "der" mit 968832 Erscheinungen.
Test 2: Speziell generierter zufälliger Text giganovel
(ca. 1 GB).
- Python 3-Generator-Skript hier .
- Der Text enthält 148391 verschiedene Wörter, die den natürlichen Sprachen ähnlich sind.
- Häufigste Wörter: "e" (11309 Auftritte) und "ihit" (11290 Auftritte).
Allgemeinheitstest: Beliebig große Wörter mit beliebig großen Lücken.
Referenzimplementierungen
Nachdem ich Rosetta Code auf dieses Problem untersucht und festgestellt habe, dass viele Implementierungen unglaublich langsam sind (langsamer als das Shell-Skript!), Habe ich hier einige gute Implementierungen getestet . Nachfolgend finden Sie die Leistung ulysses64
zusammen mit der Zeitkomplexität:
ulysses64 Time complexity
C++ (prefix trie + heap) 4.145 O((N + k) log k)
Python (Counter) 10.547 O(N + k log Q)
AWK + sort 20.606 O(N + Q log Q)
McIlroy (tr + sort + uniq) 43.554 O(N log N)
Kannst du das schlagen?
Testen
Die Leistung wird mit dem 13 "MacBook Pro 2017 mit dem Standard-Unix- time
Befehl (" Benutzer "-Zeit) bewertet . Wenn möglich, verwenden Sie bitte moderne Compiler (z. B. verwenden Sie die neueste Haskell-Version, nicht die ältere).
Bisherige Platzierungen
Timings, einschließlich der Referenzprogramme:
k=10 k=100K
ulysses64 giganovel giganovel
C (trie + bins) by Moogie 0.704 9.568 9.459
C (trie + list) by Moogie 0.767 6.051 82.306
C (trie + sorted list) by Moogie 0.804 7.076 x
Rust (trie) by Anders Kaseorg 0.842 6.932 7.503
J by miles 1.273 22.365 22.637
C# (trie) by recursive 3.722 25.378 24.771
C++ (trie + heap) 4.145 42.631 72.138
APL (Dyalog Unicode) by Adám 7.680 x x
Python (dict) by movatica 9.387 99.118 100.859
Python (Counter) 10.547 102.822 103.930
Ruby (tally) by daniero 15.139 171.095 171.551
AWK + sort 20.606 213.366 222.782
McIlroy (tr + sort + uniq) 43.554 715.602 750.420
Kumulatives Ranking * (%, bestmögliche Punktzahl - 300):
# Program Score Generality
1 Rust (trie) by Anders Kaseorg 334 Yes
2 C (trie + bins) by Moogie 384 x
3 J by miles 852 Yes
4 C# (trie) by recursive 1278 x
5 C (trie + list) by Moogie 1306 x
6 C++ (trie + heap) 2255 x
7 Python (dict) by movatica 4316 Yes
8 Python (Counter) 4583 Yes
9 Ruby (tally) by daniero 7264 Yes
10 AWK + sort 9422 Yes
11 McIlroy (tr + sort + uniq) 28014 Yes
* Summe der Zeitleistung im Verhältnis zu den besten Programmen in jedem der drei Tests.
Bestes Programm: hier .
quelle
Antworten:
[C]
Das Folgende dauert weniger als 1,6 Sekunden für Test 1 auf meinem 2,8-GHz-Xeon W3530. Mit MinGW.org GCC-6.3.0-1 unter Windows 7 erstellt:
Es werden zwei Argumente als Eingabe verwendet (Pfad zur Textdatei und für k Anzahl der am häufigsten aufzulistenden Wörter)
Es wird einfach ein Baum erstellt, der sich aus Buchstaben von Wörtern verzweigt, und an den Blattbuchstaben wird ein Zähler erhöht. Überprüfen Sie dann, ob der aktuelle Blattzähler größer als das kleinste häufigste Wort in der Liste der häufigsten Wörter ist. (Die Listengröße ist die Zahl, die über das Befehlszeilenargument festgelegt wird.) Wenn ja, stufen Sie das Wort, das durch den Blattbuchstaben dargestellt wird, als eines der häufigsten ein. Dies alles wiederholt sich, bis keine Buchstaben mehr eingelesen werden. Danach wird die Liste der häufigsten Wörter über eine ineffiziente iterative Suche nach dem häufigsten Wort aus der Liste der häufigsten Wörter ausgegeben.
Derzeit wird standardmäßig die Verarbeitungszeit ausgegeben. Deaktivieren Sie jedoch aus Gründen der Konsistenz mit anderen Übermittlungen die TIMING-Definition im Quellcode.
Außerdem habe ich dies von einem Arbeitscomputer übermittelt und konnte Test 2-Text nicht herunterladen. Es sollte mit diesem Test 2 ohne Änderung funktionieren, der MAX_LETTER_INSTANCES-Wert muss jedoch möglicherweise erhöht werden.
Für Test 1 und für die 10 häufigsten Wörter sollte bei aktiviertem Timing Folgendes gedruckt werden:
quelle
letters
Arrays, während die Referenzimplementierung Baumknoten dynamisch zuweist .mmap
-ing sollte schneller (~ 5% auf meinem Linux - Laptop):#include<sys/mman.h>
,<sys/stat.h>
,<fcntl.h>
, ersetzt Datei mit dem Lesenint d=open(argv[1],0);struct stat s;fstat(d,&s);dataLength=s.st_size;data=mmap(0,dataLength,1,1,d,0);
und kommentieren Siefree(data);
APL (Dyalog Unicode)
Das Folgende läuft in weniger als 8 Sekunden auf meinem 2.6 Ghz i7-4720HQ mit 64-Bit Dyalog APL 17.0 unter Windows 10:
Zuerst wird nach dem Dateinamen gefragt, dann nach k. Beachten Sie, dass ein erheblicher Teil der Laufzeit (ca. 1 Sekunde) nur das Einlesen der Datei ist.
Um die Zeit zu bestimmen, sollten Sie in der Lage sein, Folgendes in Ihre
dyalog
ausführbare Datei zu leiten (für die zehn häufigsten Wörter):Es sollte drucken:
quelle
export MAXWS=4096M
. Ich denke, es verwendet Hash-Tabellen? Wenn Sie die Größe des Arbeitsbereichs auf 2 GB reduzieren, wird dieser um ganze 2 Sekunden langsamer.∊
verwendet eine Hash - Tabelle nach dieser , und ich bin mir ziemlich sicher , dass⌸
intern auch der Fall ist.WS FULL
, obwohl ich den Arbeitsspeicher auf 6 GB vergrößert habe.Rost
Auf meinem Computer läuft giganovel 100000 etwa 42% schneller (10,64 s gegenüber 18,24 s) als die C-Lösung von Moogie mit dem Präfix "tree + bins". Außerdem gibt es keine vordefinierten Grenzen (im Gegensatz zur C-Lösung, die Grenzen für die Wortlänge, eindeutige Wörter, wiederholte Wörter usw. festlegt).
src/main.rs
Cargo.toml
Verwendung
quelle
-O3
und-Ofast
macht keinen messbaren Unterschied.gcc -O3 -march=native -mtune=native program.c
.[C] Präfixbaum + Bins
HINWEIS: Der verwendete Compiler hat einen erheblichen Einfluss auf die Programmausführungsgeschwindigkeit! Ich habe gcc (MinGW.org GCC-8.2.0-3) 8.2.0 verwendet. Bei Verwendung der Option -Ofast wird das Programm fast 50% schneller ausgeführt als das normalerweise kompilierte Programm.
Komplexität von Algorithmen
Seitdem ist mir klar geworden, dass die Sortierung der Behälter, die ich durchführe, eine Art von Pigeonhost-Sortierung ist. Dies bedeutet, dass ich die Big O-Komplexität dieser Lösung ableiten kann.
Ich rechne damit:
Die Komplexität der Baumkonstruktion entspricht der Baumdurchquerung, da auf jeder Ebene der richtige Knoten, zu dem durchquert werden soll, O (1) ist (da jeder Buchstabe direkt einem Knoten zugeordnet ist und wir für jeden Buchstaben immer nur eine Ebene des Baums durchqueren).
Die Sortierung der Brieftaschen ist O (N + n), wobei n der Bereich der Schlüsselwerte ist. Für dieses Problem müssen jedoch nicht alle Werte sortiert werden, sondern nur die k-Zahl, sodass der schlechteste Fall O (N + k) ist.
Die Kombination ergibt O (1 + N + k).
Die Raumkomplexität für die Baumkonstruktion beruht auf der Tatsache, dass der schlechteste Fall 26 * M Knoten ist, wenn die Daten aus einem Wort mit einer Buchstabenanzahl von M bestehen und jeder Knoten 26 Knoten hat (dh für die Buchstaben des Alphabets). Also ist O (26 * M) = O (M)
Für das Pigeon Hole hat die Sortierung eine Raumkomplexität von O (N + n)
Zusammenfügen ergibt O (26 * M + N + n) = O (M + N + n)
Algorithmus
Es werden zwei Argumente als Eingabe verwendet (Pfad zur Textdatei und für k Anzahl der am häufigsten aufzulistenden Wörter)
Basierend auf meinen anderen Einträgen hat diese Version eine sehr kleine Zeitkostenrampe mit steigenden Werten von k im Vergleich zu meinen anderen Lösungen. Ist aber bei niedrigen Werten von k merklich langsamer, sollte aber bei größeren Werten von k deutlich schneller sein.
Es wird ein Baum erstellt, der sich aus Buchstaben von Wörtern verzweigt, und an den Blattbuchstaben wird ein Zähler erhöht. Fügt das Wort dann zu einer Gruppe von Wörtern derselben Größe hinzu (nachdem das Wort zuerst aus der Gruppe entfernt wurde, in der es sich bereits befunden hat). Dies alles wiederholt sich, bis keine Buchstaben mehr eingelesen werden. Danach werden die Bins k-mal rückwärts iteriert, beginnend mit dem größten Bin, und die Wörter jedes Bins werden ausgegeben.
Derzeit wird standardmäßig die Verarbeitungszeit ausgegeben. Deaktivieren Sie jedoch aus Gründen der Konsistenz mit anderen Übermittlungen die TIMING-Definition im Quellcode.
BEARBEITEN: Verschiebt jetzt das Auffüllen von Behältern, bis der Baum erstellt wurde, und optimiert die Konstruktion der Ausgabe.
EDIT2: Jetzt wird Zeigerarithmetik anstelle von Arrayzugriff zur Geschwindigkeitsoptimierung verwendet.
quelle
ulysses64
einmal sehr schnell lief , ist es jetzt ein aktueller Spitzenreiter.J
Als Skript mit ausführen
jconsole <script> <input> <k>
. Zum Beispiel die Ausgabe vongiganovel
withk=100K
:Es gibt keine Begrenzung außer der Menge des verfügbaren Systemspeichers.
quelle
...
kommt vor, weil die Ausgabe pro Zeile abgeschnitten wurde . Ich habe am Anfang eine Zeile hinzugefügt, um alle Kürzungen zu deaktivieren. Es verlangsamt sich auf giganovel, da es viel mehr Gedächtnis verwendet, da es einzigartigere Wörter gibt.Python 3
Diese Implementierung mit einem einfachen Wörterbuch ist etwas schneller als die mit
Counter
einem auf meinem System.quelle
heapq
fügt es derCounter.most_common
Methode keine Leistung hinzu oder es wirdenumerate(sorted(...))
auchheapq
intern verwendet.Counter.most_common
.[C] Präfixbaum + Sortierte verknüpfte Liste
Es werden zwei Argumente als Eingabe verwendet (Pfad zur Textdatei und für k Anzahl der am häufigsten aufzulistenden Wörter)
Basierend auf meinem anderen Eintrag ist diese Version für größere Werte von k viel schneller, bei niedrigeren Werten von k jedoch mit geringen Leistungskosten.
Es wird ein Baum erstellt, der sich aus Buchstaben von Wörtern verzweigt, und an den Blattbuchstaben wird ein Zähler erhöht. Überprüfen Sie dann, ob der aktuelle Blattzähler größer als das kleinste häufigste Wort in der Liste der häufigsten Wörter ist. (Die Listengröße ist die Zahl, die über das Befehlszeilenargument festgelegt wird.) Wenn ja, stufen Sie das Wort, das durch den Blattbuchstaben dargestellt wird, als eines der häufigsten ein. Wenn bereits ein häufigstes Wort vorhanden ist, tauschen Sie es mit dem nächsthäufigsten aus, wenn die Wortanzahl jetzt höher ist. Auf diese Weise bleibt die Liste sortiert. Dies alles wiederholt sich, bis keine Buchstaben mehr eingelesen werden. Danach wird die Liste der häufigsten Wörter ausgegeben.
Derzeit wird standardmäßig die Verarbeitungszeit ausgegeben. Deaktivieren Sie jedoch aus Gründen der Konsistenz mit anderen Übermittlungen die TIMING-Definition im Quellcode.
quelle
12 eroilk 111 iennoa 10 yttelen 110 engyt
.C #
Dieser sollte mit den neuesten .net SDKs funktionieren .
Hier ist eine Beispielausgabe.
Zuerst habe ich versucht, ein Wörterbuch mit String-Schlüsseln zu verwenden, aber das war viel zu langsam. Ich denke, es liegt daran, dass .net-Zeichenfolgen intern mit einer 2-Byte-Codierung dargestellt werden, was für diese Anwendung eine Verschwendung darstellt. Also habe ich einfach zu reinen Bytes und einer hässlichen goto-artigen Zustandsmaschine gewechselt. Die Konvertierung von Groß- und Kleinschreibung ist ein bitweiser Operator. Die Prüfung des Zeichenbereichs erfolgt in einem einzigen Vergleich nach der Subtraktion. Ich habe mir keine Mühe gegeben, die endgültige Sortierung zu optimieren, da sie weniger als 0,1% der Laufzeit beansprucht.
Fix: Der Algorithmus war im Wesentlichen korrekt, es wurden jedoch zu viele Wörter gemeldet, indem alle Präfixe von Wörtern gezählt wurden. Da die Gesamtwortzahl keine Voraussetzung für das Problem ist, habe ich diese Ausgabe entfernt. Um alle k Wörter auszugeben, habe ich auch die Ausgabe angepasst. Schließlich habe ich mich entschlossen
string.Join()
, die gesamte Liste auf einmal zu verwenden und dann zu schreiben. Überraschenderweise ist dies auf meinem Computer etwa eine Sekunde schneller, als wenn jedes Wort einzeln für 100 KB geschrieben wird.quelle
tolower
und einzelnen Vergleichstricks. Ich verstehe jedoch nicht, warum Ihr Programm mehr eindeutige Wörter als erwartet ausgibt. Außerdem muss das Programm gemäß der ursprünglichen Problembeschreibung alle k Wörter in absteigender Reihenfolge der Häufigkeit ausgeben, sodass ich Ihr Programm nicht für den letzten Test gezählt habe, bei dem 100.000 häufigste Wörter ausgegeben werden müssen.Ruby 2.7.0-preview1 mit
tally
Die neueste Version von Ruby hat eine neue Methode namens
tally
. Aus den Release Notes :Dies löst fast die gesamte Aufgabe für uns. Wir müssen nur zuerst die Datei lesen und später die max finden.
Hier ist das Ganze:
edit: Wurde
k
als Befehlszeilenargument hinzugefügtEs kann mit
ruby k filename.rb input.txt
der Version 2.7.0-preview1 von Ruby ausgeführt werden. Dies kann von verschiedenen Links auf der Seite mit den Versionshinweisen heruntergeladen oder mit rbenv installiert werdenrbenv install 2.7.0-dev
.Beispiellauf auf meinem eigenen kaputten, alten Computer:
quelle