Datenstrukturen Schwerpunkte Dynamische Datenstrukturen mit Java Umgang mit Objektreferenzen als "Pointer" Praktische Ergänzung zur Vorlesung A&D Vorbereitung zum Collection Framework Noch keine / kaum Verwendung von Generics Praktisch-orientierte Literatur : G.Saake, K-U. Sattler Algorithmen & Datenstrukturen. Eine Einführung mit Java dpunkt.Verlag © H.Neuendorf (44) (45) Datenstrukturen Zusammenfassung von Daten in semantisch strukturierter Weise Kennzeichen : Datenanordnung + erlaubte Operationen Bereits behandelte "Datenstrukturen" : 1. Klassen → Attribute enthalten Daten + Methoden regeln Zugriff Sehr allgemein - damit beliebige konkrete Datenstrukturen darstellbar 2. Arrays → Indizierte Anordnung : Datenzugriff via Index Wahlfreier Zugriff = Elemente in beliebiger Reihenfolge zugreifbar aber : Einfügen zwischen belegte Plätze muss implementiert werden Anzahl der Elemente steht fest - nicht veränderbar (statisch) ! unflexibel ! Dynamische Datenstrukturen = Container, die sich dynamisch der wachsenden oder verringernden Datenmenge anpassen ! © H.Neuendorf Datenstrukturen Stack und Queue zur Einführung Realisation auf Basis von Arrays © H.Neuendorf (46) Stack (47) ( Keller, Stapel ) Stapel von Daten (Objekten), die "übereinander liegen" : LIFO = Last In First Out - Zuletzt abgelegtes Element wird als erstes wieder entnommen Typische Operationen: Stapel s, Element x : Ablegen neuer Objekte oben auf Stapel push Zurückliefern + Entfernen des obersten Stapelobjekts pop Zurückliefern oberstes Stapelobjekt ohne Entfernen peek s.push( x ) ; // Element abgelegt x = s.pop( ) ; // Rückgabe des entfernten obersten Elements KEIN wahlfreier Zugriff - nur oberstes Element jeweils ansprechbar x s.push(x); y x s.push(y); Einfügen und Entnehmen am selben Ende = "oben" x zB Bücherstapel … z = s.pop(); z = s.pop(); // z == y // z == x Informatik → Methodenstack / Rekursions-Zwischenergebnisse auf Stack ( kein Basisfall © H.Neuendorf stack overflow! ) Stack-Implementierung via Array push : Anfügen des Elements "am Ende" des Arrays pop : (48) an jeweils oberster Position Abholen des Elements "vom Ende" des Arrays Fehlersituationen : Kein Platz mehr für neue Elemente overflow Keine Elemente mehr im Array underflow Stack - int [ ] s - int pos + void push( int x ) Durch Hilfsmethoden wie isFull() isEmpty() kann man Prüfung auf RuntimeExceptions vermeiden Bedeutung pos : Anzahl der momentan im Stack gespeicherten Datenelemente Zugleich Index des nächsten freien Platzes, auf den das nächste einzufügende Datenelement zu setzen ist = aktuelle Einfügeposition + int pop( ) Belegung beginnt bei Indexposition 0 // … Wirkung von Array s pos = pos - 1 ; nächster freier Platz hat Index pos -1; dortiger Wert wird überschrieben Statt int könnte man beliebige Objekte im Stack ablegen durch entsprechenden Typ der Arrayelemente Bsp: Mitarbeiter[ ] s Hier : © H.Neuendorf Array fester Größe !? push ... s[3] s[2] s[1] s[0] pop Stack-Impl. via Array (49) class Stack { Stack hat "Gedächtnis" - merkt sich zuletzt besetzten Platz private int[ ] s ; // privater Datenbehälter private int pos ; // Anzahl gespeicherter Elemente public Stack( int size ) { s = new int[size] ; Array gewünschter Größe anlegen pos = 0 ; } Zugriff nur über öffentliche Schnittstelle! public void push( int x ) { boolean overflow = ( pos >= s.length ) ; Kein direkter Zugriff auf Daten im Array ! if ( overflow ) { /* Ausnahmebehandlung */ } s[ pos ] = x ; Prüfung, ob Array voll ist pos++ ; } public int pop( ) { Wenn noch Platz, dann Datenelement einfügen boolean underflow = ( pos == 0 ) ; if ( underflow ) { /* Ausnahmebehandlung */ } Zähler hochzählen pos-- ; return s[ pos ] ; Prüfung, ob Array leer ist } Wenn nicht leer, dann Datenelement zurückliefern public boolean isFull( ) { return pos >= s.length ; } Zähler vermindern © H.Neuendorf public boolean isEmpty( ) { return pos == 0 ; } public void clear( ) { pos = 0 ; } } (50) Queue ( Schlange, Puffer ) Daten-Röhre : An einem Ende rein, am andern raus ... FIFO = First In First Out - Zuerst abgelegtes Element als erstes wieder entnommen Typische Operationen : Anfügen neuer Objekte am Queue-Ende Queue q, Element x : put Zurückliefern + Entfernen vorderstes Objekt get Zurückliefern vorderstes Objekt ohne Entfernen peek q.put( x ) ; // Element abgelegt x = q.get( ) ; // Rückgabe + Entfernen vorderstes Element KEIN wahlfreier Zugriff - nur vorderstes Element ansprechbar ! x xy x q.put(x); y x q.put(y); z q.put(z); y z e = q.get(); z e = q.get(); // e == x x y z Head Tail Entnahmestelle Einfügestelle Einfügen und Entfernen an verschiedenen Enden ! // e == y Warteschlangen dienen der Entkopplung. Zwischengeschalteter Puffer zwischen Erzeuger (Einfügen am tail) und Verbraucher (Entnahme am head) von Daten, Nachrichten etc. Keine direkte Interaktion. Voraussetzung ist korrekte Synchronisation. Informatik / Modellierung : Warteschlangen, Tasks, Message Queues, Puffer ... © H.Neuendorf Queue-Implementierung via Array (51) put : Anfügen Element am Array-Ende get : Abholen "ältestes" Elements vom am "längsten belegten" Array-Platz Index head : erstes = "ältestes" Element Index tail : Entnahme alter Elemente aus q[ head ] nächster freier Platz Fehlersituationen : Queue - int[ ] q Kein Platz mehr für neue Elemente overflow Keine Elemente mehr im Array underflow 0 1 2 4 5 6 7 4 5 6 7 tail head 0 1 2 3 q head + void put( int x ) + int get( ) 3 q - int head, tail - int size Einfügen neuer Elemente in q[ tail ] tail Vereinfachung: Nach Entnahme wird Inhalt nicht nach links verschoben ! Stattdessen Index head erhöht belegter Teil wandert nach rechts Belegter Teil erreicht Arrayende - obgleich Array nicht voll ist Nutzung leerer Plätze am Anfang : © H.Neuendorf Zyklisches Array … Queue-Implementierung via Array 0 1 2 3 4 5 6 7 q (52) Zyklisches Array (Ringpuffer) : Indices head und tail laufen "im Kreis" tail head 0 1 2 3 4 5 6 7 Darstellbar durch Berechnung des Index tail nach Einfügen : tail = ( tail + 1 ) % 8 ; q allgemein : modulo size head tail Berechnung Index head nach Entnahme : 7 head = ( head + 1 ) % size ; 0 Erkennen Ausnahmesituationen 6 1 a) Leere Schlange (underflow) : head == tail ; b) Volle Schlange (overflow) : tail ist "rundherum gelaufen" - also wiederum head == tail. Zur Unterscheidung von leerer Schlange wird deshalb als overflow definiert, dass tail direkt vor head steht, d.h. : ( tail +1 ) % size == head ; Man verschenkt einen Platz zugunsten Eindeutigkeit … © H.Neuendorf Queue via Array Queue - int[ ] q class Queue { private int[ ] q ; private int head ; private int tail ; private int size ; - int head, tail + void put( int x ) + int get( ) Zugriff nur über öffentliche Schnittstelle! Kein direkter Zugriff auf die Daten im Array! Statt int könnte man beliebige Objekte in Queue ablegen durch entsprechenden Typ der Arrayelemente Hier : Array fester Größe !? © H.Neuendorf // // // // privater Datenbehälter Index erstes Element Index nächster freier Platz = Einfügeposition Queue-Größe public Queue( int n ) { q = new int[ n ] ; size = n ; tail = 0 ; head = 0 ; } public void put( int x ) { boolean overflow = ( (tail + 1) % size == head ) ; if ( overflow ) throw new RuntimeException( "Full" ) ; q[ tail ] = x ; tail = (tail + 1) % size ; } public int get( ) { boolean underflow = ( head == tail ) ; if ( underflow ) throw new RuntimeException( "Empty" ) ; int x = q[ head ] ; head = (head + 1) % size ; return x ; } public boolean isEmpty( ) { return head == tail ; } public boolean isFull( ) { return ( (tail + 1) % size == head ) ; } public void clear( ) { head = tail = 0 ; } - int size Umständliche Verwaltung der Belegung als zyklisches Array (53) } Abstrakte Datentypen (54) ADT Abstrakter Datentyp von Implementierung unabhängige Spezifikation einer Datenstruktur unterschiedliche Implementierungen sind möglich Details der Implementierung interessieren Anwender nicht – sind weggekapselt universelle, von der Implementierung unabhängige Verwendung "abstrakt" - da nach außen nur öffentliche Methoden bekannt sind, nicht aber Impl.-Details Realisierung von ADTs in Java Spezifikation (der Methoden) der Datenstruktur durch ein Interface verschiedene implementierende Klassen Unabhängigkeit von Implementierungen durch Verwendung des Interface-Typs beim Aufrufer Instanziiert werden jedoch natürlich stets konkrete Implementierungen Beispiel Abstrakte Datentypen 'Stack' + 'Queue' © H.Neuendorf → intern array-basiert oder list-basiert realisierbar Abstrakte Datentypen Bsp : Datentyp Stack (55) Spezifikation (der Methoden) des Datentyps durch Interface public interface Stack { public void push( Object obj ) ; public Object pop( ) ; public Object peek( ) ; public void clear( ) ; public boolean isEmpty( ) ; } Wichtige Detail-Entscheidung bei Dokumentation des Interfaces + seiner Implementierung (zB auch im Collection Framework) : Werden null-Werte als Datenelemente akzeptiert – und führen dann zur Erhöhung der internen size ? Können null-Werte von Methoden zurückgeliefert werden ? Zurückliefern von null sollte eher bedeuten, dass gesuchtes Elemente nicht vorhanden ist ! Bereitstellen (mindestens) einer Implementierung public class ArrStack implements Stack { private Object[ ] s = new Object[ 100 ] ; private int pos = 0 ; public void push( Object obj ) { // Implementierung … } /* … */ } Anwendung : Methodenaufruf via Interface-Typ – sorgt für semantische Klarheit Stack s = new ArrStack( ) ; s.push( "Hallo" ) ; © H.Neuendorf Datenstrukturen Dynamische Datenstrukturen Realisation durch Pointer = Objektreferenzen © H.Neuendorf (59) (60) Dynamische Datenstrukturen Verketten von Objekten mittels Zeigern / Referenzen Grundlage dynamischer Datenstrukturen : Verkettete Objekte = Knoten Dynamisch : Knoten zur Laufzeit erzeugt + verkettet Datenstruktur kann dynamisch wachsen + schrumpfen Elemente können jederzeit instanziiert oder freigegeben werden Anzahl der Knoten nicht prinzipiell festgelegt ! (≠ Arrays!) Keine Speicherplatz-Verschwendung / Überläufe wie bei Arrays ! Baum : Verzweigte Struktur Liste : Lineare unverzweigte Struktur Jeder Knoten hat nur einen Nachfolger Objektreferenz = "Zeiger" auf Objekt Java : © H.Neuendorf Objektvariable Jeder Knoten kann mehrere Nachfolger (childs) haben Jeder Knoten wird nur von einem Vorgänger referenziert (ein parent-Knoten) Wiederholung : Objekt-Referenzen class Mitarbeiter { public String name ; public double gehalt ; } class Verwaltung { public static void main( String[ ] args ) { (61) (2048) m1 (####) Schmidt 5000.0 m2 (2048) m1 (2048) Mitarbeiter m1 = new Mitarbeiter( ) ; (2048) (2048) Schmidt 5000.0 m2 m1.name = "Schmidt"; m1.gehalt = 5000.0 ; Mitarbeiter m2 ; m2 = m1 ; ( null ) m1 ( null ) Schmidt 5000.0 m2 m1 = null ; m2 = null ; } } © H.Neuendorf Nicht mehr referenzierte Objekte werden durch Garbage Collection automatisch gelöscht Verketten von Knoten via Objektreferenzen class Node { // Knotenklasse public int val ; // Datenelement // Objektreferenz! public Node next ; (62) Selbstbezügliche Datenstruktur : Attribut next der Knotenklasse Node ist vom gleichen Typ wie Klasse Node selbst : public Node( int v ) { val = v ; next = null ; } Jedes Element kann Referenz auf ein Nachfolger-Objekt halten ! Typ von val könnte auch Referenztyp sein, zB Mitarbeiter, Konto, Object … } Verketten zweier Node-Objekte : Node a = new Node(3) ; a b 3 Node b = new Node(5) ; val next 5 a.next = b ; a (5078) (1024) b © H.Neuendorf val 3 next (1024) val next 5 null (5078) (1024) b a Realisierung mittels Referenzen 3 5 Verketten von Knoten via Objektreferenzen class Node { // Knotenklasse public int val ; public Node next ; // Objektreferenz! public Node( int v ) { val = v ; next = null ; } } (63) Selbstbezügliche Datenstruktur Bedeutung der Variablen next : Zeiger zum Verketten von Node-Objekten = Referenz auf Nachfolger ! Inhalt = Referenz auf anderes Node-Objekt (Nachfolger) oder null (kein Nachfolger) null kennzeichnet Listenende a b 3 Liste aus zwei Knoten val next 5 Kann von vorne nach hinten durchlaufen werden : → Startknoten (head) ist Objekt a → Von a kommt man zu b mittels Attribut a.next b a 3 Via Referenz auf Startknoten kann man sich durch gesamte Liste "hangeln" Wenn erster Knoten verfügbar ist, so sind alle Knoten und deren Elemente verfügbar © H.Neuendorf 5 Array ↔ Verkettete Liste : Komplementäre Leistungen tail head 5 3 12 Array : Verkettete Liste : Rascher direkter wahlfreier Random AccessZugriff durch Index O(1) Langsamerer Random Access-Zugriff aufgrund Durchwanderns der Liste O(n) Einfügen am Array-Ende schnell wegen direkten Zugriffs O(1) Einfügen am Ende bei tail-Pointer schnell O(1) Einfügen am Anfang bzw. bei Index i langsam, da n bzw. (n-i) Elemente verschoben werden müssen: O(n) Es wird bei primitiven Datentypen nur ein Objekt = Array benötigt Größe ist fest vorgegeben Arrays können effizienter sein für Daten mit häufigem nur lesenden Random Access Zugriff © H.Neuendorf (64) Einfügen generell relativ schnell, da keine Elemente verschoben und kopiert werden müssen. Modifikation bei jeder Größe relativ schnell. Durch doppelte Verkettung verbesserbar. Es sind stets zahlreiche Objekte involviert Es wird nur soviel Speicher wie erforderlich belegt Datenstrukturen Dynamische Datenstrukturen Verkettete unsortierte Liste Operationen in iterativer Formulierung (Rekursive Formulierungen jedoch auch gut darstellbar) © H.Neuendorf (65) Unsortierte Listen - (66) Operationen tail head 5 3 12 Knoten-Reihenfolge entspricht nicht Sortierreihenfolge ihrer Datenelemente Methoden : boolean add( Object obj ) // fügt obj am Ende der Liste an und gibt true zurück boolean addFirst( Object obj ) // fügt obj am Anfang der Liste an und gibt true zurück int size( ) // Anzahl der Elemente in Liste Object get( int pos ) // liefert an Position pos gespeichertes Objekt zurück ohne Entfernen ?! besser : int key // liefert Objekt mit bestimmtem key (ID) zurück boolean contains( Object obj ) // liefert true, falls Liste spezifiziertes Objekt enthält boolean remove( Object obj ) // löscht erstes Vorkommen des spezifizierten Objekts gibt true zurück, falls Objekt gefunden und gelöscht wurde void clear( ) // leert Liste boolean isEmpty( ) // testet, ob Liste leer Anmerkung 1: Manche Spezifikationen fordern für add()-Methoden keinen Rückgabewert, da Hinzufügen von Elementen in Liste immer funktioniert. In bestimmten Datenstrukturen (zB Set) ist Hinzufügen nicht immer möglich, so dass Erfolg oder Misserfolg durch den Rückgabewert ausgedrückt wird. Um gleiche Semantik dieser Methode für möglichst viele Datenstrukturen zu erhalten, macht es Sinn, die Methoden wie angegeben zu spezifizieren. Anmerkung 2: Beim Verwalten von Objekten (statt einfacher Werte) ist Methode get( ) sinnvoll, die Referenz auf das Objekt gemäß ID = key zurückliefert. Vor- oder Rückgabe von Indexpositionen ist weniger sinnvoll. © H.Neuendorf Listen - Erwägungen (67) tail head 3 5 12 1. Doppelte Einträge Sind Einträge äquivalenter Objekte erlaubt ? Stack, Queue, Pool, UnsortedList → ja (tendenziell) Baum, Hashtable → nein 2. Abgriff von Einträgen Werden Objekte beim Zugriff entfernt ? Stack, Queue, Pool → ja - ansonsten eher nicht 3. Rückgabewerte Welche Art von Rückgabewerten machen Sinn ? Die an der Position pos bzw. unter key gespeicherten Werte bzw. Referenzen Keine Rückgabe von Referenzen auf Node-Objekte → sind internes technisches Detail der Datenstruktur, das Verwender nicht kümmert am besten als private innere Klasse darstellen Keine Rückgabe von Indexpositionen – ebenfalls technisches Detail © H.Neuendorf Unsortierte Listen Spezifikation des abstrakten Datentyps List als Interface : public interface List { public boolean add( Object obj ) ; public boolean addFirst( Object obj ) ; public int size( ) ; public Object get( int pos ) ; public boolean contains( Object obj ) ; public boolean remove( Object obj ) ; public void clear( ) ; public boolean isEmpty( ) ; } Eleganter : Definition von Node als private Klasse innerhalb von LinkedList. Somit Node von außen nicht sichtbar © H.Neuendorf (68) - Operationen Implementierung von List durch die Klasse LinkedList und Klasse Node : class LinkedList implements List { private Node head ; public boolean add( Object obj ) { /* …*/ } // … } class Node { public Node( Object obj ) { val = obj; next = null ; } public Object val ; public Node next ; } class LinkedList implements List { private Node head ; public boolean add( Object obj ) { /* …*/ } // … private class Node { Node( Object obj ) { val = obj; next = null ; } Object val ; Node next ; } } Unsortierte Listen (69) - Operationen tail head 5 3 12 Liste = Knoten + typische Operationen : Vereinfachte Veranschaulichung mit Datentyp int // Knotenklasse class Node { Anhängen von neuen Werten / Knoten am Ende public int val ; Einfügen von neuen Werten / Knoten am Anfang public Node next ; Löschen von Werten / Knoten public Node( int v ) { Suchen nach Werten / Knoten val = v ; Objektorientiertes Umsetzung : next = null ; Klasse LinkedList , die Knoten-Objekte enthält } LinkedList - Node head, tail Node int val + boolean addFirst( int x ) } // Listenklasse class LinkedList { Node next private Node head = null ; + boolean add( int x ) + boolean remove( int x ) private Node tail = null ; + boolean contains( int x ) Es könnten beliebige Datentypen in Listen gespeichert werden. // … Attribute von Node nur deshalb public, um Implementierung zu vereinfachen © H.Neuendorf // … } Einfügen List-Elemente → Am Listenende tail head 3 5 (70) class LinkedList { // kein Zugriff auf Pointer! 12 private Node head = null ; private Node tail = null; Liste am Anfang leer head + tail mit null initialisiert public boolean add( int val ) { 1. Neuen Knoten p erzeugen und Wert darin speichern Node p = new Node( val ) ; 2. Mittels tail.next auf den neuen Knoten verweisen: if ( head == null ) tail.next = p ; { head = p ; } Dadurch ist dieser nun ans Ende der Liste gehängt else 3. Variable tail auf den neuen Knoten setzen, { tail.next = p ; } da dieser nun das neue Ende der Liste darstellt : tail = p ; return true ; tail = p ; } Fälle : a) Liste noch leer } (head == null) : neuer Knoten p ist einziger Knoten head = tail = p b) Liste nicht mehr leer (head != null) : neuer Knoten p ans Ende der Liste © H.Neuendorf tail.next = p Initialzustand head tail add12); head (p) 12 tail (71) Einfügen List-Elemente → Am Listenende Initialzustand head tail (p) 12 tail private Node tail = null ; public boolean add( int val ) { Dann : Node p = new Node(val) ; Neuen tail erzeugen, indem tail nun auf neuen Knoten p zeigt : tail = p ; (p) 3 12 if ( head == null ) { head = p ; } else add(3); head private Node head = null ; Alten tail verwenden, um auf neuen Knoten zu verweisen : tail.next = p ; add(12); head class LinkedList { Zuerst : { tail.next = p ; } tail = p tail = p ; return true ; tail.next = p } } add(45); head (p) 12 3 45 tail.next = p Wachstumsrichtung der Liste © H.Neuendorf tail = p Objektvariable p ist lokale Variable der Methode add : Lebt nur während Ausführung der Methode - im Gegensatz zu globalen Variablen head + tail ! Einfügen List-Elemente → Am Listenanfang tail head 5 (72) 3 class LinkedList { private Node head = null ; 12 private Node tail = null ; public boolean addFirst( int val ) { 1. Neuen Knoten p erzeugen und Wert darin speichern Node p = new Node( val ) ; 2. Der neue Knoten hat als Nachfolger den bisherigen if( tail == null ) tail = p; p.next = head ; Listenanfang (head) head = p ; p.next = head ; return true ; 3. Der neue Kopf der Liste ist nun der neue Knoten : Knoten p wird zum neuen Listenanfang head = p ; Keine Unterscheidungen hinsichtlich Listenlänge (leer / nicht leer) erforderlich Bei unsortierten Listen ist es egal, wo man neues Element einfügt … } } Vorteil gegenüber Arrays : Einfügen von Elementen am Anfang erfordert kein Verschieben von Elementen ! Initialzustand head addFirst(12); head 12 © H.Neuendorf (73) Einfügen List-Elemente → Am Listenanfang class LinkedList { Initialzustand head tail private Node head = null ; private Node tail = null ; addFirst(12); head = p (p) 12 public boolean addFirst( int val ) { tail = p Node p = new Node( val ) ; if( tail == null ) tail = p; p.next = head ; p.next = head = null head = p head = p ; return true ; addFirst(3); } (p) 3 12 } Zuerst : p.next = head head = p addFirst(45); Wachstumsrichtung der Liste Dann : (p) 45 3 p.next = head © H.Neuendorf Next-Zeiger des neuen Knotens p auf alten head zeigen lassen : p.next = head ; 12 Neuen head erzeugen, indem head nun auf neuen Knoten p zeigt : head = p ; Unsortierte Listen : Suchen von Elementen (74) tail head 3 5 12 class LinkedList { private Node head = null ; Suchen private Node tail = null ; 1. An den Anfang der Liste gehen : public boolean addFirst( int val ) { … } Hilfszeiger p = head ; 2. Knoten der Liste durchlaufen und prüfen, ob gesuchter Wert enthalten : public boolean contains( int val ) { p.val == val Node p = head ; Wenn nicht, zum nächsten Knoten übergehen : while( p != null && p.val != val ) { p = p.next ; p = p.next ; 3. Wenn p == null ist Listenende erreicht } return p != null ; Wert nicht gefunden } Umsetzung durch while-Schleife → Läuft, solange gesuchter Wert noch nicht gefunden evtl. bis ans Ende der Liste ! } Erstes vorkommendes Element wird gefunden – Mehrfacheinträge können nicht erfasst werden … Bei Objekten mit equals( ) arbeiten © H.Neuendorf Unsortierte Listen : Löschen von Elementen head (75) Problem : 3 5 12 Klasse Node kennt nur Nachfolger Wie bekommt man Vorgängerknoten ? p head Beim Durchlauf zweiten Hilfszeiger prev mitführen, der auf Knoten vor p zeigt ! 3 5 12 "Umbiegen" : prev p prev.next = p.next ; Sonderfall : Löschen p ist erstes Element der Liste → hat keinen Vorgänger ! (prev == null) 1. Zu löschenden Knoten suchen Neuer Listenanfang ist Nachfolger von p Hilfszeiger p darauf setzen 2. Next-Zeiger des Vorgängerknotens auf Nachfolger Liste beginnt nun mit Nachfolger von p "Umbiegen" : head = p.next ; des zu löschenden Knotens setzen ("umbiegen, überbrücken") Zu löschender Knoten nun nur noch von p referenziert Zeiger p ist lokale Variable der Methode Erstes vorkommendes Element gelöscht Garbage Collection entsorgt den nicht mehr referenzierten Knoten nach Methodenende ! Mehrfacheinträge nicht erfassbar … © H.Neuendorf Unsortierte Listen : Löschen von Elementen class LinkedList { private Node head ; private Node tail ; public boolean remove( int val ) { Node p = head ; Node prev = null ; // Hilfszeiger while( p != null && p.val != val ) { prev = p ; p = p.next ; } if ( p != null ) { if ( p == head ) { head = p.next ; } else { prev.next = p.next ; } if( p == tail ) { tail = prev ; } return true ; } else return false ; } } © H.Neuendorf (76) Abfrage darf nicht lauten : if ( p.val == val ) da p auch null sein könnte, wenn Element nicht in Liste enthalten Weiterführen von prev und p p == head : Lösch-Element am List-Anfang p == tail : Tail-Pointer umsetzen ! Wenn p == null ist zu löschendes Element nicht in Liste enthalten false zurückgeben ! Ausgabe Listenelemente → Verwendung toString( ) class LinkedList { Ausgabe Liste : private Node head ; private Node tail ; z.B. durch runde Klammern begrenzt + Listenelemente durch Leerzeichen getrennt. public String toString( ) { Werte werden in Stringbuffer geschrieben und am Ende in String umgewandelt. StringBuffer sb = new StringBuffer( ) ; String wird zurückgegeben. sb.append( "( " ) ; Liste von Anfang bis Ende durchlaufen. Node p = head ; if( p == null ) return "Leer" ; while( p != null ) { sb.append( p.val ) ; sb.append( " " ) ; p = p.next ; Weiterführen von p } sb.append( " )" ) ; return sb.toString( ) ; } } © H.Neuendorf Übung : Wie lauten Methoden void clear( ) und boolean isEmpty( ) ? (77) Doppelt verkettete Liste Erlaubt Traversieren in zwei Richtungen (78) Node class DLinkedList { private Node head ; prev private Node tail ; val public boolean add( int val ) { next Node p = new Node( val ); if( head == null ){ tail = p ; class Node { head = p ; } public int val ; else{ public Node next ; public Node( int v ) { tail.next = p ; val = v ; p.prev = tail ; next = null ; tail = p ; } return true ; public boolean remove( int val ) { /* Übung */ } } © H.Neuendorf prev = null ; } } } public Node prev ; head tail Datenstrukturen Dynamische Datenstrukturen Verkettete sortierte Liste © H.Neuendorf (79) (80) Sortierte Liste tail head 5 7 Knoten-Reihenfolge entspricht Sortierreihenfolge ihrer Datenelemente 12 Suchen Vorteil : class SortedList { Sortierung erlaubt sortierte Ausgabe der Werte ohne extra Sortieroperation private Node head ; private Node tail ; Schnelleres Aufsuchen von Elementen - Liste muss nicht bis Ende durchsucht werden : // public boolean contains( int val ) { Abbrechen der Suche, wenn man auf Elemente stößt, die "größer" sind als gesuchtes Element Node p = head ; Aber : while( p! = null && p.val < val ) Nicht für alle Typen existiert Ordnungskriterium p = p.next ; Für effizientes Suchen in großen Datenbeständen sind sortierte Suchbäume geeigneter als Listen Lineare Liste zum Durchsuchen nicht gut geeignet, da kein wahlfreier Zugriff Binary Search nicht effizient darstellbar ! © H.Neuendorf if ( p != null && p.val == val ) return true ; else return false ; } } (81) Lineares Suchen in verketteter Liste class LinkedList { private Node head ; class SortedList { private Node head ; private Node tail ; private Node tail ; public boolean contains( int val ) { Node p = head ; public boolean contains( int val ) { Node p = head ; while( p! = null && p.val < val ) { p = p.next ; } while( p != null && p.val != val ) { p = p.next ; } if( p != null ) return true ; if ( p != null && p.val == val ) return true ; else return false ; else return false ; } } Zeitbedarf : hoch, da ungeordnet ! neg. Fall → n Vergleiche pos. Fall → ∅ n /2 Vergleiche Komplexität O(n) Wächst linear mit Zahl der Elemente © H.Neuendorf } } Zeitbedarf : bleibt hoch ! neg. Fall → ∅ n / 2 Vergleiche kann früher abgebrochen werden pos. Fall → ∅ n /2 Vergleiche Komplexität bleibt O(n) Wächst linear mit Zahl der Elemente Datenstrukturen Dynamische Datenstrukturen Stack und Queue auf Basis verketteter Listen © H.Neuendorf (87) (88) Stack via verketteter Liste Nachteil Array-Implementierung : Fest vorgegebene Größe kann überlaufen ! Vorteil List-Implementierung : Beliebig viele Elemente hinzufügbar - bis Heap erschöpft Darstellung als verkettete Liste : Gespeicherte Werte könnten auch Referenzen sein ... Zeiger top zeigt auf obersten Knoten next-Zeiger liefern darunterliegende Elemente Operation push( ) : Einfügen eines neuen Knotens am Anfang der Liste Operation pop( ) : Entfernen eines Knotens vom Anfang der Liste top - Node top + int pop( ) val next Stack + void push( int x ) Node Node int val Node next Schnittstelle der Stack-Klasse ist die selbe wie bei Realisation mit Array Außensicht + Verwendbarkeit unverändert. Node gerne als private innere Klasse! Interne Umsetzung ist völlig anders - ohne dass Verwender dies wahrnehmen Konstanz der Methodenschnittstellen + ihrer wahrnehmbaren Funktionalität ↔ Verbergen der internen Abläufe + Datenstrukturen Information Hiding ! © H.Neuendorf Abstrakte Datenstrukturen Stack via verketteter Liste Einfügen push( ) : (89) class Stack { private Node top = null ; 1. Neuen Knoten p mit Wert erzeugen 2. Nachfolger des neuen Knotens ist bisheriger top : public void push( int x ) { Zugriff nur über öffentliche Schnittstelle! Kein direkter Zugriff auf top-Pointer ! Node p = new Node( x ) ; p.next = top ; p.next = top ; 3. Neuer top ist nun der neue Knoten p : top = p ; top = p ; } public int pop( ) { Entfernen pop( ) : if ( top == null ) { /* Exception */ } 1. Wenn Stack leer ist ( top == null ) Exception werfen else { Node p = top ; 2. Wenn Stack nicht leer top-Element entfernen : top = top.next ; 2.1. Lokalen Hilfszeiger p auf top setzen: Node p = top ; return p.val ; 2.1. Nachfolger des bisherigen top-Elements wird top : } top = top.next ; } public boolean isEmpty( ) { 2.2. Wert des bisherigen top-Elements zurückgeben return top == null ; return p.val ; 3. Altes top-Objekt nun nicht mehr referenziert von GC nach Methodenende entsorgt nachdem lokale Referenz p verschwunden © H.Neuendorf } } top top (p) Stack via verketteter Liste : top 1. 5 push( ) push( 5 ) (90) class Stack { private Node top = null ; Node p = new Node( 5 ) ; public void push( int x ) { p.next = top; // = null ! p Node p = new Node( x ) ; p.next = top ; top top = p ; 2. 5 } top = p ; p // … } 8 p top push( 8 ) 1. Node p = new Node( 8 ) ; p.next = top; top 8 p 5 5 © H.Neuendorf 2. top = p ; Stack via verketteter Liste : top pop( ) 3 p 8 (91) pop( ) class Stack { 1. Node p = top ; private Node top = null ; ...................... public int pop( ) { if ( top == null ) { /* Exception */ } else { Node p = top ; top = top.next ; return p.val ; } } // … return p.val ; 5 Methodenende : Garbage Collection 3 top (p) 8 5 © H.Neuendorf } Referenz p ist lokale Variable der Methode pop( ) 2. top = top.next ; Nach Methodenende verschwindet p und somit die letzte Referenz auf Element Garbage Collection entfernt Element ! (92) Queue via verketteter Liste Nachteil Array-Implementierung : Fest vorgegebene Größe Vorteil List-Implementierung : Beliebig viele Elemente hinzufügbar - bis Heap erschöpft Darstellung als verkettete Liste : kann überlaufen head tail Zeiger head zeigt auf Anfang der Schlange Zeiger tail zeigt auf Ende der Schlange Verkettung läuft von Anfang Richtung Ende Operation put( ) : Anhängen neues Element ans Ende Operation get( ) : Entfernen des ältesten Elements vom Anfang Sonderfall : Schlange ist leer es kann nichts entnommen werden Queue - Node head, tail + void put ( int x ) + int get ( ) © H.Neuendorf Node int val Node next tail-Pointer ermöglicht bei put( )Operation direkten Zugriff auf das letzte Element … (93) Queue via verketteter Liste class Queue { private Node head = null ; private Node tail = null ; Einfügen put( ) : 1. Neuen einzufügenden Knoten p erzeugen 2. Leere Schlange (head == p) : public void put( int x ) { Node p = new Node( x ) ; if ( head == null ) head = p ; else tail.next = p ; tail = p ; } public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null ; return p.val ; } } neuer Knoten p wird neuer head : head = p ; Nichtleere Schlange - neuen Knoten p ans Ende : tail.next = p ; 3. Nach Anhängen ist neuer Knoten p das neue Ende : tail = p ; Entfernen get( ) : 1. Wenn Schlange leer ( head == null ) Exception werfen 2. Hilfszeiger p auf Kopf setzen - soll entfernt werden : Node p = head ; Rückgabe von p.val 3. Neuer Kopf ist der Nachfolger des bisherigen Kopfes: head = head.next ; 4. Wenn letztes Element der Schlange entfernt wird, dann auch tail = null ; head head + tail nun beide null public boolean isEmpty( ) { tail return head == null ; } } © H.Neuendorf head put( 9 ) Queue via verketteter Liste tail (94) class Queue { private Node head = null ; private Node tail = null ; 1. Node p = new Node( 9 ) ; 9 public void put( int x ) { p head Node p = new Node( x ) ; tail if ( head == null) head = p ; else tail.next = p ; tail = p ; } 2. tail.next = p ; // ... 9 } p Sonderfall : Leere Queue head - head ist noch null - head zeigt nach Erzeugen von tail 3. tail = p ; 9 (p) © H.Neuendorf neuem Knoten p auf diesen head = p ; Methodenende: Lokale Variable p gelöscht get( ) head Queue via verketteter Liste tail (95) class Queue { private Node head = null ; private Node tail = null ; 1. Node p = head ; p head 2. p GC ! (p) © H.Neuendorf public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null; return p.val ; } } // ... tail head = head.next ; ................ return p.val ; head tail } Methodenende : Lokale Variable p gelöscht Altes head-Objekt nicht mehr referenziert Garbage Collection ! head tail Queue via verketteter Liste get( ) Sonderfall : (96) class Queue { Queue enthält nur noch ein Element p head private Node head = null ; private Node tail = null ; 1. Node p = head ; tail public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null ; return p.val ; } } // ............ 2. head = head.next ; // = null ! ...................................... return p.val ; p head GC ! tail 3. if( head == null ) tail = null ; Methodenende : Lokale Variable p gelöscht Objekt nicht mehr referenziert (p) © H.Neuendorf } Garbage Collection ! Es bleibt leere Queue zurück - head + tail sind wieder null ! (97) Datenabstraktion Grundprinzip = Information Hiding ↔ Ziel = Reduktion von Komplexität : Implementierungsdetails verbergen → Zugriff auf komplexe Strukturen nur über wenige öffentliche Methoden = einfache Schnittstelle ohne technische interne Implementierungsdetails offenzulegen ! Vorteil: Interne Implementierungsdetails können jederzeit geändert werden … … solange Schnittstelle konstant bleibt Verwender / Clients werden nicht invalidiert Datenabstraktion : Abstrakte Datentypen : "Wie" der Implementierung wird versteckt Nicht Implementierung ist vorgegeben, sondern die semantischen Operationen auf Datenelementen. Somit kann Implementierung jederzeit geändert und neuen Erfordernissen angepasst werden – ohne dass verwendende Programme verändert werden müssten. Nur das "Was" der Datenhaltung exponiert in öffentlicher Schnittstelle Bsp : Stack-Klasse mit konventioneller Schnittstelle public int pop( ) public void push( int x ) Implementierung via Array oder Verketteter Liste Implementierungsdetail - von Außen nicht sichtbar Ist austauschbar ! © H.Neuendorf Datenstrukturen Dynamische Datenstrukturen Design-Varianten © H.Neuendorf (98) Strategy-Interfaces interface Action { // Strategy-IF public abstract Object act( Node h ) ; } class LinkedList { // Liste ganzer Zahlen private Node head = null ; private Node tail = null ; public boolean add( int val ) { Node p = new Node( val ) ; if ( head == null ) { head = p ; } else { tail.next = p ; } tail = p ; return true ; } public Object work( Action obj ) { return obj.act( head ) ; } } Ohne Klasse List zu verändern kann man zusätzliche Funktionalität einfügen … © H.Neuendorf (99) Strategy- Methoden : Aktionsobjekte als Parameter deren Methoden auf anderen Objekten arbeiten Dadurch besitzt Klasse Schnittstelle, an die beliebige Funktionalität PlugIn-artig übergeben werden kann class Sum implements Action { public Object act( Node h ) { Node p = h ; int summe = 0 ; while( p != null ) { summe = summe + p.val ; p = p.next ; } return new Integer( summe ) ; } } class Output implements Action { public Object act( Node h ) { Node p = h ; String str = null ; while( p != null ) { str = str + p.val + " " ; p = p.next ; } return str ; } } Strategy-Interfaces class UList { public static void main( String[ ] args ) { LinkedList MyList = new LinkedList( ) ; MyList.add( 17 ) ; MyList.add( 3 ) ; MyList.add( 6 ) ; class Sum implements Action { public Object act( Node h ) { Node p = h ; int summe = 0 ; while( p != null ) { summe = summe + p.val ; p = p.next ; } return new Integer( summe ) ; } } Sum su = new Sum( ) ; Integer summe = (Intreger) MyList.work( su ) ; Output out = new Output( ) ; String output = (String) MyList.work( out ) ; } } class Output implements Action { public Object act( Node h ) { Node p = h ; String str = null ; while( p != null ) { str = str + p.val + " " ; p = p.next ; } return str ; } } Verschiedene Aktionen ("konkrete Strategien") erfordern verschiedene Aktionsobjekte mit Methode korrekter Signatur, die über "PlugIn"Methode der Ziel-Klasse (hier: work() von LinkedList) aufgerufen wird. Implementierende Klassen sind typischerweise zustandslos (ohne Attribute), besitzen nur entsprechende Aktionsmethode. © H.Neuendorf Referenzen auf Aktionsobjekte als Ersatz für in Java nicht vorhandene Funktions- Pointer (100) (101) Generische Datenstruktur via Object class Node { // Generische Knotenklasse public Object val ; public Node next ; public Node( Object obj ) { val = obj ; next = null ; } } class LinkedList { private Node head = null ; private Node tail = null ; © H.Neuendorf Ein Datencontainer für verschiedene Datentypen ! Kann beliebige Objekte aufnehmen, da jede JavaKlasse implizit Unterklasse von Object ist Speichern einfacher Datentypen durch Hüllklassen Anm: Liste kann sich nicht selbst enthalten ! MyList.add( MyList ) Keine null-Werte als Datenelemente akzeptiert ! public boolean add( Object obj ) { if( obj==null ) return false ; Node p = new Node( obj ) ; if ( head == null ) { head = p ; } else { tail.next = p ; } tail = p ; return true ; } } Elemente vom generischen Typ Object : // LZ-StackOverflow LinkedList MyList = new LinkedList( ) ; int[ ] ary = { 1 , 6 , 8 , 3 } ; String s = "Test" ; Long Lg = new Long( 27 ) ; MyList.add( ary ) ; // geht alles – leider … MyList.add( s ) ; MyList.add( Lg ) ; // besser : Generics … Speichern spezieller Objekte // Nutzdatenklasse class Mitarbeiter { private String name ; public String getName( ) { return name ; } public Mitarbeiter( String n ) { name = n ; } } // Knotenklasse = Behälter für Mitarbeiter-Objekte class Node { private Mitarbeiter val ; public Node next ; public Node( Mitarbeiter obj ) { val = obj ; next = null ; } } class LinkedList { private Node head, tail ; public boolean addFirst( Mitarbeiter obj ) { if( obj==null ) return false ; Node p = new Node( obj ); if( tail == null ) tail = p; p.next = head ; head = p ; Keine null-Werte als Datenelemente return true ; akzeptiert ! } } © H.Neuendorf In Node Datenelemente vom Typ Mitarbeiter speicherbar (102) // Alternativ : Verkettungsfähige Mitarbeiterklasse // Verfügt über passendes next-Attribut class Mitarbeiter { private String name ; public Mitarbeiter next ; public String getName( ) { return name ; } public Mitarbeiter( String n ){ name = n ; } } class Linkedist { private Mitarbeiter head, tail ; public boolean addFirst( Mitarbeiter obj ) { if( obj == null ) return false ; Mitarbeiter p = obj ; if( tail == null ) tail = p ; p.next = head ; head = p ; return true ; } } Wie sehen die entstehenden Objekt-Strukturen aus ? (103) Speichern von Objekten in Listen - Referenzbild a) Mit separater Knotenklasse Node : Müller Haller Gross Mitarbeiter head List val next Node Jede Klasse kann durch entsprechendes next Attribut verkettungsfähig gemacht werden Vorteil : b) Mit verkettungsfähiger Mitarbeiter-Klasse : Keine separate Knoten-Klasse nötig Nachteil : Klasse muss angepasst werden head Müller next Mitarbeiter © H.Neuendorf Haller Gross List Beim Verwalten von Objekten ist Methode get( ) sinnvoll, die Referenz auf das Objekt gemäß Schlüsselwert (ID) = key zurückliefert (104) Generische Datentypen mittels Generics // Generische Klassen : Typ-Variablen / - Platzhalter : class Node<T> { public T val ; <T> Man vermeidet den total generischen, völlig unspezifizierten Typ Object und gibt Verwender die Möglichkeit zur flexiblen Typ-Vorgabe mit Typsicherheit : public Node<T> next ; public Node( T obj ) { val = obj ; Compiler kann bei konkreter Verwendung auf einen korrekten Typ prüfen ! next = null ; } } class ListUser { public static void main( String[] args) { class LinkedList<T> { private Node<T> head = null ; private Node<T> tail = null ; LinkedList<String> MyList = new LinkedList<String>( ) ; public boolean add( T obj ) { MyList.add( "dies geht" ) ; Node<T> p = new Node<T>( obj ) ; MyList.add( "dies auch" ) ; if ( head == null ) { head = p ; } MyList.add( new Long(27) ) ; // Compilerfehler !! else { tail.next = p ; } tail = p ; return true ; } } } } © H.Neuendorf Unterdrücken von null-Werten als Datenelemente noch hinzufügen .. Thema Generics wird separat behandelt … Datenstrukturen Bäume als dynamische Datenstrukturen © H.Neuendorf (118) Baumstrukturen (119) Hierarchische Datenstruktur Baum = Verzweigte Struktur aus Knoten + Folge von Nachfolgern (≠ Graph) Binärbaum : Jeder Knoten hat maximal zwei Nachfolger Jeder Knoten hat einen Vorgänger – mit Ausnahme Wurzelknoten Wurzelknoten (root) Ebene 0 innere Knoten Ebene 1 Ebene 2 Ebene 3 Blätter (Blattknoten) = Elemente ohne Nachfolger Tiefe T = Zahl der Ebenen = 4 Gewicht = Zahl der Knoten = 9 © H.Neuendorf Zu jedem Knoten führt ausgehend von Wurzel ein eindeutiger Weg Baum ist voll, wenn außer letzter Ebene alle seine Ebenen komplett besetzt sind : Ebene k → max. 2 k - Elemente Binärbaum ist streng sortiert, wenn für jeden Knoten und dessen beide Unterbäume gilt : 1. Alle Knoten im linken Unterbaum haben kleinere Schlüssel und … 2. Alle Knoten im rechten Unterbaum haben größere Schlüssel … … als ihr Vorgängerknoten Motivation : Effiziente Suche Hierarchische Datenrepräsentation (120) Binäre Baumstrukturen : Sortierter Suchbaum Wurzel x Bei einer sortierten verketteten Liste ist eine binäre Suche (im Gegensatz zu Arrays) nicht sinnvoll implementierbar – deshalb Übergang zum Binärbaum … Hier sind alle Elemente < x Suchvorgang Hier sind alle Elemente > x linker Unterbaum rechter Unterbaum Bsp: Mitarbeiterobjekte gemäß Personalnummern An jeder Verzweigung (beginnend mit Wurzelknoten) wird geprüft : 1. Gesuchtes Element == Knoten-Wert Suche erfolgreich beendet, gefunden ! 2. Gesuchtes Element > Knoten-Wert Suche im rechten Unterbaum fortsetzen falls Knoten keinen rechten Nachfolger besitzt : fertig - Misserfolg 3. Gesuchtes Element < Knoten-Wert Suche im linken Unterbaum fortsetzen falls Knoten keinen linken Nachfolger besitzt : fertig - Misserfolg Fahre fort, bis Element gefunden - oder kein entsprechender Teilbaum existiert. Ausgeglichener (balancierter) voller Baum hat Elementzahl : Anzahl Ebenen T somit : © H.Neuendorf n = 2 T- 1 T = ld( n +1 ) = Anzahl erforderlicher Suchschritte (121) Effizientes Suchen in sortierten Binärbäumen Beim Durchlauf reduziert sich an jeder Verzeigung die Anzahl noch zu prüfender Elemente Deutlich weniger zu durchlaufende Einträge als bei linearen Strukturen ! Wurzelknoten (root) Gute Bäume : 9 Regelmäßige Verteilung der n Elemente auf die einzelnen Ebenen 5 15 Ebene 1 Ebenen möglichst voll besetzt Tiefe möglichst klein : T = ld( n+1 ) 3 1 12 Ebene 2 19 4 32 Maximale Gesamtknotenzahl n = 2T - 1 Tiefe eines ausgeglichenen vollen Baums : T = ld( n + 1) Vergleich lineare Liste : "Tiefe" = n Zu durchsuchende Elemente im Mittel : lineare Liste = n / 2 © H.Neuendorf Baum = ld( n + 1 ) Ebene 3 Suchvorgang sehr effektiv Schlechte Bäume : Unausgeglichene Verteilung der n Elemente auf einzelne Ebenen Ebenen schwach besetzt Tiefe sehr groß : Extremfall = n Entartung des Baums zur linearen Liste Suchen nicht effektiver als bei Listen (122) Effizientes Suchen in sortierten Binärbäumen Suchvorgang = Durchlaufen des Baums von Wurzel bis Aufwandsvergleich im Mittel: zum gesuchten Knoten oder Blatt Einträge Liste Suchbaum 7 3.5 3 31 15.5 5 Suchwert = Knotenwert Gesuchter Wert gefunden! 127 63.5 7 Suchwert > Knotenwert weiter im rechten Teilbaum 1023 511.5 10 Suchwert < Knotenwert weiter im linken Teilbaum 16383 8191.5 14 Verzweigung: An jedem Knoten wird gesuchter Schlüssel mit Schlüssel / Wert des Knotens verglichen : class Knoten { Darstellung Binärbäume : public int val ; ähnlich wie bei Listen public Knoten links, rechts ; → Baum besteht aus Knoten → Knoten besteht aus : a) Inhalt ( Wert, Objekt = val ) public Knoten( int n ) { + val = n ; b) zwei Nachfolgern : links = null ; → wiederum vom Typ Knoten ( links, rechts ) rechts = null ; Selbstbezügliche Datenstruktur Jeder Knoten hat zwei Nachfolger // bzw. Object } } // evtl. als innere Klasse © H.Neuendorf (123) Realisierung Binärbaum durch Knoten-Objekte Objekt Baum Maximale Objekt val: 8 Knoten Wurzel links Gesamtknotenzahl : n = 2 T- 1 rechts Attribut Wurzel zeigt auf root key Objekt val: 4 Knoten links Objekt val:11 Knoten Weitere Attribute und Methoden Nutzdaten rechts links rechts val null Objekt val: 2 Knoten Objekt val: 6 Knoten Objekt val: 9 Knoten links links links null © H.Neuendorf rechts null null rechts null null rechts null Allgemeiner Fall : Es werden von den Knoten Objekte refereziert, die über eindeutigen key (Such-/ Ordnungskriterium) verfügen. Sortierter Binärbaum - Schnittstellenvarianten Schnittstellen bzw. zu speichernde Objekte können unterschiedlich konzipiert sein – zB : boolean add( int data ) Triviale Demo – es werden primitive Werte (da direkt vergleichbar) in Baumstruktur gehalten. boolean add( Comparable key, Object data ) Es werden beliebige Objekte gespeichert. Diesen ist Suchschlüssel-Objekt zugeordnet. Die keys müssen eine totale Ordnungsrelation erfüllen (>,<,==). Die keys müssen nicht mit den Objekteigenschaften korreliert sein. Durch Vorgabe des keys kann man zugeordnetes Objekt finden. Einfachste keys könnten jedoch auch primitive Werte sein. boolean add( Comparable data ) Die zu speicherden Objekte selbst erfüllen eine Ordnungsrelation (>,<,==), dienen praktisch selbst als keys. Kein separater key nötig. Nachteilig beim Suchen – man muss das zu suchende Objekt im Grunde schon haben … boolean add( IFKey data ) Die Objekte besitzen ein typisches key-Attribut (zB Mitarbeiter-ID) und eine vordefinierte Abfragemethode, zB int getKey(), evtl. gefordert durch ein Interface. Somit können Objekte verglichen, angeordnet und gefunden werden. JDK: java.util.TreeSet boolean add( Comparable data ) © H.Neuendorf (124) Iterativer Aufbau Sortierter Binärbaum (125) Durchlaufen Baumknoten ab Wurzel : → Vergleich einzufügenden Werts mit Wert des aktuellen Knotens Neuer Wert < Knotenwert links davon fortsetzen Andernfalls rechts davon fortsetzen … class Tree { // Naiver Algorithmus ! private Knoten root ; public Tree( ) { root = null ; } // Baum anfangs leer public boolean add( int data ) { if ( root == null ) { root = new Knoten( data ) ; return true ; } Knoten k = root ; // "Baumcursor" k Knoten prev ; boolean links; do { prev = k ; if( data = = k.val ) { return false ; } // Wert bereits vorhanden ! links = true ; } if ( data < k.val ) { k = k.links ; else { k = k.rechts ; links = false ; } Iteration beendet, wenn man auf } while( k != null ) ; Blatt-Knoten stößt : k wird null if ( links == true ) { prev.links = new Knoten( data ) ; } An diesen Knoten wird neues Blatt else { prev.rechts = new Knoten( data ) ; } angehängt - links oder rechts, je return true; nach Wert } } © H.Neuendorf (126) Rekursiver Aufbau Sortierter Binärbaum Durchlaufen Baumknoten ab Wurzel : → Vergleich des einzufügenden Inhalts (Wert) mit Wert des betrachteten Knotens Neuer Wert < Knotenwert Links davon einfügen Andernfalls Rechts davon einfügen Eleganter - aber doch langsamer als iterative Formulierung ! class Tree { // Naiver Algorithmus ! private Knoten root ; public Tree( ) { root = null ; } // Baum anfangs leer public boolean add( int data ) { root = add( data, root ) ; return true; } private Knoten add( int data, Knoten k ) { if ( k == null ) { return new Knoten( data ) ; } else { if ( data < k.val ) { k.links = add( data, k.links ) ; else { k.rechts = add( data, k.rechts ) ; return k ; } return k ; } } } } © H.Neuendorf Rekursion beendet, wenn man auf Blatt-Knoten stößt An diesen Knoten wird neues Blatt angehängt - links oder rechts - je nach Wert Iterative Suche von Elementen in Sortiertem Binärbaum Durchlaufen der Baumknoten ab Wurzel : Wenn aktueller Knoten gesuchten Inhalt enthält, wird true zurückgeliefert Wenn nicht in richtigem Unterbaum iterativ Suche fortsetzen class Tree { private Knoten root ; public Tree( ) { root = null ; } public boolean contains( int data ) { Knoten k = root ; // Beginn der Suche an der Wurzel while( k!= null ) { if( k.val == data ) return true ; // Element gefunden! else{ if( k.val > data ) k = k.links ; else // im linken Teilbaum weitersuchen! k = k.rechts ; // im rechten Teilbaum weitersuchen! } } return false ; } } © H.Neuendorf Iteration endet, wenn Element gefunden, oder man Baumenden erreicht - dort nimmt k Wert null an. (127) Rekursives Durchlaufen des Sortierten Binärbaums Durchlaufen aller Baumknoten : (zB Ausgabe der Inhalte) Erst wird linker Unterbaum eines Knotens k durchlaufen Dann wird Knoten k selbst durchlaufen / bearbeitet / ausgegeben / … Dann wird rechter Unterbaum von k durchlaufen class Tree { Rekursion beendet, wenn man auf Knoten ohne Nachfolger stößt private Knoten root ; public Tree( ) { root = null ; } public void durchlaufe( ) { durchlaufe( root ) ; } private void durchlaufe( Knoten k ) { if ( k != null ) { durchlaufe( k.links ) ; IO.writeln( k.val ) ; // Wert verarbeiten durchlaufe( k.rechts ) ; } } } © H.Neuendorf hier → willkürliche Reihenfolge : links / Knoten / rechts Natürlich auch andere Reihenfolgen der Berabeitung möglich : preorder inorder postorder Traversieren (129) (130) Sortierter Binärbaum - Durchlauf-Arten Reihenfolge, in der Unterbäume des Baums durchlaufen + Knoten aufgesucht werden Tiefendurchlauf : Ausgehend von Knoten k wird ein Unterbaum von k vollständig durchlaufen, ehe man zum nächsten Unterbaum von k übergeht preorder → Knoten k selbst wird vor seinen Unterbäumen ausgegeben inorder → Knoten k selbst wird zwischen seinen Unterbäumen ausgegeben postorder → Knoten k selbst wird nach seinen Unterbäumen ausgegeben public void preorder( Knoten k ) { if( k =! null ) { IO.writeln( k.val ) ; preorder( k.links ) ; preorder( k.rechts ) ; } } public void inorder( Knoten k ) { if( k =! null ) { inorder( k.links ) ; IO.writeln( k.val ) ; inorder( k.rechts ) ; } } public void postorder( Knoten k ) { if( k =! null ) { postorder( k.links ) ; postorder( k.rechts ) ; IO.writeln( k.val ) ; } } © H.Neuendorf // 1. Knoten k bearbeiten // 2. Linker Unterbaum // 3. Rechter Unterbaum // 1. Linker Unterbaum // 2. Knoten k bearbeiten // 3. Rechter Unterbaum // 1. Linker Unterbaum // 2. Rechter Unterbaum // 3. Knoten k bearbeiten Methoden unterscheiden sich nur in Reihenfolge der rekursiven Aufrufe relativ zur Bearbeitung des Knotens k selbst ! Statt Wertausgabe könnte irgend eine Operation an Knoten k durchgeführt werden (131) Durchlaufreihenfolge Sortierter Binärbäume Start 1. 2. 3. 4. Ende 9. 6. 7. 8. 4. 8. 3. 5. 9. Ende 1. Preorder-Durchlauf 7. 2. Start Bewirkt "von oben nach unten" Durchlauf ("außen herum") 5. 6. Postorder-Durchlauf Bewirkt "von unten nach oben" Durchlauf 5. Bsp: Hierachie Dateisystem 4. 2. Start 1. 7. 6. 8. 3. Inorder-Durchlauf 9. Ende Bewirkt in sortierten Bäumen eine sortierte Durchwander-Folge ! © H.Neuendorf Dagegen Breitendurchlauf durch Levelorder-Durchlauf realisiert → Saake & Sattler, Kap.14 Sortierter Binärbaum – Objekte mit totaler Ordnung (132) Vergleichsoperation implementiert – Baum mit beliebigen Objekten vom Typ Comparable class Mitarbeiter implements Comparable { // aus java.lang : private int persNr ; public interface Comparable { private String name ; // Vergleichskriterium : // ... public Mitarbeiter( int pn, String nn ) { public int compareTo( Object o ) ; persNr = pn ; } name = nn ; } public int compareTo( Object o ) { // Sort nach persNr Mitarbeiter that = (Mitarbeiter)o ; if( this.persNr < that.persNr ) return -1 ; if( this.persNr > that.persNr) return +1 ; return 0 ; } } Methode compareTo( ) beschreibt totale Ordnung für Mitarbeiter-Objekte gemäß Kriterium persNr : m1.compareTo( m2 ) < 0 wenn m1.persNr < m2.persNr m1.compareTo( m2 ) > 0 wenn m1.persNr > m2.persNr m1.compareTo( m2 ) = 0 wenn m1.persNr = m2.persNr © H.Neuendorf Anpassen der Klassen Knoten und Baum … Sortierter Binärbaum – Objekte mit totaler Ordnung (133) class Tree { private Knoten root ; private class Knoten { Comparable val ; Knoten links, rechts ; } public Tree( ) { root = null ; } public boolean contains( Comparable data ) { Dieser Klasse Tree ist konkreter Typ der gespeicherten Objekte egal. Voraussetzung ist nur deren Vergleichbarkeit gemäß IF Comparable … Statt Typ Object wird deshalb der Typ Comparable verwendet. Knoten k = root ; // Beginn Suche an Wurzel while( k != null ) { int cmp = data.compareTo( k.val ) ; if( cmp==0 ) return true ; if( cmp<0 ) k = k.links ; else k = k.rechts ; } return false ; } } © H.Neuendorf Noch mehr Typsicherheit durch Verwendung von Generics … Ausgeglichene Bäume (134) Ziel bei Einfügen von n Elementen : Minimale Suchkomplexität Baum mit gleichmäßig verteilten Zweigen und Blättern ! minimale Tiefe ≈ ld(n) optimale Struktur für Suchoperationen! O( ld( n ) ) Problem : Eintragen von Werten in ungeordneter Folge : liefert ausgeglichenen Baum Eintrag einer sortierten Folge von Werten : liefert entarteten Baum ! alle Referenzen links (rechts) sind null (keine Einträge) alle Referenzen rechts (links) bilden verkettete Liste → Baum zu linearer verketteter Liste entartet ! Tiefe des Baumes ist n Suchen hat Zeitkomplexität O( n ) >> O( ld( n ) ) Lösung : Aufwendigere Routinen zum Aufbau + Umsortieren von Bäumen Löschen von Baumknoten : Vorlesung A&D (AVL- und B-Bäume) Literatur : Saake, Sattler "Algorithmen & Datenstrukturen" dpunkt → erfordert Umsortieren des Baums (Rotationen), wenn innere Knoten entfernt werden (nur für Blattknoten einfach: Referenz des jeweiligen Elternknotens auf null setzen) © H.Neuendorf Ausgeglichene Bäume "P" Fall 2: "A" (135) Ausgeglichener Baum null "Y" "C" "C" null null null null "P" Entartung zur linearen Liste null null null null → Baumstruktur hängt von Einfügereihenfolge ab Fall 1: Eingabereihenfolge in sortierter Folge "W" "A" "C" "N" "P" "W" "Y" null Fall 2: Eingabereihenfolge in Zufallsfolge "P" "Y" "C" "W" "A" "N" "Y" null © H.Neuendorf null "W" Problem beim Einfügen von Elementen null Fall 1: "N" "A" "N" null