• Keine Ergebnisse gefunden

Implementierung des ADT Menge durch charakteristische Funktionen

Im Dokument Programmiertechniken (NF) (Seite 68-71)

5. Abstrakter Datentyp 60

5.7. Implementierung des ADT Menge durch charakteristische Funktionen

Betrachtet man noch einmal die Definition eines ADT, so sieht man, dass ein ADT im wesentlichen aus der Schnittstelle besteht, also aus Funktionen. Die Implementierungen erfolgt meist mit Hilfe von vordefinierten Datenstrukturen (insb. Tupel, Listen oder Hashes) oder Klassen. Ein Beispiel hierf¨ur waren Br¨uche, welche wir zwar nicht als ADT spezifiziert haben, welche aber ein weiteres Beispiel f¨ur einen ADT sind. als Gesetze sollen Br¨uche nat¨urlich die Gesetze der Bruchrechnung (z.B. Assoziativit¨at und Kommutativit¨at) erf¨ullen.

Die formale Defintion von Br¨uchen als ADT ist eine gute ¨Ubung.

Hier wollen wir aber einen anderen Weg beschreiben und die Funktionen des ADT Menge direkt als Python-Funktionen implementieren. Wir beginnen mit dem Konstruktor f¨ur die leere Menge. Dies ist die konstante Funktion, welche f¨ur alle potentiellen Elemente den Wert False liefert:

d e f e m p t y s e t ( ) :

r e t u r n lambda x : F a l s e d e f i s e l e m ( x , s ) :

r e t u r n s ( x )

Dar¨uber hinaus haben wir direkt auch den Selektor is elem definiert, mit welchem wir ¨ uber-pr¨ufen k¨onnen, ob ein Wert in einer Menge enthalten ist. Da wir die Menge als Funktion realisiert haben, ist die Implementierung denkbar einfach. Wir wenden die (charakteristi-sche) Funktion einfach auf den zu ¨uberpr¨ufenden Wert an.

Als n¨achstes sieht der ADT f¨ur Mengen eine Operation add elem vor, mit welcher ein Wert zur Menge hinzugef¨ugt werden kann. Wir m¨ussen also eine Funktion konstruieren, welche

die neue Menge repr¨asentiert. Vergleicht man die alte und die neue Menge, so unterschieden sich die beiden Mengen nur an der Stelle des hinzugef¨ugten Elementes. ¨Uberpr¨uft man also sp¨ater, ob das neu hinzugef¨ugte Element in der Menge ist, m¨ussen wir True zur¨uckgeben.

Ist der sp¨ater mit is elem nachgeschlagene Wert (im Code x) nicht der neu hinzugef¨ugte Wert y, so schlagen wir x einfach in der alten Menge s nach.

d e f a d d e l e m ( y , s ) :

r e t u r n lambda x : True i f x == y e l s e s ( x )

Man beachte, dass wir hier Pythons if-then-else-Ausdruck verwenden, welcher wie folgt aufgebaut ist:

if-result if cond else else-result

Der Ausdruck wertet zun¨achst die Bedingung (cond) aus. Falls diese zu True auswertet, wird dasif-result zur¨uckgegeben, sonst daselse-result. Zum Beispiel kann die Maximums-funktion unter Verwendnung des if-then-else-Ausdrucks wie folgt definiert werden:

d e f max( x , y ) :

r e t u r n x i f x >= y e l s e y

Betrachtet man noch einmal die Definition von add elem sieht man, dass der R¨ uckgabe-Typ des if-then-else-Ausdrucks der boolesche uckgabe-Typ ist. Solche if-then-else-Ausdr¨ucke k¨onnen eigentlich immer eleganter ausgedr¨uckt werden. Hierzu nutzt man h¨aufig die booleschen Funktionenandoderor, welche zus¨atzlich noch die Eigenschaft haben, nicht-strikt in ihrem zweiten Argument zu sein. Dies bedeutet, dassandsein zweites Argument nicht auswertet, wenn das erste Argument bereits zu False auswertet. Es wird direkt False zur¨uckgegeben.

Entsprechend liefert or schon True zur¨uck, wenn das erste Argument zu True auswertet, ohne das zweite Argument zu berechnen.

In obigem Beispiel kann dies einfach wie folgt ausgedr¨uckt werden:

d e f a d d e l e m ( y , s ) :

r e t u r n lambda x : x == y o r s ( x )

Man beachte, dass der obige if-then-else-Ausdruck das Ergebnis True liefert, wenn x == y zu True ausgewertet wird. Wir k¨onnen die Bedingung also direkt als erstes Argument von or verwenden. Daor nicht-strikten im zweiten Argument ist, k¨onnen wir daselse−result als zweites Argument einsetzen. Es wird nur ausgewertet, wenn x nicht gleich y ist. Man beachte hierbei, dass auch der Ergenbnis-Typ der Anwendung der Menge s auf den sp¨ater nachgeschlagenen Wert x Boolean ist und somit ebenfalls als Argumenttyp zu orpasst.

