Symboltabellen

Werbung
Diskrete Modellierung
Wintersemester 2016/17
Martin Mundhenk
Uni Jena, Institut für Informatik
12. Februar 2017
4.4 Symboltabellen
Eine Symboltabelle assoziiert Werte mit Schlüsselwörtern.
§
Ein Adressbuch, in dem Namen (Schlüsselwörter) Adressen,
Telefonnummern und Geburtstage (Werte) zugeordnet werden, ist
ein Anwendungsbeispiel dafür.
§
Eine Konkordanz, die zu einem Buch die Liste der vorkommenden
Wörter mit den Seitennummern enthält, auf denen sie vorkommen,
ist ein weiteres Beispiel.
§
Eine Suchmaschine im Web benutzt letztlich eine Konkordanz für
Internetseiten.
Symboltabellen sind von grundlegender Bedeutung beim Programmieren.
Wir werden uns ansehen, wie man sie implementieren kann.
In Python gibt es mit dem Datentyp dict (dictionary) eine ausgefeilte
Implementierung.
Eine API für SymbolTable
Für Symboltabellen braucht man Operationen zum Eintragen von
Schlüssel-Wert-Paaren, zum Abfragen des Wertes, der zu einem Schlüssel
gehört, und zum Iterieren über alle Schlüssel.
Operation
SymbolTable()
Beschreibung
eine neue (leere) Symboltabelle
st[key] = val
trage zum Schlüssel key den Wert val in die Symboltabelle st ein
st[key]
der Wert von key in der Symboltabelle st
key in st
gibt True zurück, falls st einen Wert mit Schlüssel key
enthält
for key in st:
iteriere über alle Schlüssel in der Symboltabelle st
4.4.2
csvToSt.py – Nachschlagen in der SymbolTable
Die folgende Funktion csvToSt
liest eine csv-Datei und
trägt aus jeder Zeile zwei durch ihre Indizes vorgegebene Einträge
als Schlüssel/Wert-Paar in eine Symbol-Tabelle ein.
Die erstellte Symboltabelle wir als Ergebnis zurückgegeben.
Der Test-Client liest einen Dateinamen und zwei Indizes von der
Kommandozeile.
Damit wird die Funktion csvToSt aufgerufen.
Anschließend werden strings von stdio eingelesen.
Jeder String wird als Schlüssel aufgefasst und der zugeordnete Wert in
der Symbol-Tabelle wird ausgegeben.
def csvToSt(dateiname,schluesselIndex,werteIndex):
eingabe = InStream(dateiname).readAllLines()
st = SymbolTable()
for zeile in eingabe:
eintraege = zeile.split(',')
schluessel = eintraege[schluesselIndex]
wert = eintraege[werteIndex]
st[schluessel] = wert
return st
#-----------------------------------------------------------------def test():
eingabedatei = sys.argv[1]
schluesselIndex = int(sys.argv[2])
werteIndex = int(sys.argv[3])
st = csvToSt(eingabedatei, schluesselIndex, werteIndex)
text = ""
stdio.write("Eingabe: ")
while not stdio.isEmpty():
m = stdio.readString()
if m in st:
text += st[m] + " "
stdio.writeln("Ausgabe: " + text)
#-----------------------------------------------------------------if __name__=='__main__': test()
4.4.4
Datei
morsecode.csv:
A,.B,-...
C,-.-.
D,-..
E,.
F,..-.
G,--.
H,....
I,..
J,.--K,-.L,.-..
M,-N,-.
O,--P,.--.
Q,--..
.
.
7,--...
8,---..
9,----.
Wir benutzen csvToSt.py, um Zeichenfolgen in
Morse-Code zu übersetzen und umgekehrt.
$ python csvToSt.py morsecode.csv 0 1
Eingabe: P Y T H O N
Ausgabe: .--. -.-- - .... --- -.
$ python csvToSt.py morsecode.csv 1 0
Eingabe: .--. -.-- - .... --- -.
Ausgabe: P Y T H O N
index.py – Erstellung eines Index
Das Programm index.py liest eine Text-Datei ein
und berechnet eine Index der Wörter in der Datei.
D.h. für jedes Wort wird eine Liste geführt, an welcher Stelle im Text es
vorkommt.
Bei Büchern steht im Index, auf welchen Seiten welches Wort vorkommt.
Wir fassen hier den Text als Array von Strings auf, die durch Leerzeichen
getrennt sind.
Der Index enthält dann für jeden String alle Indizes, unter denen er im
Array vorkommt.
Wir benutzen als Datenstruktur OrderedSymbolTable. Die Iteration
darüber wird in der Ordnung der Schlüssel durchgeführt.
4.4.6
import sys, stdio
reload(sys)
sys.setdefaultencoding('utf-8')
from instream import InStream
from bst import OrderedSymbolTable
def indiziere(dateiname,minLaenge=5):
eingabewoerter = InStream(dateiname).readAllStrings()
index = OrderedSymbolTable()
for i in range(len(eingabewoerter)):
w = eingabewoerter[i]
if len(w)>=minLaenge:
if w not in index: index[w] = []
index[w] += [i]
return index
def indexausgabe(index,minVorkommen=10):
for wort in index:
if len(index[wort]) >= minVorkommen:
stdio.writeln(wort + ": " + str(index[wort]))
#-----------------------------------------------------------------def test():
index = indiziere(sys.argv[1],int(sys.argv[2]))
indexausgabe(index,int(sys.argv[3]))
#-----------------------------------------------------------------if __name__=='__main__': test()
4.4.7
Datei Grundgesetz.txt:
...
Die Grundrechte
Art 1
(1) Die Würde des Menschen ist unantastbar. Sie zu achten und zu schützen ist Verpflichtung aller
Gewalt.
(2) Das Deutsche Volk bekennt sich darum zu unverletzlichen und unveräußerlichen Menschenrechten a
Grundlage jeder menschlichen Gemeinschaft, des Friedens und der Gerechtigkeit in der Welt.
(3) Die nachfolgenden Grundrechte binden Gesetzgebung, vollziehende Gewalt und Rechtsprechung als
unmittelbar geltendes Recht.
Art 2
Ein Service des Bundesministeriums der Justiz und für Verbraucherschutz
in Zusammenarbeit mit der juris GmbH - www.juris.de
- Seite
2 von 49 ...
$ python index.py Grundgesetz.txt 5 5
(Artikel: [2704, 2716, 2721, 2756, 2763, 2777, 2783, 2789, 2793, 2800, 2804, 2809]
Absatz: [1407, 1579, 1596, 1639, 1683, 1959, 1969, 1976, 2303]
Artikel: [29, 151, 1043, 1048, 2214, 2955]
Bundesministeriums: [305, 729, 1240, 1840, 2378, 2906]
Deutschen: [198, 923, 960, 1150, 1262]
Entschädigung: [2145, 2148, 2166, 2198, 2212]
Freiheit: [226, 364, 454, 461, 526, 576, 1372, 1658, 2771]
Gefahr: [1199, 1703, 1798, 1822, 1864, 1940, 2016]
Gesetz: [388, 948, 1119, 1162, 1277, 1417, 1546, 1608, 1670, 1735, 2133, 2269, 2532, 2842, 2853, 2
Gesetzes: [32, 377, 637, 953, 1093, 1167, 1282, 1422, 1551, 1613, 1675, 2026, 2138, 2242, 2847]
Grund: [130, 375, 635, 951, 1091, 1165, 1280, 1420, 1549, 1611, 1673, 1750, 1835, 2024, 2136, 2179
4.4.8
Implementierung von SymbolTable (1)
Sehr einfache Idee:
Nimm zwei Arrays keys und values.
Zum Eintragen eines neuen Schlüssel/Wert-Paares s und v
wird keys += [s] und values += [v] ausgeführt.
Beim Suchen nach dem Wert, der einem Schlüssel zugeordnet ist,
wird das keys-Array nach dem Schlüssel durchsucht.
Der Index i des Schlüssels in keys liefert dann den Wert unter
values[i].
Grober Nachteil:
zur Suche nach dem Wert für einen Schlüssel muss das
ganze keys-Array durchsucht werden: das hat lineare Rechenzeit . . .
4.4.9
class SymbolTable:
def __init__(self):
self._keys = []
self._vals = []
def __getitem__(self,key):
for i in range(len(self._keys)):
if self._keys[i] == key: return self._vals[i]
return None
def __setitem__(self,key,value):
for i in range(len(self._keys)):
if self._keys[i] == key:
self._vals[i] = value
return
self._keys += [key]
self._vals += [value]
4.4.10
Implementierung von SymbolTable (2)
Die Funktion hash liefert zu jedem string einen Zahlenwert.
Man benutzt diesen Zahlenwert als Index des Schlüssels.
Der Schlüssel wird dann im Array keys unter dem Index gespeichert,
und der zugehörige Wert im Array values unter dem gleichen Index.
Damit die Indizes in einem vernünftigen“ Bereich bleiben,
”
nimmt man die Hashwerte modulo einem vorgegebenen Wert m.
Jeder Eintrag in keys und values ist ein Array.
Wird zwei verschiedenen Schlüsseln der gleiche Index zugeordnet
(Kollision), dann wird das neue Schlüssel/Wert-Paar an diese Arrays
angehängt (wie unter (1)).
Die Hoffnung“ ist, dass es nur wenige Kollisionen gibt.
”
4.4.11
class SymbolTable:
#-- Construct a new SymbolTable object. -------------------def __init__(self, m=1024):
self._m = m
self._keys = stdarray.create2D(m, 0)
self._vals = stdarray.create2D(m, 0)
#-- Return the value associated with key in self. ---------def __getitem__(self, key):
i = hash(key) % self._m
for j in range(len(self._keys[i])):
if self._keys[i][j] == key:
return self._vals[i][j]
raise KeyError
#-- Associate key with val in self.
-----------------------
def __setitem__(self, key, val):
i = hash(key) % self._m
for j in range(len(self._keys[i])):
if self._keys[i][j] == key:
self._vals[i][j] = val
return
self._keys[i] += [key]
self._vals[i] += [val]
#-- Return True if key is in self, and False otherwise. -------------------def __contains__(self, key):
i = hash(key) % self._m
for j in range(len(self._keys[i])):
if self._keys[i][j] == key:
return True
return False
#-- Return an iterator for self.
------------------------------------------
def __iter__(self):
a = []
for i in range(self._m):
a += self._keys[i]
return iter(a)
4.4.13
Implementierung von SymbolTable (3)
als binärer Baum
Binäre Bäume sind Strukturen aus Knoten,
die jeweils maximal 2 Nachfolger haben.
Der linke binäre Baum ist vollständig.
Die Tiefe der beiden Bäume ist 4 bzw. 5.
4.4.14
Struktur eines binären Baums
Einen Knoten, der kein Nachfolger eines anderen Knotens ist,
heißt Wurzel.
Der linke Nachfolger der Wurzel ist Wurzel des linken Teilbaums,
und der rechte Nachfolger ist Wurzel des rechten Teilbaums.
1
2
3
4
Teilbaum Bl
Teilbaum Br
4.4.15
Binäre Suchbäume
Ein binärer Suchbaum ist ein binärer Baum, in dem jeder Knoten einen
Schlüssel enthält.
Die Schlüssel haben folgende Eigenschaft:
In jedem Teilbaum gilt für den Schlüssel k der Wurzel:
1. alle Schlüssel im rechten Teilbaum sind ą k und
2. alle Schlüssel im linken Teilbaum sind ă k.
20
18
32
11
35
10
5
3
20
12
8
11
3
16
19
16
42
32
40
8
45
5
12
10
42
35
18
19
45
40
4.4.16
Suchen im binären Suchbaum
Zur Suche eines Schlüssels s vergleicht man ihn mit dem Schlüssel der
Wurzel und wiederholt die Suche entsprechend im linken oder rechten
Teilbaum.
Bsp.: Suche nach Schlüssel 18.
T
T
22
32
11
3
32
3
16
16
18
12
22
11
18
12
20
20
22
T
11
3
32
32
11
3
16
12
22
18
16
12
20
T
18
20
4.4.17
Erfolglose Suche
Sucht man nach einem Schlüssel, der nicht im Suchbaum ist,
dann stößt man auf eine Lücke“.
”
Beispiel: Suche nach 34.
20
32
11
3
16
8
5
12
10
42
35
18
19
45
40
4.4.18
Einfügen eines neuen Schlüssels
Zum Einfügen eines neuen Schlüssels sucht man die Stelle, an der man
ihn finden müsste, und hängt dort einen neuen Knoten an.
Bsp.: füge Knoten mit Schlüssel 13 ein.
v
k
k
22
32
11
3
32
11
3
16
20
3
11
k
16
12
16
18
12
20
22
32
32
v
12
20
22
11
3
18
32
20
22
v
11
18
12
22
k
3
16
18
12
v
22
k
18
32
11
3
16
v
16
18
12
20
13
20
4.4.19
Anzahl der Vergleiche beim Suchen/Einfügen
Ein binärer Suchbaum mit n Knoten hat im günstigen Fall Tiefe log n
und im ungünstigen Fall Tiefe n.
3
12
16
18
22
12
3
28
16
22
28
18
Die Anzahl der Vergleiche (« Rechenzeit) beim Suchen entspricht der
Tiefe.
4.4.20
Suchbäume als Symboltabellen
Man speichert in jedem Knoten“ zusätzliche zum Schlüssel noch den
”
zugehörigen Wert.
Suche und Einfügen geht wie bei binären Suchbäumen.
Damit Suchen und Einfügen möglichst schnell geht,
gibt es Strategien, den Baum so zu balancieren,
dass seine Tiefe bei n Knoten möglichst wenig von log n abweicht.
AVL-Bäume haben Tiefe ď 1.44 ¨ log n ` 1.
Binäre Suchbäume haben bei zufällig gewählten aufeinanderfolgend
eingefügten Schlüsseln erwartete Tiefe der Größenordnung log n.
Allerdings ist der konstante Faktor etwas größer als bei AVL-Bäumen.
Vergleich von Implementierungen
Wir vergleichen folgende Implementierungen:
§ einfachst.py – einfachste Implementierung in dieser VL
§ hashst.py – Implementierung als Hashtabelle (diese VL)
§ bst buch – Impl. mit binärem Suchbaum (aus dem Buch zur VL)
§ bst mit – Impl. mit binärem Suchbaum (MIT OpenCourseWare)
§ avl mit – Impl. mit AVL-Baum (MIT OpenCourseWare)
4.4.22
Versuchsaufbau
Wir machen zwei Versuche.
1. Aufbau einer Symboltabelle
§
§
mit Einträgen, die aufsteigend sortiert sind
mit zufällig gewählten Einträgen
Hier machen wir den Verdopplungstest“ – also Testläufe mit jeweils
”
verdoppelter Anzahl der Einträge.
Der Test wird beendet, wenn mehr als 20 sec zum Aufbau der
Symboltabelle benötigt wurden.
2. Anfragen an eine Symboltabelle
mit zufällig gewählten Einträgen.
Hier kann man beim Verdopplungstest stets doppelte Rechenzeit
erwarten.
4.4.23
Ergebnisse für einfachst
Zuerst betrachten wir den Aufbau der Symboltabelle.
n ist die Größe der Symboltabelle.
Die Rechenzeit wird in Sekunden angegeben.
Hier ist es egal, ob die Einträge für Schlüssel in sortierter oder zufälliger
Reihenfolge gemacht werden.
n=
n=
n=
n=
4000,
8000,
16000,
32000,
Faktor=3.77,
Faktor=3.87,
Faktor=4.24,
Faktor=4.18,
Rechenzeit= 0.26
Rechenzeit= 1.02
Rechenzeit= 4.30
Rechenzeit= 17.97
Das Ergebnis zeigt, was zu erwarten war: Rechenzeit „ n2 .
Für 32000 Abfragen aus einer ST mit 32000 Einträgen werden 47 sec benötigt.
Ergebnisse für hashst
Zuerst betrachten wir den Aufbau der Symboltabelle.
Die Größe der Hashtabelle wird am Anfang des Aufbaus festgelegt.
Ist sie unabhängig von der Anzahl der Einträge, dann ist die Rechenzeit „ n2 .
Wählt man die Größe abhängig von der Anzahl der Einträge, wird die
Rechenzeit besser.
In den Experimenten haben wir bei n Einträgen die Größe auf n/8 gesetzt.
Es ist wieder egal, ob die Einträge für Schlüssel in sortierter oder zufälliger
Reihenfolge gemacht werden.
n=
n=
n=
n=
n=
n=
n=
128000,
256000,
512000,
1024000,
2048000,
4096000,
8192000,
Faktor=1.99,
Faktor=1.98,
Faktor=1.99,
Faktor=2.00,
Faktor=2.05,
Faktor=2.05,
Faktor=2.01,
Rechenzeit= 0.26
Rechenzeit= 0.51
Rechenzeit= 1.02
Rechenzeit= 2.05
Rechenzeit= 4.19
Rechenzeit= 8.57
Rechenzeit= 17.26
Das Ergebnis zeigt, dass die Rechenzeit „ n ist.
512000 Abfragen aus einer ST mit 128000 Einträgen dauern 0.58 sec.
Ergebnisse für bst buch
Da das Eintragen in einen binären Suchbaum komplett rekursiv implementiert
ist, scheitert der Aufbau bei Eingabe einer sortierten Folge von Schlüssel
bereits bei 1600 Einträgen.
Bei zufällig gewählten Eingaben kann man große Symboltabellen aufbauen.
n=
32000, Faktor=2.06, Rechenzeit= 0.17
n=
64000, Faktor=2.26, Rechenzeit= 0.37
n= 128000, Faktor=2.85, Rechenzeit= 1.06
n= 256000, Faktor=2.02, Rechenzeit= 2.14
n= 512000, Faktor=2.26, Rechenzeit= 4.83
n= 1024000, Faktor=2.22, Rechenzeit= 10.74
n= 2048000, Faktor=2.30, Rechenzeit= 24.65
n= 4096000, Faktor=2.13, Rechenzeit= 52.55
Das Ergebnis lässt erahnen, dass die Rechenzeit „ n ¨ log n ist.
512000 Abfragen aus einer ST mit 128000 Einträgen dauern 3.25 sec.
4.4.26
Ergebnisse für bst mit
Hier wurde das Eintragen in einen binären Suchbaum nicht rekursiv
implementiert ist. Das Experiment für die Eingabe einer sortierten Folge von
Schlüsseln lässt erahnen, dass die Rechenzeit „ n2 ist.
n=
n=
n=
n=
4000,
8000,
16000,
32000,
Faktor=3.91,
Faktor=3.99,
Faktor=4.27,
Faktor=4.28,
Rechenzeit= 0.83
Rechenzeit= 3.31
Rechenzeit= 14.16
Rechenzeit= 60.56
16000 Abfragen aus einer ST mit 16000 Einträgen dauern 30 sec.
4.4.27
Die Ergebnisse werden entschieden besser, wenn die Eintäge zufällig
ausgewählt werden.
Das Experiment lässt erahnen, dass die Rechenzeit „ n ¨ log n ist.
n=
32000, Faktor=2.32, Rechenzeit= 0.15
n=
64000, Faktor=2.30, Rechenzeit= 0.34
n= 128000, Faktor=2.36, Rechenzeit= 0.81
n= 256000, Faktor=2.34, Rechenzeit= 1.91
n= 512000, Faktor=2.25, Rechenzeit= 4.29
n= 1024000, Faktor=2.26, Rechenzeit= 9.71
n= 2048000, Faktor=2.34, Rechenzeit= 22.72
n= 4096000, Faktor=2.08, Rechenzeit= 47.25
512000 Abfragen aus einer ST mit 128000 Einträgen dauern 2.34 sec.
4.4.28
Ergebnisse für avl mit
Beim AVL-Baum ist es egal, ob die Schlüssel sortiert oder zufällig gewählt
werden.
n=
32000,
n=
64000,
n= 128000,
n= 256000,
n= 512000,
n= 1024000,
Faktor=2.20,
Faktor=2.09,
Faktor=2.18,
Faktor=2.21,
Faktor=2.13,
Faktor=2.13,
Rechenzeit= 0.73
Rechenzeit= 1.52
Rechenzeit= 3.32
Rechenzeit= 7.32
Rechenzeit= 15.60
Rechenzeit= 33.19
Der Steigerungsfaktor ist etwas kleiner als bei den bst-Implementierungen.
Um ihn zuverlässiger zu ermitteln, müste man mehr Experimente machen.
Das Experiment lässt erahnen, dass die Rechenzeit „ n ¨ log n ist.
512000 Abfragen aus einer ST mit 128000 Einträgen dauern 2.54 sec.
4.4.29
Vergleich der Ergebnisse der Experimente
Die einfachste Implementierung ist offensichtlich so schlecht, dass man
sie nicht benutzen kann.
Die Implementierung mittels Hashtabelle schneidet sehr gut ab.
Wenn man abschätzen kann, wie groß die Symboltabelle wird, ist sie zu
bevorzugen.
Die Implementierungen mit binären Suchbäumen lohnen sich, wenn man
die Größe der Symboltabelle nicht abschätzen kann.
Die Implementierung mit AVL-Bäumen ist (bei zufälligen Einträgen) nicht
wirklich besser als mit (unbalancierten) binären Bäumen. Der Aufwand
für die Implementierung ist jedoch viel höher.
4.4.30
Herunterladen