“…einen habe ich noch!”
Otto Waalkes
Eine angenehme Seite des Programmieren ist, wenn es läuft, dann läuft es. Unter Software Entwicklern wird dieser Zustand gerne “The Flow” genannt. Es sind die wenigen wirklich produktiven Stunden des Sprints, die das Projekt voranbringen. Besonders schade, wenn sie von Vorgesetzten oder Ehefrauen unterbrochen werden. Kein Bug-Report oder Einkaufszettel kann so wichtig sein, dass dadurch dieser kreative Zustand gestört wird.
In den ersten drei Beiträgen dieser Reihe wurde das Thema Löschen aus einem Double Array Trie immer wieder umgangen. Eine kleine Sünde, die in diesem Beitrag nun endlich ihr Ende findet. Außerdem wird die Einsatzmöglichkeit des Trie erweitert, in dem eine Map Implementierung basierend auf dem Double Array Trie Algorithmus vorgestellt wird.
Das Löschen aus einem Trie
Das Löschen eines Wortes aus einem Double Array Trie scheint mit vielen Mühen behaftet zu sein. Aus diesem Grund blieb diese Funktionalität lange nicht umgesetzt. Ein Blick auf die Abbildung eines DAT zeigt jedoch, wie einfach die Implementierung wirklich ist.
Um aus dem obigen Double Array Trie das Wort jar
zu löschen, muss nur das entsprechende letzte Element für diesen Eintrag in der Liste gefunden werden und dann der Verweis auf seine Basis gelöscht werden. Da in diesem Beispiel die Basis 1 ist, sind wir schon fertig mit dem Löschen.
Etwas komplizierter ist das Löschen des Wortes baby
, da wir nicht direkt aus dem roten Basis Knoten löschen, sondern zuerst das blaue Element an Position 4 löschen. Beim Löschen aus einem anderen als den Basis-Knoten muss geprüft werden, ob er noch weitere Elemente enthält. In diesem Fall die Elemente an Position 5 und 6. Enthält er weitere Einträge, dann muss er nicht gelöscht werden und der Löschvorgang ist beendet. Enthält er keine weiteren Elemente, dann muss er aus seinem Vorgänger gelöscht werden. Dieser Prozess endet, wenn der Basis-Knoten erreicht wird.
Eine Map implementieren
Die Map Implementierung lebt von einem kleinen Trick bei der Implementierung des Double Array Trie. Die gemeinsamen Präfixe von Wörtern sind in den Knoten des Double Array Trie kodiert, der Rest eines Wortes ist als String gespeichert. In der Original-Implementierung gab es dafür eine eigene, als Tail bezeichnete Speicherstruktur, in dieser Implementierung existiert in den einzelnen Node
Objekten ein Attribut tail
.
Es vereinfacht zwar die Implementierung, verursacht aber einen unnötig großen Speicherverbrauch, da auch alle inneren Knoten dieses Attribut mit sich tragen. Eine Unterscheidung zwischen InnerNode
und TailNode
könnte in Zukunft für geringeren Speicherverbrauch sorgen.
Versehen wir nun jedes Node
Objekt neben dem tail
Attribut einem value
Attribut, dann haben wir aus unserem Double Array Trie, das Grundgerüst für eine Double Array Map geschaffen.
Das Auslesen eines Wertes aus der Map ist nichts weiter, als das Finden des letzen Node
des Schlüssels. Existiert so ein Node
, dann steht in seinem value
Attribute der zu diesem Schlüssel gespeicherte Wert. Eingefügt wird ein Wert, in dem der entsprechende Schlüssel eingefügt wird und in den letzten Node
neben dem Suffix in tail
auch der Wert in value
gespeichert wird.
Interessante Methoden einer Map sind keySet
, entrySet
und values
, die verschiedene Sichten auf die Daten in der Double Array Map liefern. Erstaunlich einfach sind keySet
und entrySet
zu implementieren. Das Ergebnis von keySet
ist ein Set
der Wörter in der internen Struktur der Map. Im vorherigen Beitrag wurden Tries als Set<String>
implementiert. Die naheliegende Implementierung für die keySet
Methode ist ein spezieller Trie, der die internen Strukturen der Double Array Map nutzt.
@Override public Set<String> keySet() { return Tries.unmodifiable(new KeySet<>(da, alphabet, size)); } static class KeySet<V> extends AbstractDoubleArrayTrie<EntryNode<V>> { KeySet(DoubleArray<EntryNode<V>> da, Alphabet alphabet, int size) { super(da, alphabet, size); } }
Die Methode entrySet
liefert ein Set
zurück, das einen speziellen Iterator
über den Trie aus dem keySet
verwendet, um seine Einträge zu iterieren.
Für die Implementierung der Collection
aus der values
Methode existieren zwei Varianten. Die einfache Version läuft über die interne Liste der Map und liefert alle Werte aus Node
Elementen, deren tail
Attribute gesetzt ist. Die andere Version verwendet arbeitet ähnlich der entrySet
Methode, aber liefert nur den Wert aus dem value
Attribut.
Beispielhaftes mit der Double Array Map
Zur Demonstration der Möglichkeiten der Double Array Map liest der folgende Code die Wörter von Kafkas – Die Verwandlung ein und zählt ihre Häufigkeit.
final Alphabet alphabet = Alphabet.with().letters().letters("äöüßÄÖÜ-").build(GERMANY); Map<String, AtomicInteger> map = new DoubleArrayMap<>(alphabet); try (Scanner s = new Scanner(getClass().getResourceAsStream("/dieverwandlung.txt"))) { while (s.hasNext()) { final String word = s.next().replaceAll("[.,;:!?»«]", ""); if (word.matches("^[a-zA-ZaäöüßÄÖÜ-]+$")) { map.computeIfAbsent(word, k -> new AtomicInteger()).getAndIncrement(); } } }
String result = map.entrySet().stream() .sorted(reverseOrder(comparingInt(a -> a.getValue().get()))) .map(e -> e.getKey() + "=" + e.getValue()) .collect(joining(", ")); System.out.println(result);
Die Ausgabe beginnt mit den folgenden Einträgen.
die=585, und=585, er=405, der=403, zu=347, sich=311, in=297, nicht=259, sie=250, den=243, war=234, Gregor=218, mit=188, das=179, auf=175, es=172, dem=164, the=160, hatte=155, aber=136, ein=136, daß=133, als=122, auch=110, of=108, Schwester=107, an=104, nur=95, von=93, wie=93, --=92, ihn=91, Mutter=91, so=91, Vater=91, schon=89, um=88, im=87, des=81, noch=80, ihm=79, address=1, addresses=1, against=1, ...
Die folgenden Zeilen geben alle Wörter die mit da
beginnen und ihre Häufigkeit aus.
String result = ((Trie) map.keySet()).stream("da") .map(k -> k + "=" + map.get(k)) .collect(joining(", ")); System.out.println(result);
Die Ausgabe beginnt mit da=47
und endet mit dazwischen=2
.
da=47, dabei=3, dachte=20, dachten=1, dadurch=7, dafür=2, dagegen=8, daher=4, dahin=1, dahintorkelte=1, dalag=1, damage=1, damaged=1, damages=2, damals=2, damit=7, dampfte=1, danach=1, dankbar=1, dankbaren=1, danke=1, danken=2, dankte=1, dann=47, dar=2, daran=20, daranzusetzen=1, darauf=9, daraus=2, darf=2, darin=1, darnach=2, darstellte=1, darum=2, darunter=1, darüber=12, das=179, dasaß=1, dastand=2, daß=133, data=1, date=2, dauern=1, dauerte=1, davon=6, davonzulaufen=1, days=4, dazu=5, dazwischen=2
Fazit
Obwohl die Double Array Map
schon einsatzbereit ist, sind noch diverse Methoden einer Map nicht unterstützt und einige Methoden liefern bislang nicht veränderbare Instanzen zurück. Auch muss über den Speicherverbrauch der Trie und Map Implementierungen nachgedacht werden. Zwei ungenutzte Attribute an jedem internen Knoten ist pure Verschwendung.
Wer mehr über die Implementierung des Doube Array Trie und der Double Array Map erfahren möchte, die Sourcen sind wieder auf GitLab zu finden und eine erste Version liegt zum Download auf JFrog Bintray bereit.
Nachtrag
Im Zuge der Ankündigung von JFrog, ihr Repository JCenter zu schließen, ist die Bibliothek als Java 11 Binary nach Maven Central migriert.
<dependency> <groupId>de.schegge</groupId> <artifactId>double-array-trie</artifactId> <version>0.1.2</version> </dependency>