Es bleibt nun nur noch die Definition der Operationen union und intersect . Ein nachge-schlagenes Element x ist in der Vereinigung der Mengen s1 und s2 enthalten, wenn x in einer der beiden Mengen enthalten ist. Dies k¨onnen wir wieder unter Verwendung vonor realisieren und schlagen den Wert x nur dann in der zweiten Menge nach, wenn x nicht in der ersten Menge enthalten ist.

Entsprechend ¨uberpr¨ufen wir das Enthaltensein eines Elementes in intersect unter Ver-wendung vonand.

d e f u n i o n ( s1 , s 2 ) :

r e t u r n lambda x : s 1 ( x ) o r s 2 ( x ) d e f i n t e r s e c t ( s1 , s 2 ) :

r e t u r n lambda x : s 1 ( x ) and s 2 ( x )

Eine Verwendung dieser Implementierung des ADT Menge sieht dann wie folgt aus:

s 1 = a d d e l e m ( 4 2 , a d d e l e m ( 7 3 , e m p t y s e t ( ) ) ) p r i n t( i s e l e m ( 7 3 , s 1 ) ) # True

s 2 = a d d e l e m ( 5 5 , a d d e l e m ( 4 2 , e m p t y s e t ( ) ) ) s = u n i o n ( s1 , s 2 )

p r i n t( i s e l e m ( 5 5 , s ) ) # True p r i n t( i s e l e m ( 5 , s ) ) # F a l s e

Betrachtet man die Laufzeit der Implementierung, so ist diese Implementierung zugege-bener Maßen nicht immer besonders effizient. Die konstruierten Funktionen, welche eine Mengen repr¨asentieren, klappern der Reihe nach alle Konstruktionsschritte der Menge ab.

Das heißt im Worst-Case, false das Element nicht in der Menge vorkommt, ist die Funk-tion is elem linear in der Anzahl der add elem-Schritte zur KonstrukFunk-tion der Menge. Im Gegenzug ist aber die Funktion add elem konstant.

Der Vorteil dieses Ansatzes ist aber, dass sie extrem schnell und nah an der formalen, mathematischen Spezifikation realisiert werden kann. Dar¨uber hinaus k¨onnen wir f¨ur diese Implementierung den ADT auch noch um eine Funktion invert erweitern, welche das Komplement einer Menge berechnet. Man beachte, dass eine solche Menge in er Regel unendlich groß ist und dennoch im endlichen Speicher eines Computers abgebildet werden kann:

d e f i n v e r t ( s ) :

r e t u r n lambda x : not s ( x )

Wir k¨onnen einfach das Ergebnis der charakteristischen Funktion negieren. Als Beispiel k¨onnen wir die Menge aller Werte, ohne die Zahlen 42 und 73 wie folgt definieren:

s 1 = i n v e r t ( a d d e l e m ( 4 2 , a d d e l e m ( 7 3 , e m p t y s e t ( ) ) ) ) p r i n t( i s e l e m ( 7 3 , s 1 ) ) # F a l s e

s 2 = a d d e l e m ( 5 5 , a d d e l e m ( 4 2 , e m p t y s e t ( ) ) ) s = u n i o n ( s1 , s 2 )

p r i n t( i s e l e m ( 4 2 , s ) ) # True p r i n t( i s e l e m ( 7 3 , s ) ) # F a l s e

außerdem ist es unter Bruch der Abstraktionsbarriere des ADT Menge, auch m¨oglich, unendliche Mengen direkt zu definieren. Als Beispiel definieren wir die Menge aller geraden Zahlen.

e v e n n u m s = lambda x : x % 2 == 0 odd nums = i n v e r t ( e v e n n u m s )

p r i n t( i s e l e m ( 4 2 , e v e n n u m s ) ) # True p r i n t( i s e l e m ( 7 3 , e v e n n u m s ) ) # F a l s e p r i n t( i s e l e m ( 4 2 , odd nums ) ) # F a l s e p r i n t( i s e l e m ( 7 3 , odd nums ) ) # True

Wir fassen noch einmal zusammen. Vorteile dieser Implementierung sind:

• sehr kurzer und ¨ubersichtlicher Code

• unendliche Mengen sind m¨oglich

• auch eine Funktion invert kann implementiert werden Nachteile dieser Implementierung:

• die Funktion size (s) l¨asst sich nicht mehr (so einfach oder ggf. auch gar nicht mehr) realisieren

• Mengen k¨onnen nicht in Listen umgewandelt, aufgez¨ahlt oder ausgegeben werden

• die Funktion is elem hat ggf. eine schlechte Laufzeit

5.8. Darstellung eines Key-Value-Stores durch charakteristische

Im Dokument Programmiertechniken (NF) (Seite 68-71)