10.07.2015 Aufrufe

Algorithmen und Datenstrukturen - Fachgebiet Theoretische ...

Algorithmen und Datenstrukturen - Fachgebiet Theoretische ...

Algorithmen und Datenstrukturen - Fachgebiet Theoretische ...

MEHR ANZEIGEN
WENIGER ANZEIGEN

Erfolgreiche ePaper selbst erstellen

Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.

<strong>Algorithmen</strong> <strong>und</strong> <strong>Datenstrukturen</strong>Skript zur VorlesungDieter Hofbauer <strong>und</strong> Friedrich OttoFB Elektrotechnik/Informatik <strong>und</strong> FB Mathematik/InformatikUniversität Kassel


VorwortEffiziente <strong>Algorithmen</strong> <strong>und</strong> <strong>Datenstrukturen</strong> sind ein zentrales Thema der Informatik.Man macht sich leicht klar, dass ein enger Zusammenhang bestehtzwischen der Organisation von Daten (ihrer Strukturierung) <strong>und</strong> dem Entwurfvon <strong>Algorithmen</strong>, die diese Daten bearbeiten.In dieser Vorlesung werden <strong>Algorithmen</strong> für eine Reihe gr<strong>und</strong>legender Aufgaben<strong>und</strong> die dabei verwendeten <strong>Datenstrukturen</strong> vorgestellt <strong>und</strong> analysiert. Für diemeisten der <strong>Algorithmen</strong> wird zudem eine konkrete Implementierung in derProgrammiersprache Java angegeben.Wichtige Informationen zur Lehrveranstaltung werden unterhttp://www.theory.informatik.uni-kassel.de/~dieter/algo/bereitgestellt, unter anderem die Übungsaufgaben, die Programme <strong>und</strong> dasSkript selbst.Kassel, April 20023


Inhaltsverzeichnis1 Einleitung 71.1 <strong>Datenstrukturen</strong> <strong>und</strong> ihre Spezifikation . . . . . . . . . . . . . . . 71.2 Einige elementare <strong>Datenstrukturen</strong> . . . . . . . . . . . . . . . . . 171.3 Einige einfache strukturierte Datentypen . . . . . . . . . . . . . . 181.3.1 Keller (Stacks) . . . . . . . . . . . . . . . . . . . . . . . . 181.3.2 Schlangen (Queues) . . . . . . . . . . . . . . . . . . . . . 241.3.3 Einfach verkettete Listen . . . . . . . . . . . . . . . . . . 291.3.4 Stacks über Listen implementieren . . . . . . . . . . . . . 351.3.5 Queues über Listen implementieren . . . . . . . . . . . . . 371.3.6 Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . 381.4 Rechenzeit- <strong>und</strong> Speicherplatzbedarf von <strong>Algorithmen</strong> . . . . . . 391.5 Asymptotisches Wachstumsverhalten . . . . . . . . . . . . . . . . 401.6 Die Java-Klassenbibliothek . . . . . . . . . . . . . . . . . . . . . 422 Bäume <strong>und</strong> ihre Implementierung 452.1 Die Datenstruktur Baum . . . . . . . . . . . . . . . . . . . . . . 462.2 Implementierung binärer Bäume . . . . . . . . . . . . . . . . . . 492.3 Erste Anwendungen binärer Bäume . . . . . . . . . . . . . . . . . 702.3.1 TREE SORT . . . . . . . . . . . . . . . . . . . . . . . . . 702.3.2 HEAP SORT . . . . . . . . . . . . . . . . . . . . . . . . . 722.4 Darstellungen allgemeiner Bäume . . . . . . . . . . . . . . . . . . 795


6 INHALTSVERZEICHNIS3 Datentypen zur Darstellung von Mengen 853.1 Mengen mit Vereinigung, Schnitt <strong>und</strong> Differenz . . . . . . . . . . 853.2 Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 913.3 Gewichtsbalancierte Bäume . . . . . . . . . . . . . . . . . . . . . 983.4 Höhenbalancierte Bäume . . . . . . . . . . . . . . . . . . . . . . . 1143.4.1 AVL-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . 1153.4.2 (2,4)-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . 1223.5 Hashing (Streuspeicherung) . . . . . . . . . . . . . . . . . . . . . 1253.6 Partitionen von Mengen mit UNION <strong>und</strong> FIND . . . . . . . . . . 1404 Graphen <strong>und</strong> Graph-<strong>Algorithmen</strong> 1494.1 Gerichtete Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . 1504.1.1 Traversieren von Graphen . . . . . . . . . . . . . . . . . . 1584.1.2 Kürzeste Wege . . . . . . . . . . . . . . . . . . . . . . . . 1654.1.3 Starke Komponenten . . . . . . . . . . . . . . . . . . . . . 1724.2 Ungerichtete Graphen . . . . . . . . . . . . . . . . . . . . . . . . 1784.2.1 Minimale aufspannende Wälder . . . . . . . . . . . . . . . 1785 Sortieralgorithmen 1875.1 Elementare Sortieralgorithmen . . . . . . . . . . . . . . . . . . . 1875.2 Sortierverfahren mit Divide-and-Conquer . . . . . . . . . . . . . 1895.3 Sortieren durch Fachverteilen . . . . . . . . . . . . . . . . . . . . 1965.4 Sortierverfahren im Vergleich . . . . . . . . . . . . . . . . . . . . 197


Kapitel 1Einleitung1.1 <strong>Datenstrukturen</strong> <strong>und</strong> ihre Spezifikation<strong>Algorithmen</strong> <strong>und</strong> <strong>Datenstrukturen</strong> sind untrennbar miteinander verknüpft. EinAlgorithmus realisiert eine Funktion, <strong>und</strong> er wird selbst wiederum durch einProgramm realisiert. Auf der Seite der Daten entsprechen diesen Begriffen dieAlgebra (d.h. ein konkreter Datentyp), die Datenstruktur, die eine Implementierungeiner Algebra ist, <strong>und</strong> die programmiertechnischen Konzepte der Klasse,des Moduls oder des Typs. Der Zusammenhang zwischen diesen Begriffen lässtsich graphisch wie folgt veranschaulichen [Güting, Abb. 1.1]:Abstrakter DatentypMathematikSpezifikationFunktionAlgebra (Datentyp)ImplementierungAlgorithmikSpezifikationAlgorithmusDatenstrukturImplementierungProgrammierungProgramm, ProzedurFunktionTyp, Modul, KlasseDabei werden wir uns überwiegend mit der mittleren Ebene dieses Diagrammsbefassen. Die folgenden Beispiele sollen das obige Diagramm ein wenig erläutern,wobei sich Beispiel 1.1.1 auf die linke <strong>und</strong> Beispiel 1.1.2 auf die rechte Spaltein obigem Diagramm bezieht.7


8 KAPITEL 1. EINLEITUNGBeispiel 1.1.1. Aufgabenstellung: Sei S eine endliche Menge von ganzen Zahlen.Stelle fest, ob eine gegebene Zahl in S enthalten ist!Diese ”informelle“ Beschreibung kann auf verschiedene Weisen formalisiert werden.Was soll gemacht werden? Mathematische Formulierung, Spezifikation:Die Funktion contains : V(Z) × Z → {true, false} mit{true falls c ∈ S,contains(S, c) =false sonst,soll berechnet werden (V(M) bezeichnet die Menge aller endlichen (engl. finite)Teilmengen einer Menge M).Wie kann dies geschehen? Formulierung eines Algorithmus; mehr oder wenigerformal, hier meist als (Pseudo-)Java-Programm:1 /** Test, ob die ganze Zahl c in der Menge S enthalten ist.2 * Dabei ist S ein Array ueber dem Typ int.3 */4 contains( int[ ] S, int c ) { contains5 for( int i = 0; i < S.length; i++ ) {6 if( S[i] == c )7 return true;8 }9 return false;10 }Praktische Realisierung, hier als Java-Programm:1 /** Die Klasse Menge1 implementiert Mengen ganzer Zahlen. */2 public class Menge1 {34 /** Die Menge als Array ueber dem Typ int. */5 private int[ ] array;67 /** Konstruiert eine Menge aus einem Array. */8 public Menge1( int[ ] array ) { Menge19 this.array = array;10 }1112 /** Test, ob die Zahl c in der Menge enthalten ist. */13 public boolean contains( int c ) { contains14 for( int i = 0; i < array.length; i++ ) {15 if( array[i] == c )16 return true;17 }18 return false;19 }


1.1. DATENSTRUKTUREN UND IHRE SPEZIFIKATION 92021 public static void main( String[ ] args ) { main22 Menge1 S = new Menge1( new int[ ] { 5, 8, 42 } );23 System.out.println( S.contains( 8 ) ); // true24 System.out.println( S.contains( 9 ) ); // false25 }26 } // class Menge1Beispiel 1.1.2. Aufgabenstellung: Verwalte eine Menge von Objekten (ganzeZahlen), so dass Objekte eingefügt oder entfernt werden können <strong>und</strong> der Testauf Enthaltensein durchgeführt werden kann!Angabe der Signatur: Drei Datenmengen spielen hier eine Rolle, nämlich dieganzen Zahlen, die endlichen Teilmengen der ganzen Zahlen <strong>und</strong> die Wahrheitswerte(Boolesche 1 Werte). Für jede dieser Menge wählen wir ein Sortensymbol,hierinteger, intset, boolean.Zur Angabe der Signatur gehört nun noch, Symbole für jede der Operationen(synonym: Funktionen) festzulegen, die sog. Operationssymbole, sowie diejeweiligen Argumentsorten <strong>und</strong> die Zielsorte anzugeben:EMPTY : → intsetINSERT : intset × int → intsetDELETE : intset × int → intsetCONTAINS : intset × int → booleanISEMPTY : intset → booleanDie Signatur gibt somit die Syntax unseres Datentyps an. Die Semantik kannnun abstrakt (durch eine Menge von Axiomen, vgl. die Beispiele 1.1.3 <strong>und</strong> 1.1.5)oder konkret durch eine spezielle Algebra angegeben werden.Angabe einer Algebra: Dazu müssen konkrete Datenmengen <strong>und</strong> Operationenangegeben werden. Jedes Symbol der zugehörigen Signatur wird interpretiert,im Beispiel etwa wie folgt. Wir wählen die Menge Z als Interpretation desSortensymbols int, die Menge V(Z) als Interpretation des Symbols intset <strong>und</strong>die Menge {true, false} als Interpretation von boolean. Die OperationssymboleEMPTY, INSERT, DELETE, CONTAINS bzw. ISEMPTY werden durch die1 Nach George Boole (1815-1864)


10 KAPITEL 1. EINLEITUNGfolgenden Operationen interpretiert (M ∈ V(Z), c ∈ Z):empty = ∅,insert(M, c) = M ∪ {c},delete(M, c) = M \ {c},{true falls c ∈ M,contains(M, c) =false sonst,{true falls M = ∅,isEmpty(M) =false sonst.Wie man unschwer verifiziert, erfüllen die Operationen die Vorgaben der obigenSignatur bezüglich der Argument- <strong>und</strong> Zielmengen.Algorithmische Realisierung: Man wählt eine Realisierung für die Datenmengen<strong>und</strong> gibt dann entsprechende <strong>Algorithmen</strong> an, die die obigen Operationenrealisieren (vgl. Beispiel 1.1.1). Hier eine mögliche Implementierung inJava für unser Beispiel:1 /** Die Klasse Menge2 implementiert Mengen ganzer Zahlen. */2 public class Menge2 {34 /** Die maximale Groesse der Menge. */5 private static final int MAX CARDINALITY = 10;67 /** Die Menge als Array ueber dem Typ int. */8 private int[ ] array = new int[MAX CARDINALITY];910 /** Die Groesse der Menge. */11 private int size;1213 /** Konstruiert eine leere Menge. */14 // Die Angabe des parameterlosen Konstruktors ist hier red<strong>und</strong>ant.15 public Menge2( ) { Menge216 }1718 /** Liefert eine neue leere Menge zurueck. */19 public static Menge2 empty( ) { empty20 return new Menge2( );21 }2223 /** Fuegt die Zahl c in die Menge ein. */24 public void insert( int c ) { insert25 if( contains( c ) ) // Element bereits vorhanden26 return;27 if( size == MAX CARDINALITY ) // Menge ist bereits voll28 throw new IllegalStateException( "Menge ist bereits voll." );29 array[size] = c;30 size++;31 }32


1.1. DATENSTRUKTUREN UND IHRE SPEZIFIKATION 1133 /** Entfernt die Zahl c aus der Menge, falls vorhanden. */34 public void delete( int c ) { delete35 int i = 0;36 while( array[i] != c && i < size ) // suche c37 i++;38 if( i < size ) { // falls c in der Menge enthalten ist:39 for( int j = i; j < size; j++ )40 array[j] = array[j+1]; // fuelle die neue Luecke41 size−−;42 }43 }4445 /** Test, ob die Zahl c in der Menge enthalten ist. */46 public boolean contains( int c ) { contains47 for( int i = 0; i < MAX CARDINALITY; i++ ) {48 if( array[i] == c )49 return true;50 }51 return false;52 }5354 /** Test, ob die Menge leer ist. */55 public boolean isEmpty( ) { isEmpty56 return size == 0;57 }5859 /** Gibt eine String-Repraesentation der Menge zurueck. */60 public String toString( ) { toString61 StringBuffer ausgabe = new StringBuffer( );62 for( int i = 0; i < size ; i++ )63 ausgabe.append( array[i] + " " );64 return ausgabe.toString( );65 }6667 public static void main( String[ ] args ) { main68 Menge2 S = empty( );69 System.out.println( S.isEmpty( ) ); // true70 S.insert( 5 );71 System.out.println( S.isEmpty( ) ); // false72 S.insert( 8 );73 S.insert( 42 );74 System.out.println( S ); // 5 8 4275 S.delete( 9 );76 System.out.println( S ); // 5 8 4277 S.delete( 8 );78 System.out.println( S ); // 5 4279 S.delete( 5 );80 System.out.println( S ); // 4281 S.delete( 42 );82 System.out.println( S.isEmpty( ) ); // true83 }84 } // class Menge2


12 KAPITEL 1. EINLEITUNGWir sehen an diesen Beispielen, wie sich Spezifikation <strong>und</strong> Implementierung von<strong>Algorithmen</strong> <strong>und</strong> <strong>Datenstrukturen</strong> gegenseitig beeinflussen.Bei der Implementierung einer Datenstruktur werden wir stets zwei Stufen unterscheiden:• ”Definitionsmodul“ oder ”Interface“: Dies ist die Schnittstelle zu den Anwendungsprogrammen.Hierin wird die Signatur (Syntax) der Datenstrukturfestgelegt. Alle Anwendungen können nur die hier eingeführten Operationenverwenden, um Objekte der Struktur zu bearbeiten.• ”Implementierungsmodul“: Hierin wird eine Datenstruktur realisiert. DieDetails sind nicht nach außen sichtbar.In Java spiegelt sich die Signatur in den Methodenköpfen <strong>und</strong> den Typdeklarationender Felder.Für die Verwendung einer Datenstruktur müssen Anwender neben der Syntaxnatürlich auch die Semantik dieser Datenstruktur kennen. Diese wird durchdie Implementierung festgelegt, was natürlich unbefriedigend ist. Daher wirdim Allgemeinen die ”angestrebte Semantik“ formal (durch Axiome) oder halbformal beschrieben ( ”spezifiziert“), <strong>und</strong> man verlangt, dass die Implementierungdieser Spezifikation entspricht ( ”Korrektheit der Implementierung“). Wir gebenhierzu einige einfache Beispiele an.Beispiel 1.1.3. Die Datenstruktur Boole stellt die Booleschen Werte true <strong>und</strong>false zur Verfügung sowie einige logische Operationen wie Negation <strong>und</strong> Konjunktion.Eine algebraische Spezifikation dieser Struktur könnte so aussehen:Über der SignaturTRUE : → booleanFALSE : → booleanNOT : boolean → booleanAND : boolean × boolean → booleanOR : boolean × boolean → booleanmit dem Sortensymbol boolean schreiben wir die folgenden Gleichungen (x <strong>und</strong>y sind Variablen zur Sorte boolean):NOT(TRUE) = FALSENOT(FALSE) = TRUEAND(x, TRUE) = xAND(x, FALSE) = FALSEOR(x, y) = NOT(AND(NOT(x), NOT(y)))Mit diesen Gleichungen kann man rechnen, indem man sie als Ersetzungsregeln


1.1. DATENSTRUKTUREN UND IHRE SPEZIFIKATION 13verwendet:OR(x, TRUE) = NOT(AND(NOT(x), NOT(TRUE)))= NOT(AND(NOT(x), FALSE))= NOT(FALSE)= TRUEAuf diese Weise gelangt man zu neuen Gleichungen, hier OR(x, TRUE) =TRUE, die in allen Modellen der Spezifikation gelten. Solche Gleichungen sindlogische Folgerungen aus den Axiomen. Andere Gleichungen, im Beispiel etwaNOT(NOT(x)) = x, sind keine logischen Konsequenzen der Axiome; sie geltenaber im sog. initialen Modell der Gleichungmenge, einer Art ”Standardmodell“.Dieses Thema wollen wir hier aber nicht weiter vertiefen.Programm 1.1.4. Eine einfache Implementierung der Datenstruktur Boole:1 /** Die Klasse Boole implementiert boolesche Werte. */2 public class Boole {34 /** Der boolesche Wert, realisiert als eine nicht-negative Zahl5 * vom Typ int. Dabei entspricht der Wert 0 dem booleschen Wert6 * false <strong>und</strong> jeder Wert groesser 0 dem booleschen Wert true.7 */8 private int wert;910 /** Konstruiert ein Objekt vom Typ Boole mit int-Wert i. */11 public Boole( int i ) { Boole12 wert = Math.abs( i );13 }1415 /** Statische Felder fuer die Konstanten TRUE <strong>und</strong> FALSE. */16 public static final Boole TRUE = new Boole( 1 );17 public static final Boole FALSE = new Boole( 0 );1819 /** Gibt ein neues Objekt zurueck, dessen Wert die Negation20 * des Werts dieses Objekts ist.21 */22 public Boole not( ) { not23 return new Boole( wert == 0 ? 1 : 0 );24 }2526 /** Gibt ein neues Objekt zurueck, dessen Wert die Konjunktion27 * des Werts dieses Objekts <strong>und</strong> des Werts von b ist.28 */29 public Boole and( Boole b ) { and30 return new Boole( wert * b.wert );31 }3233 /** Gibt ein neues Objekt zurueck, dessen Wert die Disjunktion34 * des Werts dieses Objekts <strong>und</strong> des Werts von b ist.35 */36 public Boole or( Boole b ) { or37 return new Boole( wert + b.wert );38 }


14 KAPITEL 1. EINLEITUNG3940 /** Gibt eine String-Darstellung des Booleschen Werts zurueck. */41 public String toString( ) { toString42 return wert > 0 ? "true" : "false";43 }4445 public static void main( String[ ] args ) { main46 System.out.println( TRUE ); // true47 System.out.println( FALSE ); // false48 System.out.println( TRUE.not( ) ); // false49 System.out.println( FALSE.not( ) ); // true50 System.out.println( TRUE.and( TRUE ) ); // true51 System.out.println( FALSE.and( TRUE ) ); // false52 System.out.println( FALSE.or( FALSE ) ); // false53 System.out.println( FALSE.or( TRUE ) ); // true54 }5556 } // class BooleEine solche Implementierung der Datenstruktur Boole ist in der Praxis überflüssig,da fast jede Programmiersprache bereits eine analoge Datenstrukturmitbringt (in Java ist dies der Gr<strong>und</strong>typ boolean).Das Beispiel ist noch in einer anderen Hinsicht untypisch. Jede der BooleschenOperationen erzeugt hier nämlich ein neues Objekt; ein solches Vorgehen istmeist nicht angemessen, da hierbei viel Speicherplatz verschwendet wird. In derRegel wird man wie im nächsten Beispiel vorgehen <strong>und</strong> alternativ die bereitsvorhandenen Objekte manipulieren.Beispiel 1.1.5. Die Datenstruktur Nat enthält einige einfache Operationenüber den natürlichen Zahlen. Wollen wir Nat durch eine algebraische Spezifikationdefinieren, so gehen wir wie folgt vor. Wir erweitern die Spezifikation ausBeispiel 1.1.3 um das Sortensymbol nat, ergänzen die Signatur um<strong>und</strong> die Axiome umNULL : → natSUCC : nat → natISTNULL : nat → booleanADD : nat × nat → natEQ : nat × nat → booleanISTNULL(NULL) = TRUEISTNULL(SUCC(x)) = FALSEADD(NULL, y) = yADD(SUCC(x), y) = SUCC(ADD(x, y))EQ(x, NULL) = ISTNULL(x)EQ(NULL, SUCC(y)) = FALSEEQ(SUCC(x), SUCC(y)) = EQ(x, y)


1.1. DATENSTRUKTUREN UND IHRE SPEZIFIKATION 15Eine Beispielrechung:EQ(ADD(SUCC(NULL), SUCC(NULL)), SUCC(NULL))= EQ(SUCC(ADD(NULL, SUCC(NULL))), SUCC(NULL))= EQ(ADD(NULL, SUCC(NULL)), NULL)= ISTNULL(ADD(NULL, SUCC(NULL)))= ISTNULL(SUCC(NULL))= FALSEProgramm 1.1.6. Eine einfache Implementierung der Datenstruktur Nat:1 /** Die Klasse Nat implementiert natuerliche Zahlen. */2 public class Nat {34 /** Die natuerliche Zahl als Zahl vom Typ int, die nie negativ ist. */5 private int wert;67 /** Konstruiert die Zahl Null. */8 private Nat( ) { Nat9 wert = 0;10 }1112 /** Statische Methode zur Konstruktion der Zahl Null. */13 public static Nat Null( ) { Null14 return new Nat( );15 }1617 /** Der Wert dieses Objekts wird um eins inkrementiert. */18 public Nat succ( ) { succ19 wert++;20 return this;21 }2223 /** Test, ob der Wert null (0) ist. */24 public Boole istNull( ) { istNull25 return new Boole( wert ).not( );26 }2728 /** Zum Wert dieses Objekts wird n addiert. */29 public Nat add( Nat n ) { add30 wert += n.wert;31 return this;32 }3334 /** Test, ob der Wert gleich dem Wert von n ist. */35 public Boole eq( Nat n ) { eq36 return new Boole( wert − n.wert ).not( );37 }3839 /** Gibt die Zahl als String zurueck. */40 public String toString( ) { toString41 return String.valueOf( wert );42 }


16 KAPITEL 1. EINLEITUNG4344 public static void main( String[ ] args ) { main45 System.out.println( Null( ) ); // 046 System.out.println( Null( ).istNull( ) ); // true47 Nat zwei = Null( ).succ( ).succ( );48 System.out.println( zwei ); // 249 System.out.println( zwei.istNull( ) ); // false50 Nat vier = Null( ).succ( ).succ( ).succ( ).succ( );51 System.out.println( zwei.add( vier ) ); // 652 System.out.println( zwei.eq( vier ) ); // false53 System.out.println( vier.eq( zwei ) ); // false54 System.out.println( zwei.eq( zwei ) ); // true55 }5657 } // class NatBei der Beschreibung der Semantik muss man aufpassen, dass diese widerspruchsfrei<strong>und</strong> vollständig ist. Diese Forderungen sind oft nur schwer zu erfüllen(<strong>und</strong> noch schwerer zu verifizieren).Beispiel 1.1.7. Eine andere Spezifikation für eine Datenstruktur Boole erhaltenwir, wenn wir die Signatur aus Beispiel 1.1.3 beibehalten, die Axiomenmengeaber wie folgt modifizieren:Dann gilt beispielsweiseNOT(TRUE) = FALSENOT(FALSE) = TRUETRUE ≠ FALSENOT(NOT(x)) = xAND(x, FALSE) = FALSEAND(x, y) = NOT(OR(NOT(x), NOT(y)))OR(x, y) = OR(y, x)OR(x, TRUE) = xFALSE = AND(TRUE, FALSE)= NOT(OR(NOT(TRUE), NOT(FALSE)))= NOT(OR(NOT(TRUE), TRUE))= NOT(NOT(TRUE))= TRUE,was der Ungleichung TRUE ≠ FALSE widerspricht. Also ist die obige Spezifikationnicht widerspruchsfrei.


1.2. EINIGE ELEMENTARE DATENSTRUKTUREN 171.2 Einige elementare <strong>Datenstrukturen</strong>Gr<strong>und</strong>typen wie Boolesche Werte (boolean), Buchstaben (char) oder Zahlen(int, double etc.) werden im Speicher im Allgemeinen durch eine bestimmteAnzahl von Wörtern (Bytes) zu je acht Bits dargestellt. In Java ist diese Darstellung(im Gegensatz zu den meisten anderen Programmiersprachen) festgelegt:Typ Größe in Bits Wertebereichboolean 8 true, falsechar 16 ’\u0000’ bis ’uFFFF’ (0 bis 65535)byte 8 −2 7 bis 2 7 − 1short 16 −2 15 bis 2 15 − 1int 32 −2 31 bis 2 31 − 1long 64 −2 63 bis 2 63 − 1float 32 1.40239846e−45 bis 3.40282347e+38 (positiv)double 64 4.94065645841246544e−324 bis1.79769313486231570e+308 (positiv)Der Typ char entspricht ISO Unicode, die Fließkommatypen float <strong>und</strong> doubleentsprechen IEEE 754. Für die Speicherabbildung von ganzen Zahlen, hieram Beispiel des Typs int, wird der Wert im Binärcode in folgendem Formatdargestellt:2er-Komplement: z ≥ 0 : 0 bin(z)z < 0 :bin(2 32 − |z|)zum Beispiel: z = −1 : 1 1 1 . . . 1 1da bin(2 32 − 1) = 11 . . . 11 (32-mal) ist.Hier noch zwei weitere geläufige Speicherabbildungen (man beachte, dass hierdie Null jeweils zwei Darstellungen hat):Vorzeichen <strong>und</strong> Betrag: z : v bin(|z|)1er-Komplement: z ≥ 0 : 0 bin(z)z < 0 :bin(2 32 − 1 − |z|)z.B.: z = −1 : 1 1 1 . . . 1 0Die Speicherabbildung von Fließkommazahlen:v Exponent e Mantisse m


18 KAPITEL 1. EINLEITUNGstellt die Zahlz = (−1) v · 2 e · (1 + m)dar, wobei 0 ≤ m < 1 gilt <strong>und</strong> e eine ganze Zahl mit Vorzeichen ist. Bei derKodierung mit 32 Bits (float) benötigt das Vorzeichen 1 Bit, der Exponent 8Bits <strong>und</strong> die Mantisse 23 Bits; mit 64 Bits (double) benötigt das Vorzeichen 1Bit, der Exponent 11 Bits <strong>und</strong> die Mantisse 52 Bits.1.3 Einige einfache strukturierte DatentypenHier wollen wir folgende Datentypen vorstellen:• Keller (engl. Stacks),• Schlangen (engl. Queues),• verkettete Listen.1.3.1 Keller (Stacks)Ein Stack besteht aus einer Folge a 1 , . . . , a m von Elementen einer DatenmengeItem, wobei nur an einem Ende dieser Folge ( ”oben“) Elemente gelesen, gelöschtoder eingefügt werden können. Stacks sind daher auch unter dem Namen LIFO-Listen (Last-I n-F irst-Out) bekannt.a m← oben (top).a 2a 1Einige Anwendungen:• Auswertung von Ausdrücken in Postfix-Notation (siehe Beispiel 1.3.2)• Umwandlung von Ausdrücken in Infix-Notation in Postfix-Notation• Realisierung des Bindungskellers von Namen bei der syntaktischen Analysevon Programmen• Umwandlung von rekursiven Programmen in nicht-rekursive Programme• Backtracking-<strong>Algorithmen</strong>• Laufzeitstack bei der Speicherplatzverwaltung von Programmen


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 19Eine algebraische Spezifikation des Datentyps Stack kann wie folgt aussehen.Die Signatur mit den Sortensymbolen stack, boolean <strong>und</strong> item:Und die Axiome:EMPTY : → stackPUSH : stack × item → stackISEMPTY : stack → booleanMAKEEMPTY : stack → stackTOP : stack → itemPOP : stack → stackERROR1 : → itemERROR2 : → stackTRUE, FALSE : → booleanISEMPTY(EMPTY) = TRUEISEMPTY(PUSH(s, i)) = FALSEMAKEEMPTY(s) = EMPTYTOP(EMPTY) = ERROR1TOP(PUSH(s, i)) = iPOP(EMPTY) = ERROR2POP(PUSH(s, i)) = sDiese Spezifikation ist mit dem Sortensymbol item parametrisiert, das durcheine beliebige Menge interpretiert werden kann.Programm 1.3.1. Eine Implementierung der Datenstruktur Stack durch dieDatenstruktur Array:1 import java.util.NoSuchElementException; // zur Fehlerbehandlung2 import java.io.*; // Testroutine main benutzt I/O-Klassen34 /** Die Klasse Stack implementiert Stacks beschraenkter Groesse mittels Arrays. */5 public class Stack {67 /** Das Array. */8 private Object[ ] array;910 /** Die Position des Top-Elements des Stacks im Array. */11 private int topPosition;1213 /** Die maximale Laenge des Arrays. */14 private static final int MAX LAENGE = 10;15


20 KAPITEL 1. EINLEITUNG16 /** Konstruiert den leeren Stack mit maximaler Groesse MAX LAENGE. */17 public Stack( ) { Stack18 array = new Object[MAX LAENGE];19 topPosition = −1;20 }2122 /** Konstruiert den leeren Stack mit maximaler Groesse n. */23 public Stack( int n ) { Stack24 array = new Object[n];25 topPosition = −1;26 }2728 /** Test, ob der Stack leer ist. */29 public boolean isEmpty( ) { isEmpty30 return topPosition == −1;31 }3233 /** Macht den Stack leer. */34 public void makeEmpty( ) { makeEmpty35 topPosition = −1;36 }3738 /** Test, ob der Stack voll ist. */39 public boolean isFull( ) { isFull40 return topPosition == array.length−1;41 }4243 /** Fuegt ein neues Element oben in den Stack ein.44 * Ist der Stack voll, so wird eine Ausnahme ausgeloest.45 */46 public void push( Object inhalt ) { push47 if( isFull( ) )48 throw new IllegalStateException( "Stack ist voll." );49 topPosition++;50 array[ topPosition ] = inhalt;51 }5253 /** Gibt das oberste Element des Stacks zurueck, wenn vorhanden.54 * Ist der Stack leer, so wird eine Ausnahme ausgeloest.55 */56 public Object top( ) { top57 if( isEmpty( ) )58 throw new NoSuchElementException( "Stack ist leer." );59 return array[ topPosition ];60 }6162 /** Entfernt das oberste Element des Stacks, wenn vorhanden.63 * Ist der Stack leer, so wird eine Ausnahme ausgeloest.64 */65 public void pop( ) { pop66 if( isEmpty( ) )67 throw new NoSuchElementException( "Stack ist bereits leer." );68 topPosition−−;69 }


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 217071 /** Gibt das oberste Element des Stacks zurueck <strong>und</strong> entfernt es,72 * wenn vorhanden. Ist der Stack leer, so wird eine Ausnahme ausgeloest.73 */74 public Object topAndPop( ) { topAndPop75 if( isEmpty( ) )76 throw new NoSuchElementException( "Stack ist bereits leer." );77 Object topElement = array[ topPosition ];78 topPosition−−;79 return topElement;80 }8182 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen83 * der Stack-Elemente besteht, jeweils durch eine Leerzeile getrennt.84 * Die Reihenfolge ist von oben nach unten.85 */86 public String toString( ) { toString87 StringBuffer ausgabe = new StringBuffer( );88 for( int i = topPosition; i >= 0 ; i−− )89 ausgabe.append( array[i] + "\n" );90 return ausgabe.toString( );91 }9293 /** Eine kleine Testroutine, die ueber die Kommandozeile benutzt wird. */94 public static void main( String[ ] args ) throws IOException { main9596 System.out.println( "Ein Stack mit Eintraegen vom Typ String\n" +97 "kann ueber die Kommandozeile modifiziert werden.\n" +98 "\tVerfuegbare Kommandos:\n" +99 "\t\"e\" -- Stack leer machen (makeEmpty)\n" +100 "\t\"u\" -- Element oben einfuegen (push)\n" +101 "\t\"t\" -- Oberstes Element ausgeben (top)\n" +102 "\t\"o\" -- Oberstes Element loeschen (pop)\n" +103 "\t\"p\" -- Stack von oben nach unten ausgeben (print)\n" +104 "\t\"q\" -- beendet das Programm (quit)" );105 Stack stack = new Stack( );106 BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );107 char command = ’ ’;108 while( command != ’q’ ) {109 switch( command ) {110 case ’ ’ : { // Tue nichts111 break;112 }113 case ’e’ : { // Stack leer machen114 stack.makeEmpty( );115 break;116 }117 case ’u’ : { // String oben einfuegen118 System.out.println( "\tEinen String eingeben:" );119 stack.push( in.readLine( ) );120 break;121 }


22 KAPITEL 1. EINLEITUNG122 case ’t’ : { // Oberstes Element ausgeben123 try {124 System.out.println( "Oberstes Element: " + stack.top( ) );125 }126 catch( NoSuchElementException e ) { System.out.println( e ); }127 break;128 }129 case ’o’ : { // Oberstes Element loeschen130 try {131 stack.pop( );132 }133 catch( NoSuchElementException e ) { System.out.println( e ); }134 break;135 }136 case ’p’ : { // Stack von oben nach unten ausgeben137 System.out.println( "Stack:\n" + stack );138 break;139 }140 default :141 System.out.println( "Kommando " + command + " existiert nicht." );142 }143 System.out.println( "Bitte Kommando eingeben:" );144 try {145 // Kommando einlesen, Leerzeichen entfernen, erstes Zeichen auswaehlen:146 command = in.readLine( ).trim( ).charAt( 0 );147 }148 catch( IndexOutOfBo<strong>und</strong>sException e ) {149 System.out.println( "Keine leeren Kommandos eingeben!" );150 command = ’ ’;151 continue;152 }153 }154 } // main155156 } // class StackProblematisch ist bei dieser Implementierung, dass durch die Größe des benutztenFeldes eine maximale Größe für die realisierten Stacks vorgegeben wird. Dadurchweicht diese Implementierung von der Spezifikation des Datentyps Stackab. Abhilfe kann hier eine dynamisch expandierende Struktur schaffen, etwa diein Abschnitt 1.3.3 vorgestellten verketteten Listen.Beispiel 1.3.2. Als beispielhafte Anwendung von Stacks wird hier zuletzt einRechner“ implementiert, der arithmetische Ausdrücke in Postfix-Notation akzeptiert<strong>und</strong>”auswertet.1 import java.io.*; // main benutzt I/O-Klassen23 /** Die Klasse PostfixRechner implementiert einen Stack-basierten Rechner fuer4 * ganze Zahlen, der Ausdruecke in Postfix-Notation akzeptiert <strong>und</strong> auswertet.5 */6 public class PostfixRechner {


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 2378 public static void main( String[ ] args ) throws IOException { main910 System.out.println( "Der Rechner fuer ganze Zahlen ist bereit!\n" +11 "\tVerfuegbare Operationen:\n" +12 "\t+ -- Addition\n" +13 "\t- -- Subtraktion\n" +14 "\t* -- Multiplikation\n" +15 "\t/ -- Division\n" +16 "\t= -- Endergebnis ausgeben\n" +17 "Einen arithmetischen Ausdruck in Postfix-Notation eingeben,\n" +18 "dabei jede Zahl <strong>und</strong> jeden Operator mit Enter bestaetigen:\n" );1920 Stack stack = new Stack( ); // ein leerer Stack21 BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );22 String eingabeString;23 int eingabeZahl, erstesArgument, zweitesArgument, zwischenErgebnis;24 char operation;2526 while( true ) { // potentielle Endlosschleife27 eingabeString = in.readLine( );28 try { // Ist die Eingabe eine Zahl oder eine Operation?29 eingabeZahl = Integer.parseInt( eingabeString );30 // Die Eingabe war eine Zahl, also Zahl in den Stack einfuegen:31 stack.push( new Integer( eingabeZahl ) );32 continue;33 }34 catch( NumberFormatException e ) { // Die Eingabe war keine Zahl:35 if( eingabeString.length( ) != 0 ) { // Eingabe-String war nicht leer.36 // Leerzeichen entfernen, erstes Zeichen als Operationssymbol auswaehlen:37 operation = eingabeString.trim( ).charAt( 0 );38 }39 else continue;40 }41 if( operation == ’=’ ) {42 // Endergebnis ausgeben (0 bei leerem Stack) <strong>und</strong> terminieren:43 System.out.println( "Endergebnis: " +44 ( stack.isEmpty( ) ? "0" : stack.top( ) ) );45 return;46 }47 // Andernfalls die Operation auswerten:48 if( stack.isEmpty( ) ) { // Der Stack kann leer sein.49 System.out.println( "Bitte zuerst ein Argument eingeben!" );50 continue;51 }52 // Das zweite Argument aus dem Stack holen:53 zweitesArgument = ( (Integer)stack.topAndPop( ) ).intValue( );54 if( stack.isEmpty( ) ) { // Der Stack kann leer sein.55 System.out.println( "Bitte zuerst ein Argument eingeben!" );56 // Das zweite Argument wieder auf den Stack legen:57 stack.push( new Integer( zweitesArgument ) );58 continue;59 }


24 KAPITEL 1. EINLEITUNG60 // Das erste Argument aus dem Stack holen:61 erstesArgument = ( (Integer)stack.topAndPop( ) ).intValue( );62 switch( operation ) {63 case ’+’ : { // Addition ausfuehren64 zwischenErgebnis = erstesArgument + zweitesArgument;65 break;66 }67 case ’-’ : { // Subtraktion ausfuehren68 zwischenErgebnis = erstesArgument − zweitesArgument;69 break;70 }71 case ’*’ : { // Multiplikation ausfuehren72 zwischenErgebnis = erstesArgument * zweitesArgument;73 break;74 }75 case ’/’ : { // Division ausfuehren76 if( zweitesArgument == 0 ) // Division durch Null loest Ausnahme aus:77 throw new ArithmeticException( "Division durch Null verboten!" );78 zwischenErgebnis = erstesArgument / zweitesArgument;79 break;80 }81 default :82 System.out.println( "Operation " + operation + " existiert nicht." );83 continue;84 }85 stack.push( new Integer( zwischenErgebnis ) ); // Zwischenergebnis auf Stack86 System.out.println( "Zwischenergebnis: " + zwischenErgebnis );87 }8889 } // main90 } // class PostfixRechner1.3.2 Schlangen (Queues)Auch eine Queue besteht aus einer Folge a 1 , . . . , a m von Elementen einer DatenmengeItem. Wieder wird nur an einem Ende dieser Folge ( ”vorne“) gelesen <strong>und</strong>gelöscht, allerdings nun am anderen Ende ( ”hinten“) eingefügt. Queues werdendaher auch FIFO-Listen (F irst-I n-F irst-Out) genannt.Einige Anwendungen:a 1 a 2 . . . a m−1 a m↑↑ANFANGENDE• Betriebsmittelverwaltung (Warteschlangen für Prozessoren, Drucker, usw.)• Branch-and-Bo<strong>und</strong>-<strong>Algorithmen</strong> für Suchprobleme in Bäumen <strong>und</strong> Graphen


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 25Auch hierfür wollen wir eine algebraische Spezifikation angeben. Die Signaturmit den Sortensymbolen queue, boolean <strong>und</strong> item:Die Axiome:EMPTY : → queueENQUEUE : queue × item → queueISEMPTY : queue → booleanMAKEEMPTY : queue → queueFRONT : queue → itemDEQUEUE : queue → queueERROR1 : → itemERROR2 : → queueTRUE, FALSE : → booleanISEMPTY(EMPTY) = TRUEISEMPTY(ENQUEUE(q, i)) = FALSEMAKEEMPTY(q) = EMPTYFRONT(EMPTY) = ERROR1FRONT(ENQUEUE(EMPTY, i)) = iFRONT(ENQUEUE(ENQUEUE(q, i), j)) = FRONT(ENQUEUE(q, i))DEQUEUE(EMPTY) = ERROR2DEQUEUE(ENQUEUE(EMPTY, i)) = EMPTYDEQUEUE(ENQUEUE(ENQUEUE(q, i), j)) =ENQUEUE(DEQUEUE(ENQUEUE(q, i)), j)Programm 1.3.3. Eine Implementierung der Datenstruktur Queue durch dieDatenstruktur Array:0 1 2 3 n−3 n−2 n−1a 1 a 2 . . . a m↑ ↑ ↑head ANFANG ENDE (rear)1 import java.util.NoSuchElementException; // zur Fehlerbehandlung2 import java.io.*; // Testroutine main benutzt I/O-Klassen34 /** Die Klasse Queue implementiert Queues beschraenkter Groesse mittels Arrays. */5 public class Queue {67 /** Das Array. */8 private Object[ ] array;9


26 KAPITEL 1. EINLEITUNG10 /** Die Position vor dem vordersten Elements der Queue im Array.11 * Bei leerer Queue ist der Wert gleich dem Wert von rear.12 */13 private int head;1415 /** Die Position des hintersten Elements der Queue im Array.16 * Bei leerer Queue ist der Wert gleich dem Wert von head.17 */18 private int rear;1920 /** Die maximale Laenge des Arrays. */21 private static final int MAX LAENGE = 10;2223 /** Konstruiert die leere Queue mit maximaler Groesse MAX LAENGE. */24 public Queue( ) { Queue25 array = new Object[MAX LAENGE];26 head = rear = −1;27 }2829 /** Konstruiert die leere Queue mit maximaler Groesse n. */30 public Queue( int n ) { Queue31 array = new Object[n];32 head = rear = −1;33 }3435 /** Test, ob die Queue leer ist. */36 public boolean isEmpty( ) { isEmpty37 return head == rear;38 }3940 /** Macht die Queue leer. */41 public void makeEmpty( ) { makeEmpty42 head = rear = −1;43 }4445 /** Test, ob die Queue voll ist. */46 public boolean isFull( ) { isFull47 return head == −1 && rear == array.length−1;48 }4950 /** Fuegt ein neues Element hinten in die Queue ein. */51 public void enqueue( Object inhalt ) { enqueue52 if( isFull( ) )53 throw new IllegalStateException( "Queue ist voll." );54 int f = head+1; // Position des Front-Elements (wenn vorhanden)55 if( rear == array.length−1 ) { // rear hat bereits maximalen Wert56 // Die Queue so weit wie moeglich, also um f Positionen nach vorn schieben:57 for( int i = f; i


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 2763 else { // rear hat noch nicht maximalen Wert64 rear++;65 array[rear] = inhalt; // Element einfuegen66 }67 }6869 /** Gibt das vorderste Element der Queue zurueck, wenn vorhanden.70 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.71 */72 public Object front( ) { front73 if( isEmpty( ) )74 throw new NoSuchElementException( "Queue ist leer." );75 return array[head+1];76 }7778 /** Entfernt das vorderste Element der Queue, wenn vorhanden.79 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.80 */81 public void dequeue( ) { dequeue82 if( isEmpty( ) )83 throw new NoSuchElementException( "Queue ist bereits leer." );84 head++;85 }8687 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen88 * der Queue-Elemente besteht, jeweils durch eine Leerzeile getrennt.89 * Die Reihenfolge ist von vorne nach hinten.90 */91 public String toString( ) { toString92 StringBuffer ausgabe = new StringBuffer( );93 for( int i = head+1; i


28 KAPITEL 1. EINLEITUNG114 switch( command ) {115 case ’ ’ : { // Tue nichts116 break;117 }118 case ’e’ : { // Queue leer machen119 queue.makeEmpty( );120 break;121 }122 case ’n’ : { // String hinten einfuegen123 System.out.println( "\tEinen String eingeben:" );124 queue.enqueue( in.readLine( ) );125 break;126 }127 case ’f’ : { // Vorderstes Element ausgeben128 try {129 System.out.println( "Vorderstes Element: " + queue.front( ) );130 }131 catch( NoSuchElementException e ) { System.out.println( e ); }132 break;133 }134 case ’d’ : { // Vorderstes Element loeschen135 try {136 queue.dequeue( );137 }138 catch( NoSuchElementException e ) { System.out.println( e ); }139 break;140 }141 case ’p’ : { // Queue von vorn nach hinten ausgeben142 System.out.println( "Queue:\n" + queue );143 break;144 }145 default :146 System.out.println( "Kommando " + command + " existiert nicht." );147 }148 System.out.println( "Bitte Kommando eingeben:" );149 try {150 // Kommando einlesen, Leerzeichen entfernen, erstes Zeichen auswaehlen:151 command = in.readLine( ).trim( ).charAt( 0 );152 }153 catch( IndexOutOfBo<strong>und</strong>sException e ) {154 System.out.println( "Keine leeren Kommandos eingeben!" );155 command = ’ ’;156 continue;157 }158 }159 } // main160161 } // class QueueBemerkung 1.3.4. Eine effizientere Implementierung von Queues erhält man,wenn die Elemente des Arrays als kreisförmig angeordnet betrachtet werden,d.h. wenn modulo n gerechnet wird:


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 29Dazu müssen natürlich die Operationen entsprechend implementiert werden.Ein Problem dabei ist, die beiden Fälle ”Schlange ist leer“ <strong>und</strong> ”Schlange istvoll“ zu unterscheiden, da beide Mal ANFANG = ENDE wäre. Dafür brauchenwir entweder eine zusätzliche boolesche Variable (was zusätzlichen Aufwandbei jeder Operation mit sich bringt), oder aber wir erlauben nur, dass maximaln−1 Elemente abgespeichert werden, d.h. die Schlange wird als voll angesehen,wenn ENDE + 1 = ANFANG (mod n) gilt.1.3.3 Einfach verkettete ListenEine einfach verkettete Liste (engl. singly linked list) besteht aus einer Folgevon Knoten, wobei jeder Knoten neben einem Element a i einer DatenmengeItem noch einen Verweis auf den Nachfolgerknoten in der Folge speichert.❝ ❝ ❝ ✓a 1 a 2a 3· · · a m✓✓Diese Datenstruktur kann wie folgt spezifiziert werden. Die Signatur mit denSortensymbolen list, boolean <strong>und</strong> item:EMPTY : → listINS FIRST : list × item → listINS LAST : list × item → listISEMPTY : list → booleanMAKEEMPTY : list → listHEAD : list → itemTAIL : list → listCONCAT : list × list → listERROR1 : → itemERROR2 : → listTRUE, FALSE : → boolean


30 KAPITEL 1. EINLEITUNGDie Axiome:ISEMPTY(EMPTY) = TRUEISEMPTY(INS FIRST(l, i)) = FALSEMAKEEMPTY(l) = EMPTYINS LAST(EMPTY, i) = INS FIRST(EMPTY, i)INS LAST(INS FIRST(l, i), j) = INS FIRST(INS LAST(l, j), i)HEAD(EMPTY) = ERROR1HEAD(INS FIRST(l, i)) = iTAIL(EMPTY) = ERROR2TAIL(INS FIRST(l, i)) = lCONCAT(EMPTY, l) = lCONCAT(INS FIRST(l 1 , i), l 2 ) = INS FIRST(CONCAT(l 1 , l 2 ), i)Weitere mögliche Operationen (Spezifikation <strong>und</strong> Implementierung als Übung):EQUAL : list × list → boolean, LENGTH : list → int, REVERT : list → list.Natürlich kann man die Datenstruktur ”einfach verkettete Liste“ auch in derDatenstruktur Array implementieren. Dann muss man wieder eine maximaleGröße vorgeben, die die Länge der implementierten Listen beschränkt. Stattdessenbetrachten wir hier eine Implementierung durch Referenzen.Programm 1.3.5. Eine Implementierung einfach verketteter Listen mittelsReferenzen:1 import java.util.NoSuchElementException; // zur Fehlerbehandlung2 import java.io.*; // Testroutine main benutzt I/O-Klassen34 /** Die Klasse EinfachVerketteterKnoten implementiert5 * die Knoten einer einfach verketteten Listen.6 */7 class EinfachVerketteterKnoten {89 /** Das Feld inhalt referiert auf ein beliebiges Objekt. */10 private Object inhalt;1112 /** Das Feld next referiert auf den Nachfolgerknoten in einer linearen Liste.13 * Gibt es keinen Nachfolger, so ist der Wert null.14 */15 private EinfachVerketteterKnoten next;1617 /** Konstruiert einen Knoten mit Inhalt inhalt.18 * Das Feld next wird mit null belegt.19 */20 public EinfachVerketteterKnoten( Object inhalt ) { EinfachVerketteterKnoten21 this.inhalt = inhalt;22 }23


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 3124 /** Konstruiert einen Knoten mit Inhalt inhalt <strong>und</strong> Nachfolgerknoten next. */25 public EinfachVerketteterKnoten( Object inhalt, EinfachVerketteterKnoten next ) { EinfachVerketteterKnoten26 this.inhalt = inhalt;27 this.next = next;28 }2930 /** Gibt den Inhalt des Knotens zurueck. */31 public Object inhalt( ) { inhalt32 return inhalt;33 }3435 /** Gibt den Nachfolgerknoten zurueck, wenn vorhanden, sonst null. */36 public EinfachVerketteterKnoten next( ) { next37 return next;38 }3940 /** Aendert den Nachfolgerknoten auf next. */41 public void changeNext( EinfachVerketteterKnoten next ) { changeNext42 this.next = next;43 }4445 } // class EinfachVerketteterKnoten464748 /** Die Klasse EinfachVerketteteListe implementiert einfach verkettete49 * Listen mit Referenzen auf den ersten <strong>und</strong> den letzten Knoten.50 */51 public class EinfachVerketteteListe {5253 /** Das Feld first referiert auf den ersten Knoten der Liste. */54 private EinfachVerketteterKnoten first;5556 /** Das Feld last referiert auf den letzten Knoten der Liste. */57 private EinfachVerketteterKnoten last;5859 // Die Klasse hat den parameterlosen Standardkonstruktor60 // public EinfachVerketteteListe( ) { } zur Erzeugung der leeren Liste.6162 /** Test, ob die Liste leer ist. */63 public boolean isEmpty( ) { isEmpty64 return first == null;65 }6667 /** Macht die Liste leer. */68 public void makeEmpty( ) { makeEmpty69 first = last = null;70 }7172 /** Fuegt einen neuen Knoten mit Inhalt inhalt vorne in die Liste ein. */73 public void insertFirst( Object inhalt ) { insertFirst74 EinfachVerketteterKnoten k = new EinfachVerketteterKnoten( inhalt );75 if( isEmpty( ) )76 first = last = k;


32 KAPITEL 1. EINLEITUNG77 else {78 k.changeNext( first );79 first = k;80 }81 }8283 /** Fuegt einen neuen Knoten mit Inhalt inhalt hinten in die Liste ein. */84 public void insertLast( Object inhalt ) { insertLast85 EinfachVerketteterKnoten k = new EinfachVerketteterKnoten( inhalt );86 if( isEmpty( ) )87 first = last = k;88 else {89 last.changeNext( k );90 last = k;91 }92 }9394 /** Gibt den Inhalt des ersten Knotens der Liste zurueck, wenn vorhanden.95 * Ist die Liste leer, so wird eine Ausnahme ausgeloest.96 */97 public Object head( ) { head98 if( isEmpty( ) )99 throw new NoSuchElementException( "Liste ist leer." );100 return first.inhalt( );101 }102103 /** Entfernt den ersten Knoten der Liste, wenn vorhanden.104 * Ist die Liste leer, so wird eine Ausnahme ausgeloest.105 */106 public void tail( ) { tail107 if( isEmpty( ) )108 throw new NoSuchElementException( "Liste ist bereits leer." );109 first = first.next( );110 if( first == null ) // wenn die Liste leer geworden ist111 last = null;112 }113114 /** Haengt die Liste list hinten an. Die Argumentliste wird danach115 * leer sein. Wir fordern, dass die beiden Listen verschieden sind,116 * ausser sie sind leer; andernfalls wird eine Ausnahme ausgeloest.117 */118 public void concat( EinfachVerketteteListe list ) { concat119 if( !isEmpty( ) && first == list.first )120 throw new IllegalArgumentException( "Identische Listen verknuepft." );121 if( !list.isEmpty( ) ) {122 if( !isEmpty( ) )123 last.changeNext( list.first );124 else {125 first = list.first;126 }127 last = list.last;128 list.makeEmpty( );129 }130 }131


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 33132 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen133 * der Knoteninhalte besteht, jeweils durch eine Leerzeile getrennt.134 * Die Reihenfolge ist von first nach last.135 */136 public String toString( ) { toString137 StringBuffer ausgabe = new StringBuffer( );138 for( EinfachVerketteterKnoten k = first; k != null; k = k.next( ) )139 ausgabe.append( k.inhalt( ) + "\n" );140 return ausgabe.toString( );141 }142143 /** Eine kleine Testroutine, die ueber die Kommandozeile benutzt wird. */144 public static void main( String[ ] args ) throws IOException { main145146 System.out.println( "Liste1 <strong>und</strong> Liste2 koennen ueber die Kommandozeile" +147 " modifiziert\nwerden. Beide enthalten Eintraege vom Typ String.\n" +148 "\tVerfuegbare Kommandos:\n" +149 "\t\"e\" -- Liste1 leer machen (makeEmpty)\n" +150 "\t\"E\" -- Liste2 leer machen (makeEmpty)\n" +151 "\t\"i\" -- String vorn in Liste1 einfuegen (insertFirst)\n" +152 "\t\"I\" -- String vorn in Liste2 einfuegen (insertFirst)\n" +153 "\t\"h\" -- Erstes Element von Liste1 ausgeben (head)\n" +154 "\t\"H\" -- Erstes Element von Liste2 ausgeben (head)\n" +155 "\t\"t\" -- Erstes Element aus Liste1 loeschen (tail)\n" +156 "\t\"T\" -- Erstes Element aus Liste2 loeschen (tail)\n" +157 "\t\"c\" -- Liste2 hinten an Liste1 anhaengen (concat)\n" +158 "\t\"p\" -- Liste1 von first nach last ausgeben (print)\n" +159 "\t\"P\" -- Liste2 von first nach last ausgeben (print)\n" +160 "\t\"q\" -- beendet das Programm (quit)" );161 EinfachVerketteteListe list1 = new EinfachVerketteteListe( );162 EinfachVerketteteListe list2 = new EinfachVerketteteListe( );163 BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );164 char command = ’ ’;165 while( command != ’q’ ) {166 switch( command ) {167 case ’ ’ : { // Tue nichts168 break;169 }170 case ’e’ : { // Liste1 leer machen171 list1.makeEmpty( );172 break;173 }174 case ’E’ : { // Liste2 leer machen175 list2.makeEmpty( );176 break;177 }178 case ’i’ : { // String in Liste1 einfuegen179 System.out.println( "\tEinen String eingeben:" );180 list1.insertFirst( in.readLine( ) );181 break;182 }183 case ’I’ : { // String in Liste2 einfuegen184 System.out.println( "\tEinen String eingeben:" );


34 KAPITEL 1. EINLEITUNG185 list2.insertFirst( in.readLine( ) );186 break;187 }188 case ’h’ : { // Erstes Element von Liste1 ausgeben189 try {190 System.out.println( "Erster Eintrag von Liste1:" + list1.head( ) );191 }192 catch( NoSuchElementException e ) { System.out.println( e ); }193 break;194 }195 case ’H’ : { // Erstes Element von Liste2 ausgeben196 try {197 System.out.println( "Erster Eintrag von Liste2:" + list2.head( ) );198 }199 catch( NoSuchElementException e ) { System.out.println( e ); }200 break;201 }202 case ’t’ : { // Erstes Element aus Liste1 loeschen203 try {204 list1.tail( );205 }206 catch( NoSuchElementException e ) { System.out.println( e ); }207 break;208 }209 case ’T’ : { // Erstes Element aus Liste2 loeschen210 try {211 list2.tail( );212 }213 catch( NoSuchElementException e ) { System.out.println( e ); }214 break;215 }216 case ’c’ : { // Liste2 hinten an Liste1 anhaengen217 list1.concat( list2 );218 break;219 }220 case ’p’ : { // Liste1 von first nach last ausgeben221 System.out.println( "Liste1:\n" + list1 );222 break;223 }224 case ’P’ : { // Liste2 von first nach last ausgeben225 System.out.println( "Liste2:\n" + list2 );226 break;227 }228 default :229 System.out.println( "Kommando " + command + " existiert nicht." );230 }231 System.out.println( "Bitte Kommando eingeben:" );232 try {233 // Kommando einlesen, Leerzeichen entfernen, erstes Zeichen auswaehlen:234 command = in.readLine( ).trim( ).charAt( 0 );235 }


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 35236 catch( IndexOutOfBo<strong>und</strong>sException e ) {237 System.out.println( "Keine leeren Kommandos eingeben!" );238 command = ’ ’;239 continue;240 }241 }242 } // main243244 } // class EinfachVerketteteListeDie <strong>Datenstrukturen</strong> Stack <strong>und</strong> Queue können leicht durch einfach verketteteListen realisiert werden, wodurch dann die bei der Implementierung durchArrays erforderlichen Begrenzungen der Größe wegfallen.1.3.4 Stacks über Listen implementierenEin Stack der Forma m← topa m−1.a 1kann als Liste so realisiert werden:first❝ ❝ ✓a m a m−1· · · a 1✓✓Programm 1.3.6. Alle Stack-Operationen lassen sich nun einfach realisieren:1 import java.io.*; // Testroutine main benutzt I/O-Klassen23 /** Die Klasse Stack implementiert Stacks mittels einfach verketteter Listen. */4 public class Stack {56 /** Die einfach verkettete Liste. */7 private EinfachVerketteteListe list;89 /** Konstruiert den leeren Stack. */10 public Stack( ) { Stack11 list = new EinfachVerketteteListe( );12 }1314 /** Test, ob der Stack leer ist. */15 public boolean isEmpty( ) { isEmpty16 return list.isEmpty( );17 }18


36 KAPITEL 1. EINLEITUNG19 /** Macht den Stack leer. */20 public void makeEmpty( ) { makeEmpty21 list.makeEmpty( );22 }2324 /** Fuegt ein neues Element oben in den Stack ein. */25 public void push( Object inhalt ) { push26 list.insertFirst( inhalt );27 }2829 /** Gibt das oberste Element des Stacks zurueck, wenn vorhanden.30 * Ist der Stack leer, so wird eine Ausnahme ausgeloest.31 */32 public Object top( ) { top33 if( isEmpty( ) )34 throw new IllegalStateException( "Stack ist leer." );35 return list.head( );36 }3738 /** Entfernt das oberste Element des Stacks, wenn vorhanden.39 * Ist der Stack leer, so wird eine Ausnahme ausgeloest.40 */41 public void pop( ) { pop42 if( isEmpty( ) )43 throw new IllegalStateException( "Stack ist bereits leer." );44 list.tail( );45 }4647 /** Gibt das oberste Element des Stacks zurueck <strong>und</strong> entfernt es,48 * wenn vorhanden. Ist der Stack leer, so wird eine Ausnahme ausgeloest.49 */50 public Object topAndPop( ) { topAndPop51 if( isEmpty( ) )52 throw new IllegalStateException( "Stack ist bereits leer." );53 Object topElement = list.head( );54 list.tail( );55 return topElement;56 }5758 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen59 * der Stack-Elemente besteht, jeweils durch eine Leerzeile getrennt.60 * Die Reihenfolge ist von oben nach unten.61 */62 public String toString( ) { toString63 return list.toString( );64 }6566 /** Eine kleine Testroutine, die ueber die Kommandozeile benutzt wird. */67 public static void main( String[ ] args ) throws IOException { mainwie in Programm 1.3.1127 } // main128129 } // class Stack


1.3. EINIGE EINFACHE STRUKTURIERTE DATENTYPEN 371.3.5 Queues über Listen implementierenEine Queue der Forma 1 a 2 . . . a m−1 a m↑↑ANFANGENDEkann als Liste so realisiert werden:❝ ❝ ❝ ✓a 1 a 2· · · a m−1 a m✓✓firstlastProgramm 1.3.7. Auch alle Queue-Operationen lassen sich damit leicht realisieren:1 import java.util.NoSuchElementException; // zur Fehlerbehandlung2 import java.io.*; // Testroutine main benutzt I/O-Klassen34 /** Die Klasse Queue implementiert Queues mittels einfach verketteter Listen. */5 public class Queue {67 /** Die einfach verkettete Liste. */8 private EinfachVerketteteListe list;910 /** Konstuiert die leere Queue. */11 public Queue( ) { Queue12 list = new EinfachVerketteteListe( );13 }1415 /** Test, ob die Queue leer ist. */16 public boolean isEmpty( ) { isEmpty17 return list.isEmpty( );18 }1920 /** Macht die Queue leer. */21 public void makeEmpty( ) { makeEmpty22 list.makeEmpty( );23 }2425 /** Fuegt ein neues Element hinten in die Queue ein. */26 public void enqueue( Object inhalt ) { enqueue27 list.insertLast( inhalt );28 }2930 /** Gibt das vorderste Element der Queue zurueck, wenn vorhanden.31 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.32 */


38 KAPITEL 1. EINLEITUNG33 public Object front( ) { front34 if( isEmpty( ) )35 throw new NoSuchElementException( "Queue ist leer." );36 return list.head( );37 }3839 /** Entfernt das vorderste Element der Queue, wenn vorhanden.40 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.41 */42 public void dequeue( ) { dequeue43 if( isEmpty( ) )44 throw new NoSuchElementException( "Queue ist bereits leer." );45 list.tail( );46 }4748 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen49 * der Queue-Elemente besteht, jeweils durch eine Leerzeile getrennt.50 * Die Reihenfolge ist von vorne nach hinten.51 */52 public String toString( ) { toString53 return list.toString( );54 }5556 /** Eine kleine Testroutine, die ueber die Kommandozeile benutzt wird. */57 public static void main( String[ ] args ) throws IOException { mainwie in Programm 1.3.3117 } // main118119 } // class Queue1.3.6 Doppelt verkettete ListenBei doppelt verketteten Listen (engl. doubly linked lists) speichert jeder Knotenneben dem Verweis auf seinen Nachfolger auch einen Verweis auf seinenVorgänger:✪✪✪❝❝ ❝a 1 ❝ a · · ·2 · · · ❝ a m−1 ❝ a m✓✓✓firstlastDie zusätzlichen Verweise verbrauchen zwar zusätzlich Speicherplatz, dafürermöglichen sie es aber, gewisse Operationen effizienter zu implementieren.


1.4. RECHENZEIT- UND SPEICHERPLATZBEDARF VON ALGORITHMEN391.4 Rechenzeit- <strong>und</strong> Speicherplatzbedarf von <strong>Algorithmen</strong>Es gibt viele Kriterien zur Beurteilung von <strong>Algorithmen</strong> (Programmen), z.B.:• Korrektheit: Arbeitet der Algorithmus korrekt bzgl. der gegebenen Problemspezifikation?(Korrektheitsbeweis: Programmverifikation)• Dokumentation: Sind sowohl die Arbeitsweise als auch das Ein-/Ausgabeverhaltenhinreichend gut beschrieben?• Modularität: Werden in sich abgeschlossene Teilaufgaben als Methodenbereitgestellt?• Lesbarkeit: Ist der Code lesbar (Formatierung, Kommentare)?• Implementierbarkeit: Ist der Algorithmus leicht zu implementieren?Außer diesen statischen Eigenschaften gibt es natürlich auch solche, die sich aufdas Laufzeitverhalten beziehen.Definition 1.4.1. Sei A ein Algorithmus (Programm) zur Berechnung einerFunktion f : Σ ∗ → Σ ∗ , <strong>und</strong> für alle n ∈ N sei P n : Σ n → [0, 1] eine Wahrscheinlichkeitsverteilungauf der Menge Σ n der Eingaben der Länge n.a) Für w ∈ Σ ∗ sei t A (w) die Anzahl der Einzelschritte, die A bei Eingabew macht. Die Zeitkomplexität (Rechenzeitbedarf im schlechtesten Fall)T A : N → N von A ist definiert alsT A (n) = max{t A (w) | w ∈ Σ n }.Die mittlere Zeitkomplexität (Rechenzeitbedarf im Mittel) T (P )A: N → Nvon A ist definiert alsT (P )A (n) = E(t A(w) | w ∈ Σ n ),d.h. als der Erwartungswert für den Rechenzeitbedarf für Eingaben derLänge n; damit gilt T (P )A (n) = ∑ t A (w) · P n (w).w∈Σ n(b) Für w ∈ Σ ∗ sei s A (w) die Anzahl der Speicherplätze für Daten, die A beiEingabe w benutzt. Die Platzkomplexität (Speicherplatzbedarf im schlechtestenFall) S A : N → N von A ist definiert alsS A (n) = max{s A (w) | w ∈ Σ n }.Die mittlere Platzkomplexität (Speicherplatzbedarf im Mittel) S (P )A: N →N von A ist definiert alsS (P )A (n) = E(s A(w) | w ∈ Σ n ).


40 KAPITEL 1. EINLEITUNGDamit die Zeit- <strong>und</strong> die Platzkomplexität eines Algorithmus bestimmt werdenkann, muss genau festlegen werden, wie die Einzelschritte <strong>und</strong> Speicherplätzegezählt werden sollen. Dazu wird meist ein mehr oder weniger abstraktesMaschinenmodell festgelegt, das Schrittzahl <strong>und</strong> Platzverbrauch klar zu definierengestattet. In der Komplexitätstheorie sind beispielsweise Turing-Maschinen 2oder RAMs (Random access machines) gängige Modelle. Zur weiteren Vertiefungverweisen wir auf die Lehrveranstaltung ”Einführung in die <strong>Theoretische</strong>Informatik“.1.5 Asymptotisches WachstumsverhaltenDen Speicherplatz- <strong>und</strong> Rechenzeitbedarf eines Algorithmus kann man oft nichtexakt angeben, weil der genaue Zusammenhang zwischen der Eingabegröße <strong>und</strong>diesen Werten viel zu kompliziert ist. Wir werden uns daher meistens damitbegnügen, das asymptotische Wachstumsverhalten dieser Größen zu bestimmen.Dazu verwenden wir die folgenden Notationen.Definition 1.5.1. Sei f : N → N eine Funktion.• O(f) = {g : N → N | ∃c ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : g(n) ≤ c · f(n)}g ∈ O(f) : g wächst asymptotisch nicht schneller als f.• Ω(f) = {g : N → N | ∃c ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : g(n) ≥ c · f(n)}g ∈ Ω(f) : g wächst asymptotisch nicht langsamer als f.• Θ(f) = O(f) ∩ Ω(f), d.h. g ∈ Θ(f) gdw. g ∈ O(f) <strong>und</strong> g ∈ Ω(f).g ∈ Θ(f) : g wächst asymptotisch genauso schnell wie f.Lemma 1.5.2. Für Funktionen f, g : N → N gilt g ∈ Ω(f) gdw. f ∈ O(g).Beweis:g ∈ Ω(f) gdw. ∃c ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : g(n) ≥ c · f(n)gdw.gdw.gdw.∃c ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : 1 · g(n) ≥ f(n)c∃d ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : f(n) ≤ d · g(n)f ∈ O(g).Lemma 1.5.3. Für Funktionen f, g : N → N gilt g ∈ Θ(f) genau dann, wenn∃c, d ∈ R + ∃n 0 ∈ N ∀n ≥ n 0 : c · f(n) ≤ g(n) ≤ d · f(n).Beweis: Als Übung.2 Nach Alan Turing (1912-1954)


1.5. ASYMPTOTISCHES WACHSTUMSVERHALTEN 41Beispiel 1.5.4. Für f(n) = n 2 , g(n) = 1 2 · n · (n + 1) <strong>und</strong> h(n) = n3 gelten:g ∈ O(h),g ∈ O(f) <strong>und</strong> g ∈ Ω(f), d.h. g ∈ Θ(f),g ∉ Ω(h).Satz 1.5.5. Sei f(n) = a m·n m +a m−1·n m−1 +· · ·+a 1·n+a 0 mit a 0 , . . . , a m ∈ Zein Polynom mit a m > 0. Dann gilt f ∈ Θ(n m ).∑Beweis: Für alle n ≥ 1 gilt: f(n) = m a i · n i ≤ m (∑m∑)|a i | · n i ≤ |a i | · n m .i=0i=0i=0Also ist f ∈ O(n m ). Und wegen a m > 0 gibt es ein n 0 ∈ N mit der Eigenschaft∀n ≥ n 0 : a m · n m + 2a m−1 · n m−1 + · · · + 2a 1 · n + 2a 0 ≥ 0.Daraus folgt: ∀n ≥ n 0 : n m ≤ a m · n m ≤ 2f(n), d.h. n m ∈ O(f). Mit Lemma1.5.2 folgt: f ∈ Θ(n m ).Zum Schluss dieses Abschnitts wollen wir uns noch anschauen, wie sich dieverschiedenen Zeitkomplexitäten auswirken. Die erste Tabelle zeigt, wie sich fürdie verschiedenen Zeitschranken die Rechenzeit mit der Eingabegröße verändert.Zeitschranke Rechenzeit für Eingabegröße n bei 10 9 Operationen pro Sek<strong>und</strong>ef(n) n = 10 n = 20 n = 30 n = 40 n = 50f(n) = n 10 −8 s 2 · 10 −8 s 3 · 10 −8 s 4 · 10 −8 s 5 · 10 −8 sf(n) = n⌊log n⌋ 3 · 10 −8 s 8 · 10 −8 s 1, 5 · 10 −7 s 2, 2 · 10 −7 s 3 · 10 −7 sf(n) = n 2 1 · 10 −7 s 4 · 10 −7 s 9 · 10 −7 s 1, 6 · 10 −6 s 2, 5 · 10 −6 sf(n) = n 3 1 · 10 −6 s 8 · 10 −6 s 2, 7 · 10 −5 s 6, 4 · 10 −5 s 1, 2 · 10 −4 sf(n) = 2 √ n8 · 10 −9 s 2, 2 · 10 −8 s 4, 5 · 10 −8 s 7 · 10 −8 s 1, 3 · 10 −7 sf(n) = 2 n 1 · 10 −6 s 1 · 10 −3 s 1 sec 1000 sec 11 Tg. 13 Std.f(n) = n! 3, 6 · 10 −3 s 77 Jahre · · · · · · · · ·Die zweite Tabelle gibt für die verschiedenen Zeitschranken an, welche Eingabegrößennoch innerhalb einer gegebenen Zeit verarbeitet werden können.Zeitkomplexität Maximale Eingabegröße für Zeit tf(n) t = 1 sec. t = 1 min. t = 1 Std.f(n) = n 10 9 6 · 10 10 3, 6 · 10 12f(n) = n · ⌊log 2 n⌋ 6 · 10 7 2, 8 · 10 9 1, 3 · 10 11f(n) = n 2 3 · 10 4 2, 5 · 10 5 2 · 10 6f(n) = n 3 1000 4000 15000f(n) = 2 √ n900 1300 1400f(n) = 2 n 30 36 37f(n) = n! 12 14 15Wir sehen also:• Die Steigerung der Rechengeschwindigkeit wirkt sich am deutlichsten beischnellen <strong>Algorithmen</strong> aus.


42 KAPITEL 1. EINLEITUNG• Der Übergang zu einem schnelleren Algorithmus ist oft wirksamer als derzu einer schnelleren Maschine.• Für die Lösung von Problemen, die häufig auftreten, etwa als Teilproblemeanderer Probleme, lohnt sich der Aufwand, nach einem ”optimalenAlgorithmus“ zu suchen.1.6 Die Java-KlassenbibliothekViele objektorientierte Programmiersprachen bieten einen großen Vorrat an vordefinierten<strong>Datenstrukturen</strong>, entweder bereits als Teil der Kernsprache oderals mehr oder weniger standardisierte Erweiterungen (für C++ etwa durch dieStandard Template Library). Auch Java bringt viele für diese Lehrveranstaltungrelevanten Strukturen <strong>und</strong> <strong>Algorithmen</strong> mit.In diesem Abschnitt soll beispielhaft gezeigt werden, wie Queues über der Java-Klasse LinkedList implementiert werden können. Der Datentyp LinkedList entsprichtweitgehend den in Abschnitt 1.3.3 vorgestellten verketteten Listen. Hiereinige Ausschnitte aus dem Java-API (Application Programming Interface) (siehehttp://java.sun.com/j2se/1.3/docs/api/):Class LinkedListjava.lang.Objectjava.util.AbstractCollectionjava.util.AbstractListjava.util.AbstractSequentialListjava.util.LinkedListAll Implemented Interfaces: Cloneable, Collection, List, SerializableLinked list implementation of the List interface. Implements all optional listoperations, and permits all elements (including null). In addition to implementingthe List interface, the LinkedList class provides uniformly named methodsto get, remove and insert an element at the beginning and end of the list. Theseoperations allow linked lists to be used as a stack, queue, or double-endedqueue (deque). [ . . . ] All of the operations perform as could be expected for adoubly-linked list. [ . . . ]• public LinkedList() Constructs an empty list.• public Object getFirst() Returns the first element in this list.


1.6. DIE JAVA-KLASSENBIBLIOTHEK 43• public Object getLast() Returns the last element in this list. ThrowsNoSuchElementException if this list is empty.• public Object removeFirst() Removes and returns the first element fromthis list. Throws NoSuchElementException if this list is empty.• public Object removeLast() Removes and returns the last element fromthis list. Throws NoSuchElementException if this list is empty.• public void addFirst(Object o) Inserts the given element at the beginningof this list.• public void addLast(Object o) Appends the given element to the end ofthis list.• public boolean add(Object o) Appends the specified element to the endof this list. Returns true (as per the general contract of Collection.add 3 ).• public void clear() Removes all of the elements from this list.• public String toString() Returns a string representation of this collection.The string representation consists of a list of the collection’s elementsin the order they are returned by its iterator, enclosed in square brackets("[]"). Adjacent elements are separated by the characters ", " (commaand space). Elements are converted to strings as by String.valueOf(Object).Programm 1.6.1. Eine Implementierung von Queue über LinkedList:1 import java.util.LinkedList; // zur internen Darstellung der Queue2 import java.util.NoSuchElementException; // zur Fehlerbehandlung3 import java.io.*; // Testroutine main benutzt I/O-Klassen45 /** Die Klasse Queue implementiert Queues ueber der Java-Klasse LinkedList. */6 public class Queue {78 /** Die verkettete Liste. */9 private LinkedList list;1011 /** Konstuiert die leere Queue. */12 public Queue( ) { Queue13 list = new LinkedList( );14 }1516 /** Test, ob die Queue leer ist. */17 public boolean isEmpty( ) { isEmpty18 return list.isEmpty( );19 }203 In der Dokumentation des Interface Collection steht nämlich: Returns true if this collectionchanged as a result of the call.


44 KAPITEL 1. EINLEITUNG21 /** Macht die Queue leer. */22 public void makeEmpty( ) { makeEmpty23 list.clear( );24 }2526 /** Fuegt ein neues Element hinten in die Queue ein. */27 public void enqueue( Object inhalt ) { enqueue28 list.addLast( inhalt );29 }3031 /** Gibt das vorderste Element der Queue zurueck, wenn vorhanden.32 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.33 */34 public Object front( ) { front35 if( isEmpty( ) )36 throw new NoSuchElementException( "Queue ist leer." );37 return list.getFirst( );38 }3940 /** Entfernt das vorderste Element der Queue, wenn vorhanden.41 * Ist die Queue leer, so wird eine Ausnahme ausgeloest.42 */43 public void dequeue( ) { dequeue44 if( isEmpty( ) )45 throw new NoSuchElementException( "Queue ist bereits leer." );46 list.removeFirst( );47 }4849 /** Gibt den String zurueck, der aus der Folge der String-Darstellungen50 * der Queue-Elemente besteht, in eckigen Klammern <strong>und</strong> jeweils durch Komma getrennt.51 * Die Reihenfolge ist von vorne nach hinten.52 */53 public String toString( ) { toString54 return list.toString( );55 }5657 /** Eine kleine Testroutine, die ueber die Kommandozeile benutzt wird. */58 public static void main( String[ ] args ) throws IOException { mainwie in Programm 1.3.3118 } // main119120 } // class Queue


Kapitel 2Bäume <strong>und</strong> ihreImplementierungDie Datenstruktur ”Baum“ ist von großer Bedeutung, da man damit sehr einfachhierarchische Beziehungen zwischen Objekten darstellen kann. Solche Hierarchientreten in natürlicher Weise in vielen Anwendungen auf:• Gliederung eines Unternehmens in Bereiche, Abteilungen, Gruppen <strong>und</strong>Mitarbeiter• Gliederung eines Buches in Kapitel, Abschnitte <strong>und</strong> Unterabschnitte• Gliederung eines Programms in Methoden, Blöcke <strong>und</strong> EinzelanweisungenEin Baum besteht aus einer (endlichen) Menge von Knoten 1 V <strong>und</strong> einer (endlichen)Menge von gerichteten Kanten 2 E zwischen Knoten aus V . Führt eineKante von u ∈ V zu v ∈ V , dann heißt u der Elternknoten von v <strong>und</strong> v ein Kindvon u. Ist v in einem oder mehreren Schritten (d.h. durch eine Folge von Kanten)von u aus erreichbar, dann ist u ein Vorgänger von v <strong>und</strong> v ein Nachfolgervon u. Ein Baum hat einen ausgezeichneten Knoten, die Wurzel, der Vorgängerjedes anderen Knotens ist. Außerdem ist jeder Knoten auf nur genau eine Weisevon der Wurzel aus erreichbar.1 engl. vertices2 engl. edges45


46 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGIm Allgemeinen stellen wir einen Baum graphisch wie folgt dar: 1 2 3 4 5 6 7 8 9 10 11 12 13Dieser Baum hat 13 Knoten <strong>und</strong> 12 Kanten. Knoten 1 ist die Wurzel. DieKnoten 2 , 3 <strong>und</strong> 4 sind die Kinder der Wurzel, sie sind also Geschwister.Die Knoten 12 , 6 bis 10 <strong>und</strong> 13 haben keine Kinder. Sie sind die Blätterdes Baums, während die anderen Knoten die inneren Knoten des Baums sind.Der Grad d(v) eines Knotens v ist die Anzahl seiner Kinder.2.1 Die Datenstruktur BaumBei Bäumen über einer Knotenmenge V unterscheiden wir zwei Arten, die ungeordneten<strong>und</strong> die geordneten. Bei der ersten Struktur kommt es auf die Reihenfolgeder Unterbäume nicht an, bei der zweiten aber sehr wohl. Wir formalisiereneinen ungeordneten Baum (oder einfach: Baum) als Paar aus seiner Wurzel<strong>und</strong> der Menge seiner direkten Unterbäume, einen geordneten Baum stattdessenals (geordnete) Folge seiner Wurzel gefolgt von der Folge seiner direktenUnterbäume. Beide Definitionen sind also rekursiv.Definition 2.1.1 ((Ungeordneter) Baum). Sei V eine Menge von Knoten.• Für v ∈ V ist (v, {}) ein Baum; er hat nur den Knoten v, seine Wurzel.• Sind B 1 , . . . , B m (m ≥ 1) Bäume mit paarweise disjunkten Knotenmengen,<strong>und</strong> ist v ∈ V ein weiterer Knoten, dann istB = (v, {B 1 , . . . , B m })ein Baum mit Wurzel v <strong>und</strong> direkten Unterbäumen (oder: Teilbäumen)B 1 , . . . , B m . Graphisch stellen wir B wie folgt dar, wobei {B i1 , . . . , B im } ={B 1 , . . . , B m } sei:✁✁✁✗✔v✖✕ ◗◗◗✑ ✑✑✑ ◗✁❆✁❆✁ ❆ . . . ✁ ❆B❆❆✁ ❆i1✁B im❆ ✁❆❆


2.1. DIE DATENSTRUKTUR BAUM 47Definition 2.1.2 (Geordneter Baum). Sei V wie oben.• Für v ∈ V ist (v) ein geordneter Baum; er hat nur den Knoten v, seineWurzel.• Sind B 1 , . . . , B m (m ≥ 1) geordnete Bäume mit paarweise disjunktenKnotenmengen, <strong>und</strong> ist v ∈ V ein weiterer Knoten, dann istB = (v, B 1 , . . . , B m )ein geordneter Baum mit Wurzel v, <strong>und</strong> für 1 ≤ i ≤ m ist B i der i-tedirekte Unterbaum (oder: Teilbaum) von B.Notationen:Die Tiefe (oder Stufe) eines Knotens v in einem Baum wird rekursiv definiert:Tiefe(v) = 0Tiefe(v) = 1 + Tiefe(v ′ )wenn v die Wurzel ist,wenn v ′ Elternknoten von v ist.Die Höhe eines Baumes B ist definiert alsHöhe(B) = max{Tiefe(v) | v ist Knoten von B}.Ein wichtiger Sonderfall der geordneten Bäume sind die binären Bäume. InAbweichung zu obiger Definition lässt man auch leere binäre Bäume zu, d.h.binäre Bäume ohne Knoten.Definition 2.1.3. Ein binärer Baum ist entweder leer, oder er ist ein geordneterBaum mit Knotenmenge V ≠ ∅, <strong>und</strong> es gilt d(v) ≤ 2 für alle v ∈ V .Wir unterscheiden bei einem Knoten eines binären Baumes zwischen dem linken<strong>und</strong> dem rechten Teilbaum.Beispiel 2.1.4. B 1 : 1 2B 2 : 1 2Als Bäume sind B 1 <strong>und</strong> B 2 identisch, nicht aber als binäre Bäume.Nachdem wir die Objekte ”Binäre Bäume“ definiert haben, können wir n<strong>und</strong>ie Datenstruktur ”Binäre Bäume über einer Menge Item“ einführen. Hier einemögliche algebraische Spezifikation dieses Datentyps:


48 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGDie Signatur mit den Sortensymbolen btree, boolean <strong>und</strong> item:Die Axiome:EMPTY : → btreeTREE : btree × item × btree → btreeISEMPTY : btree → booleanLCHILD : btree → btreeITEM : btree → itemRCHILD : btree → btreeERROR1 : → itemERROR2 : → btreeTRUE, FALSE : → booleanISEMPTY(EMPTY) = TRUEISEMPTY(TREE(b 1 , i, b 2 )) = FALSELCHILD(EMPTY) = ERROR2LCHILD(TREE(b 1 , i, b 2 )) = b 1ITEM(EMPTY) = ERROR1ITEM(TREE(b 1 , i, b 2 )) = iRCHILD(EMPTY) = ERROR2RCHILD(TREE(b 1 , i, b 2 )) = b 2Lemma 2.1.5. Sei B ein nicht-leerer binärer Baum der Höhe h.(a) Für 0 ≤ i ≤ h gilt, dass B höchstens 2 i Knoten der Tiefe i enthält.(b) B enthält mindestens h + 1 <strong>und</strong> höchstens 2 h+1 − 1 Knoten.(c) Sei n die Anzahl der Knoten in B. Dann gilt⌈log(n + 1)⌉ − 1 ≤ h ≤ n − 1.Beweis: (a) Beweis durch Induktion nach i. (b) Offensichtlich gilt n ≥ h + 1.Andererseits gilt:n =h∑i=0Anzahl der Knoten der Tiefe i in B ≤(a)h∑2 i = 2 h+1 − 1.i=0(c) Nach (b) gilt h ≤ n − 1. Andererseits gilt nach (b) auch n ≤ 2 h+1 − 1, d.h.log(n + 1) ≤ h + 1. Da h ganzzahlig ist, bedeutet dies ⌈log(n + 1)⌉ − 1 ≤ h.Lemma 2.1.6. Sei B ein nicht-leerer binärer Baum. Ist n 0 die Anzahl derBlätter <strong>und</strong> n 2 die Anzahl der Knoten vom Grad 2 in B, so gilt n 0 = n 2 + 1.Beweis: Durch Induktion über den Aufbau von B.


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 49Definition 2.1.7. Ein nicht-leerer binärer Baum der Höhe h ist voll, wenn er2 h+1 − 1 Knoten enthält.Definition 2.1.8. Sei B ein binärer Baum der Höhe h mit n > 0 Knoten<strong>und</strong> sei B ′ ein voller binärer Baum der Höhe h. In B ′ seien die Knoten stufenweisevon links nach rechts durchnummeriert (vgl. obiges Beispiel). B heißtvollständig, wenn er dem binären Baum B ′′ entspricht, der aus B ′ entsteht, indemdie Knoten mit den Nummern n + 1, n + 2, . . . , 2 h+1 − 1 gestrichen werden.Auch den leeren Baum wollen wir sowohl voll als auch vollständig nennen.Beispiel 2.1.9. Die Abbildung zeigt einen vollen binären Baum der Höhe 2.Werden die Knoten mit den Nummern 6 <strong>und</strong> 7 gestrichen, entsteht ein vollständigerbinärer Baum mit fünf Knoten.✗✔1✟✖✕✗✔ ✟✟❍ ❍❍ ✗✔2 3✖✕ ✖✕✗✔ ✗✔ ❅ ✗✔ ✗✔ ❅4 5 6 7✖✕ ✖✕ ✖✕ ✖✕Beachte: Für vollständige binäre Bäume mit n > 0 Knoten <strong>und</strong> Höhe h gilt2 h ≤ n < 2 h+1 .2.2 Implementierung binärer BäumeFür vollständige binäre Bäume (Definition 2.1.8) erhalten wir eine sehr einfache<strong>und</strong> effiziente Implementierung mittels eindimensionaler Felder.Sei B ein vollständiger binärer Baum mit n Knoten, wobei in den KnotenElemente der Menge Item gespeichert seien 3 . Wir implementieren B durch einFeld der Länge n; der Knoten mit Index i (1 ≤ i ≤ n) wird an der Positioni − 1 gespeichert. (Statt vom Knoten mit Index i sprechen wir auch kurz vomKnoten i.) Dabei ist uns die folgende Beobachtung von Nutzen.Lemma 2.2.1. Für vollständige binäre Bäume mit n Knoten gilt (1 ≤ i ≤ n):(a) Ist i > 1, so ist der Knoten ⌊i/2⌋ der Elternknoten des Knotens i.(b) Ist 2i ≤ n, so ist der Knoten 2i das linke Kind des Knotens i.(c) Ist 2i + 1 ≤ n, so ist der Knoten 2i + 1 das rechte Kind des Knotens i.3 Das in einem Knoten gespeicherte Datenelement nennen wir auch Schlüssel des Knoten.


50 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGBeweis: (a) folgt aus (b) <strong>und</strong> (c).(b) Durch Induktion nach i: Der Fall i = 1 ist klar. Für Knoten j (1 ≤ j ≤ i−1)ist nach Induktionsannahme das linke Kind der Knoten 2j. Auf das linke Kindvon i − 1 folgt das rechte Kind von i − 1 <strong>und</strong> dann das linke Kind von i; daslinke Kind des Knoten i ist also der Knoten 2(i − 1) + 2 = 2i, falls 2i ≤ n ist.(c) Analog zu (b).Programm 2.2.2. Eine Implementierung vollständiger binärer Bäume:1 /**2 * Die Klasse VollstaendigerBinaererBaum implementiert3 * vollstaendige binaere Baeume mittels Arrays.4 *5 * Die Knoten des Baums haben einen Index i mit6 * 1


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 5142 /** Gibt das im Knoten mit Index i gespeicherte Objekt zurueck.43 * Fuer die Zahl i muss 1


52 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG96 /** Gibt den Index des Elternknotens des Knotens mit Index i zurueck,97 * falls einer vorhanden ist, sonst 0.98 * Fuer die Zahl i muss 1


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 53149 System.out.println( );150151 int finda = b1.findeErstesVorkommen( new Character( ’a’ ) );152 System.out.println( "Das Zeichen ’a’ steht in " +153 ( finda != 0 ? "Knoten " + finda : "keinem Knoten" ) );154 int findh = b1.findeErstesVorkommen( new Character( ’h’ ) );155 System.out.println( "Das Zeichen ’h’ steht in " +156 ( findh != 0 ? "Knoten " + findh : "keinem Knoten" ) );157 int findi = b1.findeErstesVorkommen( new Character( ’i’ ) );158 System.out.println( "Das Zeichen ’i’ steht in " +159 ( findi != 0 ? "Knoten " + findi : "keinem Knoten" ) );160 System.out.println( );161162 b1.inhaltAendern( 6, new Character( ’z’ ) );163 System.out.print( b1 );164 } // main165166 } // class VollstaendigerBinaererBaumEin Testlauf:Stufe 0: aStufe 1: b cStufe 2: d e f gStufe 3: hDer Knoten 1 hat den Inhalt a,das linke Kind 2, das rechte Kind 3 <strong>und</strong> keinen ElternknotenDer Knoten 2 hat den Inhalt b,das linke Kind 4, das rechte Kind 5 <strong>und</strong> den Elternknoten 1Der Knoten 3 hat den Inhalt c,das linke Kind 6, das rechte Kind 7 <strong>und</strong> den Elternknoten 1Der Knoten 4 hat den Inhalt d,das linke Kind 8, kein rechtes Kind <strong>und</strong> den Elternknoten 2Der Knoten 5 hat den Inhalt e,kein linkes Kind, kein rechtes Kind <strong>und</strong> den Elternknoten 2Der Knoten 6 hat den Inhalt f,kein linkes Kind, kein rechtes Kind <strong>und</strong> den Elternknoten 3Der Knoten 7 hat den Inhalt g,kein linkes Kind, kein rechtes Kind <strong>und</strong> den Elternknoten 3Der Knoten 8 hat den Inhalt h,kein linkes Kind, kein rechtes Kind <strong>und</strong> den Elternknoten 4Das Zeichen ’a’ steht in Knoten 1Das Zeichen ’h’ steht in Knoten 8Das Zeichen ’i’ steht in keinem KnotenStufe 0: aStufe 1: b cStufe 2: d e z gStufe 3: h


54 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGNatürlich könnte man diese Implementierung für alle binären Bäume verwenden,nicht nur für die vollständigen, wenn man im Feld an den Stellen, diekeinem Knoten entsprechen, einen speziellen Wert ”Knoten nicht vorhanden“speichert. Dies führt allerdings im allgemeinen zu einer erheblichen Platzverschwendung.Man betrachte etwa den binären Baum 1 2 nder ein Feld der Größe 2 n − 1 belegt. Dieser Nachteil kann durch eine dynamischeImplementierung vermieden werden. Im Folgenden geben wir zwei solcheImplementierungen der Datenstruktur ”Binärer Baum“ an.Definition 2.2.3. (a) Man verwendet drei Felder der Länge max zur Speicherungbinärer Bäume: inhalt, linkesKind <strong>und</strong> rechtesKind. Ein binärer Baum Bwird nun in diesen Feldern gespeichert, indem jedem Knoten die drei Plätzeinhalt[j], linkesKind[j] <strong>und</strong> rechtesKind[j] zur Verfügung gestellt werden fürein j mit 0 ≤ j ≤ max − 1:inhalt[j] = Inhalt des Knotens,linkesKind[j] = Position des linken Kindes,rechtesKind[j] = Position des rechten Kindes.Ferner brauchen wir eine Variable b, die die Position der Wurzel angibt.Beispiel: Für max = 12 wird ein Baum mit acht Knoten gespeichert (b = 4): w 1 w 2 w 3 w 4 w 5 w 6 w 7 w 8j inhalt linkesKind rechtesKind01 w 4 0 02 w 2 1 33 w 5 8 104 w 1 2 65 w 6 0 06 w 3 0 578 w 7 0 0910 w 8 0 011


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 55(b) Man verwendet Knoten, die aus drei Komponenten bestehen:• dem gespeicherten Inhalt,• dem Verweis auf das linke Kind,• dem Verweis auf das rechte Kind.Ein Baum wird dann als eine Referenz auf eine durch Referenzen verb<strong>und</strong>eneStruktur von Knoten realisiert.Beispiel:w 1◗ ◗◗◗◗B✦ ✦✦✦✦✦✦✦w 2 w✟3-✟ ❧ ❧✟✟❧❧❧❧❧❧✟✟w 4- - w 5w✟6- -✟ ❧✟✟❧❧❧✟✟w 7- -w 8- -Programm 2.2.4. Die entsprechende Implementierung binärer Bäume:1 /** Die Klasse Binaerknoten implementiert Knoten eines binaeren Baums.2 * Jeder Knoten speichert ein beliebiges Objekt vom Typ Object,3 * zusaetzlich je eine Referenz auf das linke <strong>und</strong> das rechte Kind.4 */5 class Binaerknoten {67 /** Der Knoteninhalt. */8 private Object inhalt;910 /** Das linke <strong>und</strong> das rechte Kind. */11 private Binaerknoten linkesKind;12 private Binaerknoten rechtesKind;1314 /** Konstruiert einen Blattknoten mit Inhalt inhalt. */15 public Binaerknoten( Object inhalt ) { Binaerknoten16 this.inhalt = inhalt;17 }18


56 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG19 /** Konstruiert einen Knoten mit Inhalt inhalt,20 * linkem Kind knoten1 <strong>und</strong> rechtem Kind knoten2.21 */22 public Binaerknoten( Object inhalt, Binaerknoten knoten1, Binaerknoten knoten2 ) { Binaerknoten23 this.inhalt = inhalt;24 linkesKind = knoten1;25 rechtesKind = knoten2;26 }2728 /** Gibt den Inhalt des Knotens zurueck. */29 public Object inhalt( ) { inhalt30 return inhalt;31 }3233 /** Gibt das linke Kind zurueck. */34 public Binaerknoten linkesKind( ) { linkesKind35 return linkesKind;36 }3738 /** Gibt das rechte Kind zurueck. */39 public Binaerknoten rechtesKind( ) { rechtesKind40 return rechtesKind;41 }4243 /** Der Inhalt wird zu inhalt. */44 public void inhaltAendern( Object inhalt ) { inhaltAendern45 this.inhalt = inhalt;46 }4748 /** Das linke Kind wird zu knoten. */49 public void linkesKindAendern( Binaerknoten knoten ) { linkesKindAendern50 linkesKind = knoten;51 }5253 /** Das rechte Kind wird zu knoten. */54 public void rechtesKindAendern( Binaerknoten knoten ) { rechtesKindAendern55 rechtesKind = knoten;56 }5758 /** Durchsucht den hier wurzelnden Baum in Vorordnung.59 * Gibt true zurueck, wenn es darin einen Knoten mit Inhalt inhalt gibt,60 * sonst false. Die Objekte werden mit equals verglichen.61 */62 public boolean suche( Object inhalt ) { suche63 return inhalt.equals( inhalt ) | |64 ( linkesKind == null ? false : linkesKind.suche( inhalt ) ) | |65 ( rechtesKind == null ? false : rechtesKind.suche( inhalt ) );66 }6768 /** Gibt eine String-Darstellung des Inhalts zurueck. */69 public String toString( ) { toString70 return inhalt.toString( );71 }


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 577273 /** Gibt den hier wurzelnden Baum in Vorordnung aus. */74 public void druckeVorordnung( ) { druckeVorordnung75 System.out.print( this + " " );76 if( linkesKind != null )77 linkesKind.druckeVorordnung( );78 if( rechtesKind != null )79 rechtesKind.druckeVorordnung( );80 }8182 /** Gibt den hier wurzelnden Baum in Zwischenordnung aus. */83 public void druckeZwischenordnung( ) { druckeZwischenordnung84 if( linkesKind != null )85 linkesKind.druckeZwischenordnung( );86 System.out.print( this + " " );87 if( rechtesKind != null )88 rechtesKind.druckeZwischenordnung( );89 }9091 /** Gibt den hier wurzelnden Baum in Nachordnung aus. */92 public void druckeNachordnung( ) { druckeNachordnung93 if( linkesKind != null )94 linkesKind.druckeNachordnung( );95 if( rechtesKind != null )96 rechtesKind.druckeNachordnung( );97 System.out.print( this + " " );98 }99100 } // class Binaerknoten101102103 /** Die Klasse Binaerbaum implementiert binaere Baeume mittels Referenzen.104 * Ein Binaerbaum besteht einfach aus seinem Wurzelknoten vom Typ Binaerknoten.105 */106 public class Binaerbaum {107108 /** Der Wurzelknoten. */109 private Binaerknoten wurzel;110111 /** Konstruiert einen Baum, der nur einen Wurzelknoten112 * mit Inhalt inhalt besitzt.113 */114 public Binaerbaum( Object inhalt ) { Binaerbaum115 wurzel = new Binaerknoten( inhalt );116 }117118 /** Konstruiert den leeren Baum. */119 public Binaerbaum( ) { } Binaerbaum120


58 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG121 /** Gibt den Wurzelknoten zurueck. */122 public Binaerknoten wurzel( ) { wurzel123 return wurzel;124 }125126 /** Test, ob der Baum leer ist. */127 boolean istLeer( ) { istLeer128 return wurzel == null;129 }130131 /** Erzeugt einen Baum mit dem aktuellen Baum als linkem Teilbaum,132 * mit neuem Wurzelknoten mit Inhalt inhalt <strong>und</strong> mit rechtem Teilbaum baum.133 * Der Argumentbaum baum wird danach leer sein.134 * Um zu verhindern, dass die Teilbaeume uebereinstimmen, fordern wir, dass135 * die beiden Wurzelreferenzen verschieden sind, ausser beide Baeume sind leer;136 * andernfalls wird eine Ausnahme ausgeloest.137 */138 Binaerbaum anhaengen( Object inhalt, Binaerbaum baum ) { anhaengen139 if( !istLeer( ) && wurzel == baum.wurzel )140 throw new IllegalArgumentException( "Identische Baeume verknuepft." );141 wurzel = new Binaerknoten( inhalt, wurzel, baum.wurzel );142 baum.wurzel = null;143 return this;144 }145146 /** Durchsucht den Baum in Vorordnung.147 * Gibt true zurueck, wenn es darin einen Knoten mit Inhalt inhalt gibt,148 * sonst false. Die Objekte werden mit equals verglichen.149 */150 public boolean suche( Object inhalt ) { suche151 return istLeer( ) ? false : wurzel.suche( inhalt );152 }153154 /** Gibt den Baum in Vorordnung aus. */155 public void druckeVorordnung( ) { druckeVorordnung156 if( istLeer( ) )157 System.out.println( "Der Baum ist leer." );158 else {159 wurzel.druckeVorordnung( );160 System.out.println( );161 }162 }163164 /** Gibt den Baum in Zwischenordnung aus. */165 public void druckeZwischenordnung( ) { druckeZwischenordnung166 if( istLeer( ) )167 System.out.println( "Der Baum ist leer." );168 else {169 wurzel.druckeZwischenordnung( );170 System.out.println( );171 }172 }173


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 59174 /** Gibt den Baum in Nachordnung aus. */175 public void druckeNachordnung( ) { druckeNachordnung176 if( istLeer( ) )177 System.out.println( "Der Baum ist leer." );178 else {179 wurzel.druckeNachordnung( );180 System.out.println( );181 }182 }183184 public static void main( String[ ] args ) { main185 Binaerbaum b1 =186 new Binaerbaum( "A" ).anhaengen( "*", new Binaerbaum( "B" ) );187 b1.druckeVorordnung( );188 b1.druckeZwischenordnung( );189 b1.druckeNachordnung( );190 System.out.println( );191192 Binaerbaum b2 =193 new Binaerbaum( "A" ).194 anhaengen( "/", new Binaerbaum( "B" ).195 anhaengen( "*", new Binaerbaum( "C" ) ) ).196 anhaengen( "*", new Binaerbaum( "D" ) ).197 anhaengen( "+", new Binaerbaum( "E" ) );198 b2.druckeVorordnung( );199 b2.druckeZwischenordnung( );200 b2.druckeNachordnung( );201 } // main202203 } // class BinaerbaumEin Testlauf (vgl. Beispiel 2.2.6):* A BA * BA B *+ * / A * B C D EA / B * C * D + EA B C * / D * E +Eine der Operationen, die man oftmals auf Bäumen durchführen muss, bestehtdarin, jeden Knoten des Baumes genau einmal zu besuchen. Dazu müssen wirden Baum systematisch durchlaufen (oder: traversieren). Ein solches systematischesDurchlaufen liefert uns eine lineare Anordnung der Knoten <strong>und</strong> damit derim Baum gespeicherten Information. Drei wichtige Strategien hierfür ergebensich aus der rekursiven Form der Definition der binären Bäume.


60 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGDefinition 2.2.5. Sei B ein binärer Baum.Die Vorordnungsfolge der Knoten von B ist rekursiv wie folgt definiert:(i) Als erstes kommt die Wurzel von B,(ii) dann die Vorordnungsfolge des linken direkten Teilbaums,(iii) <strong>und</strong> schließlich die Vorordnungsfolge des rechten direkten Teilbaums.Die Zwischenordnungsfolge der Knoten von B ist rekursiv definiert durch:(i) Zuerst kommt die Zwischenordnungsfolge des linken direkten Teilbaums,(ii) dann die Wurzel von B,(iii) <strong>und</strong> schließlich die Zwischenordnungsfolge des rechten direkten Teilbaums.Die Nachordnungsfolge der Knoten von B ist rekursiv definiert durch:(i) Zuerst kommt die Nachordnungsfolge des linken direkten Teilbaums,(ii) dann die Nachordnungsfolge des rechten direkten Teilbaums,(iii) <strong>und</strong> schließlich die Wurzel von B.Beispiel 2.2.6. Mittels binärer Bäume lassen sich leicht arithmetische Ausdrückedarstellen. Der arithmetische Ausdruck((A/(B ∗ C)) ∗ D) + Emit den Variablen A, . . . , E kann durch den folgenden binären Baum eindeutigbeschrieben werden: + ∗ E / D A ∗ B CSeine Vorordnungsfolge ( ˆ= Präfixnotation des Ausdrucks):+ ∗ / A ∗ B C D ESeine Zwischenordnungsfolge ( ˆ= Infixnotation des Ausdrucks; ist ohne Klammerungnicht eindeutig):A / B ∗ C ∗ D + E


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 61Seine Nachordnungsfolge ( ˆ= Postfixnotation des Ausdrucks):A B C ∗ / D ∗ E +Offensichtlich lassen sich diese Folgen leicht rekursiv bestimmen (wie in derDefinition angegeben), wobei wir die Implementierung mittels Referenzen ausDefinition 2.2.3(b) zu Gr<strong>und</strong>e legen. In Programm 2.2.4 ist dies in den MethodendruckeVorordnung, druckeZwischenordnung bzw. druckeNachordnungrealisiert.Wir wollen nun die Rechenzeit bestimmen, die diese <strong>Algorithmen</strong> für einenbinären Baum der Höhe h mit n = 2 h+1 − 1 Knoten brauchen. Dazu sei z(h)die Zeit, die das Drucken in Zwischenordnung im schlechtesten Fall für einenbinären Baum der Höhe h braucht. Dann gelten für z(h) die folgenden Gleichungen:z(0) = c 1 für eine Konstante c 1 > 0,z(h + 1) = z(h) + c 2 + z(h) für eine Konstante c 2 > 0.Zur Vereinfachung ersetzen wir c 1 <strong>und</strong> c 2 durch den Wert c = max{c 1 , c 2 }. Diesist gerechtfertigt, da wir uns nur für das asymptotische Verhalten der Funktionz(h) für h → ∞ interessieren. Wir erhalten nun aus obigen Gleichungenfolgendes:z(h) = 2z(h − 1) + c = 4z(h − 2) + 2c + c = · · ·∑h−1= 2 h z(0) + c ·i=02 i= 2 h · c + c · (2 h − 1) = c · (2 h+1 − 1) = c · n ∈ O(n).Da n Knoteninhalte ausgegeben werden, erhalten wir sogar z(h) ∈ Θ(n). DasDurchlaufen eines binären Baums in Vor- oder Nachordnung wird ganz analogrealisiert, daher ergibt sich dafür derselbe Rechenzeitbedarf.Anwendungen für dieses Durchlaufen eines binären Baums sind neben der Ausgabebeispielsweise die folgenden Aufgaben:• das Erstellen der Kopie eines binären Baums,• das Durchsuchen nach einem speziellen Knoten (vgl. die Methode sucheder Klasse Binaerbaum bzw. -knoten in Programm 2.2.4),• die Auswertung der durch einen binären Baum dargestellten Information(z.B. einen Ausdruck auswerten).Wenn wir uns die Darstellung von binären Bäumen mittels Referenzen genaueranschauen, dann sehen wir, dass die meisten Felder linkesKind <strong>und</strong> rechtesKindden Wert null enthalten.


62 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGSei etwa B ein binärer Baum mit n 0 Blättern, n 1 Knoten mit nur einem Kind<strong>und</strong> n 2 Knoten mit 2 Kindern. Bei den Blättern sind jeweils beide Felder linkesKind<strong>und</strong> rechtesKind leer, bei den Knoten mit einem Kind ist jeweils genaueines dieser Felder leer, <strong>und</strong> nur bei den Knoten mit 2 Kindern sind allediese Felder nicht leer, d.h. 2n 0 + n 1 Felder sind leer, aber nur 2n 2 + n 1 =2(n 0 − 1) + n 1 < 2n 0 + n 1 Felder sind nicht leer (Lemma 2.1.6). Diese leerenFelder kann man nun benutzen, um zusätzliche Verweise zu speichern, wodurchdas Durchlaufen des Baums vereinfacht wird.Definition 2.2.7. Implementierung der Datenstruktur ”Binärer Baum“ mittelsReferenzen <strong>und</strong> zusätzlichen Verweisen (threaded binary trees):Die Darstellung erfolgt wie in Definition 2.2.3(b). Zusätzlich werden folgendeVerweise (threads) gespeichert: Ist für einen Knoten K im Baum B das FeldK.rechtesKind gleich null, so wird in K.rechtesKind ein Verweis auf den Knotengespeichert, der beim Durchlaufen von B in Zwischenordnung unmittelbarhinter K ausgegeben wird. Ist für K das Feld K.linkesKind gleich null, so wirdentsprechend in K.linkesKind ein Verweis auf den Knoten gespeichert, der beimDurchlaufen von B in Zwischenordnung unmittelbar vor K ausgegeben wird.Beispiel: w 1 w2 w 3 w 4 w 5 w 6 w 7 w 8B : linkes bzw. rechtes Kind, : linker Thread, : rechter ThreadZwischenordnungsfolge: w 4 , w 2 , w 7 , w 5 , w 8 , w 1 , w 3 , w 6Damit wir bei der Implementierung zwischen den normalen Referenzen, die aufdie Kinder eines Knotens zeigen, <strong>und</strong> den zusätzlichen Verweisen unterscheidenkönnen, ergänzen wir jeden Knoten (bzw. seine Darstellung) durch zwei BoolescheWerte linksThread <strong>und</strong> rechtsThread, die anzeigen, ob es sich bei derentsprechenden Referenz um einen zusätzlichen Verweis handelt:K.linksThread = true gdw K.linkerVerweis ist ein zusätzlicher Verweis,K.rechtsThread = true gdw K.rechterVerweis ist ein zusätzlicher Verweis.Für das obige Beispiel erhalten wir damit die folgende Realisierung (im Diagrammsind jeweils von links nach rechts die fünf Felder linksThread, linker-Verweis, inhalt, rechterVerweis, rechtsThread dargestellt):


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 63truefalse w 1 falseTB✧ ❜✧❜❜❜✧✧false w 2 false true w 3 false✧ ❜ ❜✧❜❜❜❜❜❜✧✧- w 4 true false w 5 false true w 6 -✧ ❜✧❜❜❜✧✧true w 7 true true w 8 truetrueZur Vereinfachung mancher <strong>Algorithmen</strong> kann man bei dieser Implementierungfür jeden binären Baum noch einen speziellen Kopfknoten einführen:Für den leeren Baum: truefalse TBFür den nicht-leeren Baum:falsezum Wurzelknoten falseTBDamit wird ein nicht-leerer Baum über die linke Referenz (linkesKind) desKopfknotens erreicht. Die beiden Verweise im Baum, die noch null sind, werdennun als Verweise auf den Kopfknoten verwendet. Obiges Beispiel sieht dann soaus:Kopfknotentruefalsefalse✪✪✪false w 1 false✧ ❜✧❜❜❜✧✧false w 2 false true w 3 false✧ ❜ ❜✧❜❜❜❜❜❜✧✧w 4 true false w 5 false true w 6✧ ❜✧❜❜❜✧✧true w 7 true true w 8 trueTBtrue


64 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGDie zusätzlichen Verweise kann man nun dazu verwenden, für einen Knoten denNachfolger bezüglich der Zwischenordnung zu bestimmen. In folgendem Programmtut dies die Methode nachfolger der Klasse BinaerknotenMitThreads:1 public BinaerknotenMitThreads erster( ) { erster2 BinaerknotenMitThreads k = this;3 while( !k.linksThread ) // solange ein linkes Kind vorhanden ist4 k = k.linkerVerweis;5 return k;6 }78 public BinaerknotenMitThreads nachfolger( ) { nachfolger9 return rechtsThread ? rechterVerweis : rechterVerweis.erster( );10 }Man überlegt sich leicht, dass diese Methode die folgenden Eigenschaften hat.Lemma 2.2.8. Sei T B eine Referenz auf einen Knoten eines binären Baums B,der wie oben beschrieben implementiert ist. Wird dafür die Methode nachfolgeraufgerufen, dann liefert dieser Aufruf eine Referenz auf den Knoten K in B,der in der Zwischenordnungsfolge unmittelbar auf den Knoten T B folgt. Fernergelten folgende Aussagen:• K = T B genau dann, wenn T B der Kopfknoten des leeren Baums ist.• K ist der Kopfknoten des nicht-leeren Baums B genau dann, wenn T Bder letzte Knoten von B bezüglich der Zwischenordnung ist.Unter Verwendung der Methode nachfolger kann man einen binären Baum inZwischenordnung durchlaufen, ohne dabei Rekursion bzw. einen zusätzlichenKeller zu verwenden. Dies nutzen die Methoden suche <strong>und</strong> druckeZwischenordnungin folgendem Beispielprogramm aus.Programm 2.2.9. Eine Implementierung binärer Bäume mit zusätzlichen Verweisen<strong>und</strong> mit Kopfknoten:1 /** Die Klasse BinaerknotenMitThreads implementiert Knoten eines binaeren2 * Baums. Jeder Knoten speichert ein beliebiges Objekt vom Typ Object,3 * zusaetzlich je eine Referenz auf das linke <strong>und</strong> das rechte Kind oder,4 * falls das jeweilige Kind nicht vorhanden ist, eine Referenz auf den5 * Vorgaenger bzw. Nachfolger bzgl. der Zwischenordnung.6 */7 class BinaerknotenMitThreads {89 /** Der Knoteninhalt. */10 private Object inhalt;11


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 6512 /** Das linke <strong>und</strong> das rechte Kind bzw. der jeweilige zusaetzliche Verweis. */13 private BinaerknotenMitThreads linkerVerweis;14 private BinaerknotenMitThreads rechterVerweis;1516 /** Boolesche Werte geben an, ob linker bzw. rechter Verweis auf ein Kind17 * referiert (false) oder ein zusaetzlicher Verweis ist (true).18 */19 private boolean linksThread;20 private boolean rechtsThread;2122 /** Konstruiert einen Blattknoten mit Inhalt inhalt.23 * Die Threads sind noch auf null gesetzt.24 */25 public BinaerknotenMitThreads( Object inhalt ) { BinaerknotenMitThreads26 this.inhalt = inhalt;27 linksThread = rechtsThread = true;28 }2930 /** Gibt den Inhalt des Knotens zurueck. */31 public Object inhalt( ) { inhalt32 return inhalt;33 }3435 /** Gibt den linken Verweis zurueck. */36 public BinaerknotenMitThreads linkerVerweis( ) { linkerVerweis37 return linkerVerweis;38 }3940 /** Gibt den rechten Verweis zurueck. */41 public BinaerknotenMitThreads rechterVerweis( ) { rechterVerweis42 return rechterVerweis;43 }4445 /** Gibt den Wert von linksThread zurueck. */46 public boolean linksThread( ) { linksThread47 return linksThread;48 }4950 /** Gibt den Wert von rechtsThread zurueck. */51 public boolean rechtsThread( ) { rechtsThread52 return rechtsThread;53 }5455 /** Der Inhalt wird zu inhalt. */56 public void inhaltAendern( Object inhalt ) { inhaltAendern57 this.inhalt = inhalt;58 }5960 /** Der linke Verweis wird zu knoten. */61 public void linkenVerweisAendern( BinaerknotenMitThreads knoten ) { linkenVerweisAendern62 linkerVerweis = knoten;63 }64


66 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG65 /** Der rechter Verweis wird zu knoten. */66 public void rechtenVerweisAendern( BinaerknotenMitThreads knoten ) { rechtenVerweisAendern67 rechterVerweis = knoten;68 }6970 /** Der Wert von linksThread wird zu b. */71 public void linksThreadAendern( boolean b ) { linksThreadAendern72 linksThread = b;73 }7475 /** Der Wert von rechtsThread wird zu b. */76 public void rechtsThreadAendern( boolean b ) { rechtsThreadAendern77 rechtsThread = b;78 }7980 /** Test, ob der Knoten ein Kopfknoten ist. */81 boolean istKopfknoten( ) { istKopfknoten82 return this.rechterVerweis == this;83 }8485 /** Gibt den ersten Knoten des hier wurzelnden (bzw. fuer Kopfknoten hier86 * haengenden) Baums bzgl. Zwischenordnung zurueck. Ist der aktuelle Knoten87 * der Kopfknoten des leeren Baums, wird er selbst zurueckgegeben.88 */89 public BinaerknotenMitThreads erster( ) { erster90 BinaerknotenMitThreads k = this;91 while( !k.linksThread ) // solange ein linkes Kind vorhanden ist92 k = k.linkerVerweis;93 return k;94 }9596 /** Gibt den letzten Knoten des hier wurzelnden (bzw. fuer Kopfknoten hier97 * haengenden) Baums bzgl. Zwischenordnung zurueck. Ist der aktuelle Knoten98 * der Kopfknoten des leeren Baums, wird er selbst zurueckgegeben.99 */100 public BinaerknotenMitThreads letzter( ) { letzter101 BinaerknotenMitThreads k = this;102 if( k.istKopfknoten( ) ) { // falls der Knoten ein Kopfknoten ist103 if( k.linksThread ) // falls es der Kopfknoten des leeren Baums ist104 return k;105 else // der Baum ist nicht leer106 k = k.linkerVerweis;107 }108 while( !k.rechtsThread ) // solange ein rechtes Kind vorhanden ist109 k = k.rechterVerweis;110 return k;111 }112113 /** Gibt den Nachfolgerknoten bzgl. Zwischenordnung zurueck. */114 public BinaerknotenMitThreads nachfolger( ) { nachfolger115 return rechtsThread ? rechterVerweis : rechterVerweis.erster( );116 }117


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 67118 /** Durchsucht den hier wurzelnden Baum in Zwischenordnung. Gibt true zurueck,119 * wenn es darin einen Knoten mit Inhalt inhalt gibt, sonst false. Die Objekte120 * werden mit equals verglichen. Fuer Kopfknoten wird false zurueckgegeben.121 */122 public boolean suche( Object inhalt ) { suche123 BinaerknotenMitThreads k = this;124 while( !k.istKopfknoten( ) ) {125 if( k.inhalt.equals( inhalt ) )126 return true;127 k = k.nachfolger( );128 }129 return false;130 }131132 /** Gibt eine String-Darstellung des Inhalts zurueck. */133 public String toString( ) { toString134 return inhalt.toString( );135 }136137 /** Gibt den hier wurzelnden Baum in Zwischenordnung aus.138 * Fuer Kopfknoten wird nichts ausgegeben.139 */140 public void druckeZwischenordnung( ) { druckeZwischenordnung141 BinaerknotenMitThreads k = this;142 while( !k.istKopfknoten( ) ) {143 System.out.print( k + " " );144 k = k.nachfolger( );145 }146 }147148 } // class BinaerknotenMitThreads149150151 /** Die Klasse BinaerbaumMitThreads implementiert binaere Baeume mit Threads.152 * Ein solcher Binaerbaum besteht aus seinem Kopfknoten (siehe Skripttext).153 */154 public class BinaerbaumMitThreads {155156 /** Der Kopfknoten. */157 private BinaerknotenMitThreads kopfknoten;158159 /** Konstruiert den leeren Baum, der also nur den Kopfknoten besitzt.160 * Der Kopfknoten speichert (zu Testzwecken) den String “Kopfknoten”.161 */162 public BinaerbaumMitThreads( ) { BinaerbaumMitThreads163 kopfknoten = new BinaerknotenMitThreads( "Kopfknoten" );164 kopfknoten.linkenVerweisAendern( kopfknoten );165 kopfknoten.rechtenVerweisAendern( kopfknoten );166 kopfknoten.rechtsThreadAendern( false );167 }168


68 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG169 /** Konstruiert einen Baum, der nur einen Wurzelknoten mit Inhalt170 * inhalt besitzt.171 */172 public BinaerbaumMitThreads( Object inhalt ) { BinaerbaumMitThreads173 this( ); // Konstruktor fuer den leeren Baum aufrufen174 BinaerknotenMitThreads wurzel = new BinaerknotenMitThreads( inhalt );175 kopfknoten.linksThreadAendern( false );176 kopfknoten.linkenVerweisAendern( wurzel );177 wurzel.linkenVerweisAendern( kopfknoten );178 wurzel.rechtenVerweisAendern( kopfknoten );179 }180181 /** Gibt den Kopfknoten zurueck. */182 public BinaerknotenMitThreads kopfknoten( ) { kopfknoten183 return kopfknoten;184 }185186 /** Test, ob der Baum leer ist. */187 boolean istLeer( ) { istLeer188 return kopfknoten.linksThread( );189 }190191 /** Gibt den Wurzelknoten zurueck, falls der Baum nicht leer ist,192 * sonst den Kopfknoten.193 */194 public BinaerknotenMitThreads wurzel( ) { wurzel195 return istLeer( ) ? kopfknoten : kopfknoten.linkerVerweis( );196 }197198 /** Gibt den ersten Knoten des Baums bzgl. Zwischenordnung zurueck.199 * Ist der Baum leer, wird der Kopfknoten zurueckgegeben.200 */201 public BinaerknotenMitThreads erster( ) { erster202 return kopfknoten.erster( );203 }204205 /** Gibt den letzten Knoten des Baums bzgl. Zwischenordnung zurueck.206 * Ist der Baum leer, wird der Kopfknoten zurueckgegeben.207 */208 public BinaerknotenMitThreads letzter( ) { letzter209 return kopfknoten.letzter( );210 }211212 /** Erzeugt einen Baum mit dem aktuellen Baum als linkem Teilbaum,213 * mit neuem Wurzelknoten mit Inhalt inhalt <strong>und</strong> mit rechtem Teilbaum baum.214 * Der Argumentbaum baum wird danach leer sein.215 * Um zu verhindern, dass die Teilbaeume uebereinstimmen, fordern wir, dass216 * die beiden Wurzelreferenzen verschieden sind, ausser beide Baeume sind leer;217 * andernfalls wird eine Ausnahme ausgeloest.218 */219 BinaerbaumMitThreads anhaengen( Object inhalt, BinaerbaumMitThreads baum ) { anhaengen220 BinaerbaumMitThreads neuerBaum = new BinaerbaumMitThreads( inhalt );221 if( !istLeer( ) && wurzel( ) == baum.wurzel( ) )222 throw new IllegalArgumentException( "Identische Baeume verknuepft." );


2.2. IMPLEMENTIERUNG BINÄRER BÄUME 69223 if( !istLeer( ) ) { // wenn der linke Teilbaum nicht leer ist224 neuerBaum.wurzel( ).linksThreadAendern( false );225 neuerBaum.wurzel( ).linkenVerweisAendern( wurzel( ) );226 erster( ).linkenVerweisAendern( neuerBaum.kopfknoten );227 letzter( ).rechtenVerweisAendern( neuerBaum.wurzel( ) );228 }229 if( !baum.istLeer( ) ) { // wenn der rechte Teilbaum nicht leer ist230 neuerBaum.wurzel( ).rechtsThreadAendern( false );231 neuerBaum.wurzel( ).rechtenVerweisAendern( baum.wurzel( ) );232 baum.erster( ).linkenVerweisAendern( neuerBaum.wurzel( ) );233 baum.letzter( ).rechtenVerweisAendern( neuerBaum.kopfknoten );234 }235 // Das Argument baum leer machen:236 baum.kopfknoten.linksThreadAendern( true );237 baum.kopfknoten.linkenVerweisAendern( baum.kopfknoten );238 return neuerBaum;239 }240241 /** Durchsucht den Baum in Vorordnung. Gibt true zurueck, wenn es darin242 * einen Knoten mit Inhalt inhalt gibt, sonst false. Die Objekte werden243 * mit equals verglichen.244 */245 public boolean suche( Object inhalt ) { suche246 return istLeer( ) ? false : erster( ).suche( inhalt );247 }248249 /** Gibt den Baum in Zwischenordnung aus. */250 public void druckeZwischenordnung( ) { druckeZwischenordnung251 if( istLeer( ) )252 System.out.println( "Der Baum ist leer." );253 else {254 erster( ).druckeZwischenordnung( );255 System.out.println( );256 }257 }258259 public static void main( String[ ] args ) { main260 BinaerbaumMitThreads b1 =261 new BinaerbaumMitThreads( "A" ).262 anhaengen( "*", new BinaerbaumMitThreads( "B" ) );263 b1.druckeZwischenordnung( );264265 BinaerbaumMitThreads b2 =266 new BinaerbaumMitThreads( "A" ).267 anhaengen( "/", new BinaerbaumMitThreads( "B" ).268 anhaengen( "*", new BinaerbaumMitThreads( "C" ) ) ).269 anhaengen( "*", new BinaerbaumMitThreads( "D" ) ).270 anhaengen( "+", new BinaerbaumMitThreads( "E" ) );271 b2.druckeZwischenordnung( );272 } // main273274 } // class BinaerbaumMitThreads


70 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG2.3 Erste Anwendungen binärer BäumeAls erste Anwendung der binären Bäume lernen wir hier zwei Sortierverfahrenkennen. Sei dabei Item eine Menge von Objekten <strong>und</strong> ≤ eine lineare Ordnungauf Item, d.h. ≤ erfülle die Axiome• a ≤ a• aus a ≤ b <strong>und</strong> b ≤ c folgt a ≤ c• aus a ≤ b <strong>und</strong> b ≤ a folgt a = b• a ≤ b oder b ≤ afür alle a, b, c ∈ Item. Sei nun (a 1 , . . . , a n ) eine endliche Folge von Elementenaus Item. Wir wollen diese Folge so zu einer Folge (b 1 , . . . , b n ) umordnen, dassb 1 ≤ b 2 ≤ · · · ≤ b n gilt.2.3.1 TREE SORTUnser erster Algorithmus löst diese Aufgabe unter Verwendung binärer Bäumewie folgt:Eingabe: n = 2 k Elemente a 1 , . . . , a n einer linear geordneten Menge (Item, ≤).Ausgabe: Die Elemente a 1 , . . . , a n in aufsteigend sortierter Reihenfolge, d.h.(b 1 , . . . , b n ) mit b 1 ≤ b 2 ≤ · · · ≤ b n , wobei b i = a π(i) für eine Permutation (d.h.eine bijektive Abbildung) π : {1, . . . , n} → {1, . . . , n} ist.Bemerkung: Ist 2 k < n < 2 k+1 , so wähle a n+1 , . . . , a 2 k+1 als den symbolischenWert ∞ <strong>und</strong> führe den Algorithmus für a 1 , . . . , a n , a n+1 , . . . , a 2 k+1 durch.Algorithmus 2.3.1. Sortieren mit binären Bäumen (TREE SORT):1 void treeSort( ) { treeSort2 (1.) Trage ein KO-Turnier zwischen den n Elementen a 1 , . . . , a n aus.3 Dabei soll ein binärer Baum aufgebaut werden, wobei jeder Elternknoten stets4 als Knoteninhalt den kleinsten Wert der Knoteninhalte seiner Kinder erhält.56 for( int i = 1; i


2.3. ERSTE ANWENDUNGEN BINÄRER BÄUME 71Beispiel 2.3.2. n = 4 <strong>und</strong> (b 1 , . . . , b 4 ) = (17, 2, 12, 13).(1.)2 2 12 17 2 12 13(2.) i = 1: Ausgabe 2− − 12 17 − 12 13i = 2: Ausgabe 12− 17 − 17 − − 13i = 3: Ausgabe 13, 12 17 12 17 − 12 13, 13 17 13 17 − − 13− 17 − 17 − − −i = 4: Ausgabe 17., 17 17 − 17 − − −Lemma 2.3.3. Für n = 2 k gilt:(a) TREE SORT hat bei Eingabe a 1 , . . . , a n den Platzbedarf 2n − 1.(b) TREE SORT hat bei Eingabe a 1 , . . . , a n den Zeitbedarf O(n log n).Beweis: (a) Abgesehen von einigen Referenzen muss TREE SORT den aufgebautenbinären Baum mit n Blättern <strong>und</strong> n − 1 inneren Knoten speichern.⎫(b) Schritt (1.): 2 k−1 Vergleiche auf Stufe k Damit werden insgesamt2 k − 1 = n − 12 k−2 Vergleiche auf Stufe k − 1⎪⎬. · · · . Vergleiche durchgeführt.⎪⎭1 Vergleiche auf Stufe 1Schritt (2.): Für 1 ≤ i ≤ n: Absteigen <strong>und</strong> Löschen: k Stufen; Aufsteigen <strong>und</strong>neu vergleichen: k Stufen.Damit ergibt sich ein Zeitbedarf von c · ((n − 1) + n · k) ∈ O(n log n).Der Zeitbedarf von TREE SORT ist gut, da man zeigen kann, dass für dasSortieren von n Elementen im schlechtesten Fall stets O(n log n) Schlüsselvergleichenotwendig sind. Der Platzbedarf von TREE SORT ist mit 2n − 1 aberentschieden zu hoch, wenn man sehr lange Folgen a 1 , . . . , a n sortieren will.


72 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG2.3.2 HEAP SORTHier wollen wir noch ein Sortierverfahren betrachten, das ebenfalls mit binärenBäumen arbeitet, das zum Sortieren von n Elementen aber nur Platzbedarfn + c hat. Dazu brauchen wir den folgenden Begriff.Definition 2.3.4. Sei Item eine linear geordnete Menge. Ein Heap (über Item)ist ein vollständiger binärer Baum über Item, in dem jeder Knoten einen Schlüsselhat, der höchstens so groß ist wie die Schlüssel seiner Kinder.Beispiel 2.3.5. Zwei Heaps:2 7 4 10 8 6 10 20 8350Bemerkung 2.3.6. Sei H ein Heap über Item mit n Knoten. Für 1 ≤ i ≤ nbezeichne K i den i-ten Knoten bei der stufenweisen Durchnummerierung vonH von links nach rechts.(a) Wird H in einem Feld der Länge n so gespeichert, dass K i an Position isteht, so gilt nach Lemma 2.2.1 für 1 ≤ i ≤ n folgendes:(i) Ist i > 1, so steht der Elternknoten von K i an Position ⌊i/2⌋.(ii) Ist 2i ≤ n, so steht das linke Kind von K i an Position 2i; andernfallshat K i kein linkes Kind.(iii) Ist 2i + 1 ≤ n, so steht das rechte Kind von K i an Position 2i + 1;andernfalls hat K i kein rechtes Kind.(b) Ist b i ∈ Item der Schlüssel von K i , so gilt b 1 = min{b i | 1 ≤ i ≤ n}. Diesfolgt unmittelbar aus der Definition des Heaps.Daher eignen sich Heaps insbesondere zur Speicherung von Prioritätsschlangen.Als nächstes wollen wir uns einige wichtige Operationen auf Heaps anschauen.Wir beginnen mit der Operation ”Einfügen“.Beispiel 2.3.7. H : 20 , b ∈ Item. 35 4540 50Nach dem Einfügen muss der entstandene Heap folgende Form haben:✁✁✁*❆ ❆❆*❅ ❅❅* * *✁ ✁✁*


2.3. ERSTE ANWENDUNGEN BINÄRER BÄUME 73(a) b = 55:20 ❅ ❅❅35 45✁ ❆ ✁✁ ❆❆ ✁✁✁40 50 55(b) b = 25:,,2020 ❅ ❅ ❅❅ ❅❅3545 35 * ⇐ 25 ? 35✁ ❆ ✁ ✁ ❆ ✁✁ ❆✁ ❆❆ ✁ ✁ ❆❆ ✁✁ ❆❆✁✁ ✁✁✁40 50 * ⇐ 25 ? 40 50 45 4020❅ ❅❅25♥✁ ✁✁50 45(c) b = 10:✁✁✁4020 ❅ ❅❅3545❆ ✁❆❆ ✁✁50 * ⇐ 10 ?,,20 ❅ ❅❅35* ⇐ 10 ?✁ ❆ ✁✁ ❆❆ ✁✁✁40 50 45✁✁✁4010 ♥ ❅ ❅❅3520❆ ✁❆❆ ✁✁50 45Algorithmus 2.3.8. Einfügen eines Elements in einen Heap (wir setzen voraus,dass ein Feld array zur Speicherung des Heaps zur Verfügung steht):1 void einfuegenInHeap( Comparable c ) { einfuegenInHeap2 if( knotenZahl == array.length ) // Einfuegen ist unmoeglich3 throw new IllegalStateException( "Heap ist bereits voll." );4 knotenZahl++;5 int i = knotenZahl;6 for( ; i > 1 && c.compareTo( inhalt( i/2 ) ) < 0; i = i/2 ) {7 // wenn i nicht 1 ist <strong>und</strong> c kleiner als der Inhalt des Knotens i/2 ist:8 inhaltAendern( i, inhalt( i/2 ) );9 }10 inhaltAendern( i, c ); // das neue Objekt in den Knoten i einfuegen11 }Zeitbedarf von insertHeap: O(log n).Sei H ein nicht-leerer Heap über Item mit n Knoten. Wir wollen nun die Wurzelvon H entfernen <strong>und</strong> dann die verbleibenden Knoten wieder in Form eines Heapsorganisieren.


74 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGBeispiel 2.3.9.H : 10 35 20 40 50 45Zuerst wird die 10 entfernt. Danach muss der Heap die folgenden Form haben:*✁✁✁* ❅ ❅❅**❆ ❆❆*✁✁✁4035❆ ❆❆50* ⇐ 45 ?❅ ❅❅20; min{20, 35} = 20 < 45 :✁✁✁4020 ❅ ❅❅3545❆ ❆❆50Aus dem resultierenden Heap wird nun noch die 20 entfernt:✁✁✁40* ⇐ 50 ?❅ ❅❅35 45; min{35, 45} = 35 < 50 :? 50 ⇒ *✁✁✁4035❅ ❅❅45;40 < 50 :✁✁✁5035 ❅ ❅❅4045Zur Realisierung dieser Operation entwickeln wir zunächst eine HilfsmethodelasseEinsinken, die folgendes leistet: Eine Folge von Elementen aus Item sei imFeld array im Bereich i bis n gespeichert. Falls die Teilbäume mit Wurzelknoten2i <strong>und</strong> 2i+1 bereits die Heap-Eigenschaft erfüllen, so sorgt diese Methode dafür,dass auch der Teilbaum mit Wurzelknoten i diese Eigenschaft erfüllt.


2.3. ERSTE ANWENDUNGEN BINÄRER BÄUME 75Algorithmus 2.3.10. Hilfsmethode lasseEinsinken:1 void lasseEinsinken( int i, int n ) { lasseEinsinken2 int j = 2*i;3 Comparable c = (Comparable)inhalt( i );4 while( j 0 )7 j++;8 if( c.compareTo( inhalt( j ) ) > 0 ) {9 inhaltAendern( j/2, inhalt( j ) );10 j = 2*j;11 }12 else13 break;14 }15 inhaltAendern( j/2, c );16 }Zeitbedarf von lasseEinsinken: O(Höhe des Teilbaums mit Wurzelknoten i).Der Algorithmus loeschenAusHeap kann nun wie folgt formuliert werden.Algorithmus 2.3.11. Entfernen des obersten Elements aus einem Heap:1 Object loeschenAusHeap( ) { loeschenAusHeap2 if( istLeer( ) ) // Loeschen ist unmoeglich3 throw new IllegalStateException( "Heap ist leer." );4 Comparable c = (Comparable)inhalt( 1 );5 inhaltAendern( 1, inhalt( knotenZahl ) );6 knotenZahl−−;7 if( !istLeer( ) )8 lasseEinsinken( 1, knotenZahl );9 return c;10 }Zeitbedarf von loeschenAusHeap: O(log n).Da bei einem Heap das kleinste Element immer oben steht, liefert uns dieOperation loeschenAusHeap(1.) das kleinste der n Elemente im Heap <strong>und</strong>(2.) einen Heap für die restlichen n − 1 Elemente.Also können wir durch das Aufbauen eines Heaps für n Elemente <strong>und</strong> anschließendesEntfernen diese Elemente sortieren. Dies ist die Idee des folgendenSortieralgorithmus.


76 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGAlgorithmus 2.3.12. Sortieren mit Heaps (HEAP SORT):1 void erzeugeHeap( ) { erzeugeHeap2 for( int i = knotenZahl/2; i > 0; i−− )3 lasseEinsinken( i, knotenZahl );4 }56 /** Die im Baum gespeicherten Knoteninhalte werden absteigend sortiert7 * bezueglich der durch compareTo gegebenen Ordnung.8 * Alle Knoteninhalte muessen daher paarweise mit compareTo vergleichbar sein.9 */10 void heapsort( ) { heapsort11 erzeugeHeap( );12 for( int i = knotenZahl−1; i > 0; i−− ) {13 Object c = inhalt( i+1 );14 inhaltAendern( i+1, inhalt( 1 ) );15 inhaltAendern( 1, c );16 lasseEinsinken( 1, i );17 }18 }Lemma 2.3.13. HEAP SORT hat einen Zeitbedarf von O(n log n) <strong>und</strong> einenPlatzbedarf von n + c.Beweis: (1.) Heap erstellen: Sei 2 h ≤ n < 2 h+1 , d.h. der zu erstellende Heaphat die Höhe h. Damit finden statt:auf Stufe h − 1: höchstens 2 h−1 Aufrufe von lasseEinsinken,d.h. Zeitbedarf ≤ c · 2 h−1 · 1auf Stufe h − 2: höchstens 2 h−2 Aufrufe von lasseEinsinkend.h. Zeitbedarf ≤ c · 2 h−2 · 2.Und so weiter. Damit erhalten wir:∑h−1∑h−1Zeitbedarf ≤ c · 2 i · (h − i) = c · 2 h · (h − i) · 2 i−hi=1∑h−1= c · 2 h · i · 2 −ii=1} {{ }


2.3. ERSTE ANWENDUNGEN BINÄRER BÄUME 77Programm 2.3.14. Eine einfache Implementierung von HEAP SORT:wie in Programm 2.2.211 * Werden Heaps mit vollstaendigen binaeren Baeumen realisiert,12 * dann muessen die gespeicherten Objekte vom Typ Comparable sein.13 */14 public class VollstaendigerBinaererBaum {wie in Programm 2.2.2132 /** Das Objekt c vom Typ Comparable wird in den Heap eingefuegt.133 * Wir setzen voraus, dass der Baum davor die Heap-Eigenschaft hat134 * <strong>und</strong> garantieren, dass diese Eigenschaft erhalten bleibt.135 * Ist der Heap bereits voll, so wird eine Ausnahme ausgeloest.136 */137 public void einfuegenInHeap( Comparable c ) { einfuegenInHeap138 if( knotenZahl == array.length ) // Einfuegen ist unmoeglich139 throw new IllegalStateException( "Heap ist bereits voll." );140 knotenZahl++;141 int i = knotenZahl;142 for( ; i > 1 && c.compareTo( inhalt( i/2 ) ) < 0; i = i/2 ) {143 // wenn i nicht 1 ist <strong>und</strong> c kleiner als der Inhalt des Knotens i/2 ist:144 inhaltAendern( i, inhalt( i/2 ) );145 }146 inhaltAendern( i, c ); // das neue Objekt in den Knoten i einfuegen147 }148149 /** Hier werden nur die Knoten zwischen i <strong>und</strong> n (i 0 ) {162 inhaltAendern( j/2, inhalt( j ) );163 j = 2*j;164 }165 else166 break;167 }168 inhaltAendern( j/2, c );169 }170171 /** Gibt das im Wurzelknoten des Baums gespeicherte Objekt zurueck172 * <strong>und</strong> entfernt dann den Wurzelknoten.173 * Ist der Baum leer, so wird eine Ausnahme ausgeloest.174 * Wir setzen voraus, dass der Baum davor die Heap-Eigenschaft hat175 * <strong>und</strong> garantieren, dass diese Eigenschaft erhalten bleibt.


78 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG176 * Alle Knoteninhalte muessen paarweise mit compareTo vergleichbar sein.177 */178 public Object loeschenAusHeap( ) { loeschenAusHeap179 if( istLeer( ) ) // Loeschen ist unmoeglich180 throw new IllegalStateException( "Heap ist leer." );181 Comparable c = (Comparable)inhalt( 1 );182 inhaltAendern( 1, inhalt( knotenZahl ) );183 knotenZahl−−;184 if( !istLeer( ) )185 lasseEinsinken( 1, knotenZahl );186 return c;187 }188189 /** Stellt die Heap-Eigenschaft her.190 * Alle Knoteninhalte muessen paarweise mit compareTo vergleichbar sein.191 */192 public void erzeugeHeap( ) { erzeugeHeap193 for( int i = knotenZahl/2; i > 0; i−− )194 lasseEinsinken( i, knotenZahl );195 }196197 /** Die im Baum gespeicherten Knoteninhalte werden absteigend sortiert198 * bezueglich der durch compareTo gegebenen Ordnung.199 * Alle Knoteninhalte muessen daher paarweise mit compareTo vergleichbar sein.200 * Gibt das sortierte Array zurueck.201 */202 public Comparable[ ] heapSort( ) { heapSort203 erzeugeHeap( );204 for( int i = knotenZahl−1; i > 0; i−− ) {205 Object c = inhalt( i+1 );206 inhaltAendern( i+1, inhalt( 1 ) );207 inhaltAendern( 1, c );208 lasseEinsinken( 1, i );209 }210 return (Comparable[ ])array;211 }212213 public static void main( String[ ] args ) { main214 int groesse = 30;215 int max = 100;216 // Erzeuge einen Baum der Groesse groesse, dessen Knoten Zufallszahlen217 // zwischen 0 (inklusive) <strong>und</strong> max (exklusive) speichern:218 Integer[ ] array = new Integer[groesse];219 Random zufall = new Random( );220 for( int i = 0; i < groesse; i++ )221 array[i] = new Integer( zufall.nextInt( max ) );222 VollstaendigerBinaererBaum baum = new VollstaendigerBinaererBaum( array );223 System.out.println( baum ); // unsortiert224 baum.heapSort( );225 System.out.print( baum ); // sortiert226 } // main227228 } // class VollstaendigerBinaererBaum


2.4. DARSTELLUNGEN ALLGEMEINER BÄUME 79Ein Testlauf:Stufe 0: 40Stufe 1: 6 83Stufe 2: 43 26 45 19Stufe 3: 43 47 32 1 39 18 64 46Stufe 4: 30 2 37 87 99 72 63 5 86 71 45 2 94 44 92Stufe 0: 99Stufe 1: 94 92Stufe 2: 87 86 83 72Stufe 3: 71 64 63 47 46 45 45 44Stufe 4: 43 43 40 39 37 32 30 26 19 18 6 5 2 2 12.4 Darstellungen allgemeiner BäumeNach Definition 2.1.2 wird ein geordneter Baum B = (v, B 1 , . . . , B m ) aus einemKnoten v (der Wurzel) <strong>und</strong> m (≥ 0) direkten Teilbäumen B 1 , . . . , B m gebildet.Zur Implementierung von Bäumen gibt es nun eine Reihe von Möglichkeiten.Beschränkt man sich auf Bäume mit maximalem Knotengrad k, so ist eine Implementierungganz analog zu Programm 2.2.4 möglich. Jeder Knoten speichertdann statt 2 genau k Referenzen kind1, . . . ,kindk für seine maximal k Kinder.Diese Implementierung hat aber einige gravierende Nachteile:(1.) Die Anzahl der Kinder eines Knotens ist beschränkt.(2.) Haben nur wenige Knoten tatsächlich k Kinder, so wird viel Speicherplatzverschwendet.(3.) Selbst wenn alle Knoten, die nicht Blätter sind, k Kinder haben, wirdnoch viel Speicherplatz verschwendet. Es gilt nämlich folgende Aussage.Lemma 2.4.1. Sei B ein Baum vom Grad k mit n Knoten. Wird B wie obenbeschrieben implementiert, so sind n · (k − 1) + 1 der n · k Referenzen kind1,. . . , kindk gleich null.Beweis: Es gibt n · k Referenzen für die jeweiligen Kinder. Nur n − 1 dieserReferenzen sind tatsächlich in Gebrauch, d.h. n · k − (n − 1) = n · (k − 1) + 1davon sind null.Für k = 3 sind also (2n + 1)/(3n) ≈ 2/3 aller Referenzen nicht benutzt. Manwird daher diese Implementierung i.Allg. nicht verwenden.In Kapitel 4 werden wir Graphen betrachten. Da Bäume eine spezielle Artvon Graphen sind, kann man natürlich die Methoden zur Implementierung vonGraphen verwenden.


80 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNGSchließlich kann man (allgemeine) Bäume aber auch mittels binärer Bäumeimplementieren, wie im Folgenden noch ausgeführt wird.Definition 2.4.2. Eine Implementierung (allgemeiner) geordneter Bäume durchbinäre Bäume:Sei B = (v, B 1 , . . . , B m ) ein geordneter Baum. Nun definieren wir einen binärenBaum C, der genauso viele Knoten hat wie der Baum B. Die Wurzel von Centspricht der Wurzel v von B. Als linkes Kind erhält jeder Knoten in C einenVerweis auf den Knoten, der dem ersten Kind des entsprechenden Knotens inB entspricht. Als rechtes Kind erhält jeder Knoten in C einen Verweis auf denKnoten, der dem nächsten Geschwisterknoten des entsprechenden Knotens inB entspricht.Beispiel 2.4.3. Allgemeiner Baum B:Binärer Baum C: A B C D E F G H I J E A B C F G D H I JAls über Referenzen verb<strong>und</strong>ene Struktur erhalten wir folgende Darstellung:-✜✜✜E✧✧✧✧BAC❜ ❜❜❜❜-- F - - G - D -✧✧✧✧- H-I-J-


2.4. DARSTELLUNGEN ALLGEMEINER BÄUME 81Programm 2.4.4. Eine Implementierung allgemeiner geordneter Bäume:1 /** Die Klasse Binaerknoten implementiert Knoten eines binaeren Baums.2 * Jeder Knoten speichert ein beliebiges Objekt vom Typ Object,3 * zusaetzlich je eine Referenz auf das linke <strong>und</strong> das rechte Kind.4 */5 class Binaerknoten {wie in Programm 2.2.463 /** Gibt den hier wurzelnden Baum in Vorordnung mit Klammern64 * <strong>und</strong> Kommata aus.65 */66 public void druckeVorordnung( ) { druckeVorordnung67 System.out.print( this );68 if( linkesKind != null ) {69 System.out.print( "(" );70 linkesKind.druckeVorordnung( );71 Binaerknoten k = linkesKind.rechtesKind;72 while( k != null ) {73 System.out.print( "," );74 k.druckeVorordnung( );75 k = k.rechtesKind;76 }77 System.out.print( ")" );78 }79 }8081 } // class Binaerknoten828384 /** Die Klasse AllgemeinerBaum implementiert allgemeine Baeume85 * ueber Binaerbaeume. Ein AllgemeinerBaum besteht86 * einfach aus seinem Wurzelknoten vom Typ Binaerknoten.87 */88 public class AllgemeinerBaum {8990 /** Der Wurzelknoten. */91 private Binaerknoten wurzel;9293 /** Konstruiert einen Baum, der nur einen Wurzelknoten94 * mit Inhalt inhalt besitzt.95 */96 public AllgemeinerBaum( Object inhalt ) { AllgemeinerBaum97 wurzel = new Binaerknoten( inhalt );98 }99100 /** Konstruiert den leeren Baum. */101 public AllgemeinerBaum( ) { } AllgemeinerBaum102


82 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG103 /** Gibt den Wurzelknoten zurueck. */104 public Binaerknoten wurzel( ) { wurzel105 return wurzel;106 }107108 /** Test, ob der Baum leer ist. */109 boolean istLeer( ) { istLeer110 return wurzel == null;111 }112113 /** Haengt den Baum baum als neuen ersten direkten Teilbaum an.114 * Der bisherige i-te Teilbaum (wenn vorhanden) wird zum (i+1)-ten Teilbaum.115 * Ist der aktuelle Baum leer, so wird der Baum zu baum.116 * Gibt den modifizierten Baum zurueck.117 * Der Argumentbaum baum wird danach leer sein.118 *119 * Um zu verhindern, dass die Teilbaeume uebereinstimmen, fordern wir, dass120 * die beiden Wurzelreferenzen verschieden sind, ausser beide Baeume sind leer;121 * andernfalls wird eine Ausnahme ausgeloest.122 */123 AllgemeinerBaum alsErstenTeilbaumAnhaengen( AllgemeinerBaum baum ) { alsErstenTeilbaumAnhaengen124 if( !istLeer( ) && wurzel == baum.wurzel )125 throw new IllegalArgumentException( "Identische Baeume verknuepft." );126 if( istLeer( ) )127 wurzel = baum.wurzel;128 else {129 baum.wurzel.rechtesKindAendern( wurzel.linkesKind( ) );130 wurzel.linkesKindAendern( baum.wurzel );131 }132 baum.wurzel = null;133 return this;134 }135136 /** Gibt den Baum in Vorordnung mit Klammern <strong>und</strong> Kommata aus. */137 public void druckeVorordnung( ) { druckeVorordnung138 if( istLeer( ) )139 System.out.println( "Der Baum ist leer." );140 else {141 wurzel.druckeVorordnung( );142 System.out.println( );143 }144 }145146 public static void main( String[ ] args ) { main147 AllgemeinerBaum b1 =148 new AllgemeinerBaum( "*" ).149 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "B" ) ).150 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "A" ) );151 b1.druckeVorordnung( );152


2.4. DARSTELLUNGEN ALLGEMEINER BÄUME 83153 AllgemeinerBaum b2 =154 new AllgemeinerBaum( "A" ).155 alsErstenTeilbaumAnhaengen(156 new AllgemeinerBaum( "D" ).157 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "J" ) ).158 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "I" ) ).159 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "H" ) ) ).160 alsErstenTeilbaumAnhaengen(161 new AllgemeinerBaum( "C" ).162 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "G" ) ) ).163 alsErstenTeilbaumAnhaengen(164 new AllgemeinerBaum( "B" ).165 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "F" ) ).166 alsErstenTeilbaumAnhaengen( new AllgemeinerBaum( "E" ) ) );167 b2.druckeVorordnung( );168 } // main169170 } // class AllgemeinerBaumDer Testlauf druckt zuletzt den Baum aus Beispiel 2.4.3 in Vorordnung mitKlammern <strong>und</strong> Kommata:*(A,B)A(B(E,F),C(G),D(H,I,J))


84 KAPITEL 2. BÄUME UND IHRE IMPLEMENTIERUNG


Kapitel 3Datentypen zur Darstellungvon MengenDie Darstellung von Mengen ist offensichtlich eine gr<strong>und</strong>legende Aufgabe. EinigeHilfsmittel hierzu, nämlich Listen <strong>und</strong> Bäume, haben wir bereits kennengelernt.Hier werden wir nun einige weitere Datentypen zur Implementierung vonMengen betrachten, die sich durch ihre Operationen unterscheiden. Es geht dabeidarum, die Gr<strong>und</strong>bausteine so auszuwählen <strong>und</strong> zu verfeinern, dass spezielleOperationen effizient realisiert werden können.Wir werden im wesentlichen drei verschiedene Gruppen von Operationen aufMengen betrachten. Zunächst schauen wir uns Mengen an, für die die klassischenmathematischen Operationen Vereinigung, Durchschnitt <strong>und</strong> Differenzrealisiert werden sollen. Danach wenden wir uns Mengen zu, in denen Elementegesucht, eingefügt <strong>und</strong> entfernt werden sollen. Dieser Datentyp ist auch alsdictionary“ bekannt. Wegen seiner gr<strong>und</strong>legenden Bedeutung werden wir eine”ganze Reihe verschiedener Realisierungen untersuchen. Wir beschließen diesesKapitel mit der Betrachtung eines Datentyps für die Verwaltung von Partitionenendlicher Mengen, wobei das Vereinigen zweier Teilmengen <strong>und</strong> das Auffindender Teilmenge, die ein gegebenes Element enthält, im Vordergr<strong>und</strong> stehen.3.1 Mengen mit Vereinigung, Schnitt <strong>und</strong> DifferenzHier betrachten wir Mengen mit den klassischen mathematischen OperationenVereinigung, Durchschnitt <strong>und</strong> Differenz:M 1 ∪ M 2 = {x | x ∈ M 1 oder x ∈ M 2 },M 1 ∩ M 2 = {x | x ∈ M 1 <strong>und</strong> x ∈ M 2 },M 1 \ M 2 = {x | x ∈ M 1 <strong>und</strong> x ∉ M 2 }.Als weitere Operationen nehmen wir die Konstante ∅ (die leere Menge), dasEinfügen eines Elements, das Auflisten aller Elemente einer Menge <strong>und</strong> den85


86 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENTest auf Leerheit hinzu. Wir erhalten die folgende Signatur der DatenstrukturSet (set, boolean, item <strong>und</strong> list sind Sortensymbole):EMPTY : → setISEMPTY : set → booleanINSERT : set × item → setUNION : set × set → setINTERSECTION : set × set → setDIFFERENCE : set × set → setENUMERATE : set → listHier sollen drei Implementierungen der Datenstruktur Set vorgestellt werden:(a) Ist der Wertebereich Item hinreichend klein (etwa |Item| = 32), dannkann man jede Teilmenge S von Item = {a 1 , . . . , a 32 } durch einen Bitvektor(b 1 , . . . , b 32 ) darstellen, der gerade in ein Maschinenwort passt:{1 falls a i ∈ Sb i =0 falls a i /∈ SProgramm 3.1.1. Alle Mengenoperationen lassen sich sehr effizient umsetzen:1 import java.util.LinkedList; // fuer die Methode enumerate23 /** Die Klasse Set1 implementiert Teilmengen einer4 * 32-elementigen Menge {a 1,. . .,a 32}.5 * Intern wird eine Teilmenge als Zahl vom Typ int repraesentiert,6 * die wir als Bitvektor auffassen.7 */8 public class Set1 {910 /** Der Bitvektor als Zahl vom Typ int. */11 private int vector;1213 /** Konstruiert die leere Menge. */14 public Set1( ) { Set115 }1617 /** Konstruiert eine (tiefe) Kopie der Menge otherSet. */18 public Set1( Set1 otherSet ) { Set119 vector = otherSet.vector;20 }2122 /** Liefert eine neue leere Teilmenge zurueck. */23 public static Set1 empty( ) { empty24 return new Set1( );25 }26


3.1. MENGEN MIT VEREINIGUNG, SCHNITT UND DIFFERENZ 8727 /** Test, ob die Teilmenge leer ist. */28 public boolean isEmpty( ) { isEmpty29 return vector == 0;30 }3132 /** Fuegt das i-te Element a i in die Teilmenge ein. */33 public void insert( int i ) { insert34 if( i < 1 | | i > 32 )35 throw new IllegalArgumentException( "Element nicht im Universum." );36 vector |= 1 >= 1; // schiebe alle Bits in v um eine Stelle nach rechts63 }64 return list;65 }6667 /** Gibt eine String-Repraesentation der Teilmenge zurueck:68 * Die Liste [i 1,. . .,i n] repraesentiert die Teilmenge {a (i 1),. . .,a (i n)}.69 */70 public String toString( ) { toString71 return enumerate( ).toString( );72 }7374 public static void main( String[ ] args ) { main75 Set1 s = empty( );76 s.insert( 8 );77 s.insert( 1 );78 s.insert( 32 );79 s.insert( 5 );80 s.insert( 8 );


88 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN81 System.out.println( "s = " + s ); // s = [1, 5, 8, 32]82 Set1 s0 = new Set1( s );83 Set1 s1 = new Set1( s );84 Set1 s2 = new Set1( s );8586 Set1 t = empty( );87 t.insert( 7 );88 t.insert( 8 );89 t.insert( 1 );90 System.out.println( "t = " + t ); // t = [1, 7, 8]9192 s0.union( t );93 System.out.println( "Vereinigung:\t" + s0 ); // Vereinigung: [1, 5, 7, 8, 32]94 s1.intersection( t );95 System.out.println( "Durchschnitt:\t" + s1 ); // Durchschnitt: [1, 8]96 s2.difference( t );97 System.out.println( "Differenz:\t" + s2 ); // Differenz: [5, 32]98 } // main99100 } // class Set1Bemerkung: Mengen größerer Kardinalität können auf ähnliche Weise implementiertwerden, wenn längere Bitvektoren zur Verfügung stehen; allerdingsmuss dann in der Regel ein Effizienzverlust in Kauf genommen werden. In Javaeignet sich für diesen Zweck die Klasse java.util.BitSet.(b) Natürlich kann man eine Menge durch eine ungeordnete Liste implementieren,indem man jedem Element der Menge einen Listenknoten zuordnet, <strong>und</strong>diese Knoten in einer beliebigen Reihenfolge verkettet. Für eine Menge M 1 mitm Elementen <strong>und</strong> eine Menge M 2 mit n Elementen kosten die OperationenVereinigung, Schnitt <strong>und</strong> Differenz dann Rechenzeit O(m · n). Das Einfügen inM 1 kostet O(m) Schritte, ebenso wie das Aufzählen vom M 1 .(c) Ist auf der Gr<strong>und</strong>menge Item eine lineare Ordnung definiert (siehe Abschnitt2.3), so kann man die Elemente einer Teilmenge von Item so in einerlinearen Liste speichern, dass sie in aufsteigender Reihenfolge sortiert sind. Fürdie einzelnen Operationen ergeben sich dann folgende Abschätzungen für denRechenzeitbedarf:empty Liste initialisieren O(1)isEmpty Test auf leere Liste O(1)insert richtige Position suchen, dort einfügen O(m)unionintersection Listen parallel durchlaufen O(m + n)differenceenumerate Liste durchlaufen O(m)


3.1. MENGEN MIT VEREINIGUNG, SCHNITT UND DIFFERENZ 89Programm 3.1.2. Eine Implementierung könnte wie folgt aussehen:1 import java.util.LinkedList;2 import java.util.ListIterator;34 /** Die Klasse Set2 implementiert Mengen ueber dem5 * Typ Comparable als aufsteigend sortierte Listen.6 */7 public class Set2 {89 /** Die aufsteigend sortierte Liste. */10 private LinkedList list;1112 /** Konstruiert die leere Menge. */13 public Set2( ) { Set214 list = new LinkedList( );15 }1617 /** Konstruiert eine flache Kopie der Menge otherSet. */18 public Set2( Set2 otherSet ) { Set219 list = (LinkedList)otherSet.list.clone( );20 }2122 /** Liefert eine neue leere Menge zurueck. */23 public static Set2 empty( ) { empty24 return new Set2( );25 }2627 /** Test, ob die Menge leer ist. */28 public boolean isEmpty( ) { isEmpty29 return list.size( ) == 0;30 }3132 /** Fuegt das Objekt c vom Typ Comparable in die Menge ein.33 * Objekte, die bezueglich der durch compareTo definierten34 * Ordnung aequivalent sind, werden hoechstens einmal in die35 * Menge aufgenommen. Gibt true zurueck, wenn das Einfuegen36 * die Menge veraendert, sonst false.37 */38 public boolean insert( Comparable c ) { insert39 ListIterator it = list.listIterator( );40 while( it.hasNext( ) ) { // Position suchen41 if( c.compareTo( it.next( ) )


90 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN53 /** Vereinigt diese Menge mit der Argumentmenge. */54 public void union( Set2 otherSet ) { union55 ListIterator it = list.listIterator( );56 ListIterator otherIt = otherSet.list.listIterator( );57 while( it.hasNext( ) && otherIt.hasNext( ) ) {58 Comparable c = (Comparable)otherIt.next( );59 int comparison = c.compareTo( it.next( ) );60 if( comparison < 0 ) {61 it.previous( );62 it.add( c );63 continue;64 }65 if( comparison > 0 )66 otherIt.previous( );67 // if( comparison == 0 ): nichts zu tun68 }69 while( otherIt.hasNext( ) ) // it ist am Ende70 it.add( otherIt.next( ) );71 }7273 /** Schneidet diese Menge mit der Argumentmenge. */74 public void intersection( Set2 otherSet ) { intersection75 // als Uebung76 }7778 /** Bildet die Differenz dieser Menge mit der Argumentmenge. */79 public void difference( Set2 otherSet ) { difference80 // als Uebung81 }8283 /** Gibt eine Liste der Elemente der Menge zurueck. */84 public LinkedList enumerate( ) { enumerate85 return list;86 }8788 /** Gibt eine String-Repraesentation der Menge zurueck. */89 public String toString( ) { toString90 return list.toString( );91 }9293 public static void main( String[ ] args ) { main94 Set2 s = empty( );95 s.insert( new Integer( 8 ) );96 s.insert( new Integer( 1 ) );97 s.insert( new Integer( 32 ) );98 s.insert( new Integer( 5 ) );99 s.insert( new Integer( 8 ) );100 System.out.println( "s = " + s ); // s = [1, 5, 8, 32]101102 Set2 t = empty( );103 t.insert( new Integer( 7 ) );104 t.insert( new Integer( 8 ) );105 t.insert( new Integer( 1 ) );


3.2. SUCHBÄUME 91106 System.out.println( "t = " + t ); // t = [1, 7, 8]107108 s.union( t );109 System.out.println( "Vereinigung:\t" + s ); // Vereinigung: [1, 5, 7, 8, 32]110 } // main111112 } // class Set23.2 SuchbäumeDie am häufigsten auftretenden Anwendungen der Datenstruktur ”Menge“ benötigenaber nicht die mengentheoretischen Operationen Durchschnitt, Vereinigung<strong>und</strong> Differenz. Vielmehr stehen dabei die Operation Einfügen (insert),Löschen (delete) <strong>und</strong> Test auf Enthaltensein (isMember) im Vordergr<strong>und</strong>.Ein Datentyp, der diese Operationen zur Verfügung stellt, heißt Dictionary(Wörterbuch). In den folgenden Abschnitten werden wir verschiedene Realisierungendieses Datentyps kennenlernen. Hier betrachten wir eine Realisierungmittels binärer Bäume.Definition 3.2.1. Eine Knotenmarkierung für einen Baum mit KnotenmengeV über einer Menge Item ist eine Abbildung µ : V → Item.Definition 3.2.2. Sei Item eine durch ≤ linear geordnete Menge. Ein Suchbaumfür die Schlüssel a 1 , . . . , a n ∈ Item ist ein binärer Baum mit KnotenmengeV <strong>und</strong> einer Knotenmarkierung µ : V → Item, so dass folgende Bedingungenerfüllt sind:• µ ist eine Bijektion von V auf {a 1 , . . . , a n } (also ist |V | = n).• Für Knoten k, k ′ ∈ V giltµ(k ′ ) < µ(k) wenn k ′ im linken direkten Teilbaum unterhalb von k ist,µ(k) < µ(k ′ ) wenn k ′ im rechten direkten Teilbaum unterhalb von k ist.Beispiel 3.2.3. Bezüglich der lexikographischen Ordnung der Knotenmarkierungensind die beiden folgenden Bäume Suchbäume:✗✔JAN✖✕✗✔ ✧ ✧✧✧ ◗ ◗ ◗ ✗✔FEBMAR✖✕ ✖✕✗✔ ✗✔❅ ✗✔APR MAI SEP✖✕ ✖✕ ✖✕✗✔ ❚✗✔ ✔ ✗✔ ✔AUGJUN OKT✖✕ ✖✕ ✖✕✗✔ ❚ ✗✔ ✔ ✗✔ ✔DEZ JUL NOV✖✕ ✖✕ ✖✕


92 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN✗✔JUL✖✕ ❛ ❛❛❛❛✗✔ ✦ ✦✦✦✦✗✔DEZNOV✖✕✖✕✗✔ ❅ ✗✔ ✗✔ ❅ ✗✔AUG FEBMAI OKT✖✕ ✖✕ ✖✕ ✖✕✗✔ ❅ ✗✔ ✗✔ ❭ ✗✔❅ ✗✔APRJAN JUN MAR SEP✖✕ ✖✕ ✖✕ ✖✕ ✖✕Auf Suchbäumen wollen wir nun die folgenden Operationen realisieren: DieKonstante empty für den leeren Suchbaum, die Boolesche Operation isEmpty(Test auf Leerheit), die Operationen insert <strong>und</strong> delete (Einfügen bzw. Entferneneines Elements aus Item), <strong>und</strong> zuletzt noch die Boolesche Operation isMember(Test auf Enthaltensein). Tatsächlich werden wir von der Operation isMemberetwas mehr verlangen; ist nämlich im Suchbaum ein Knoten vorhanden, der mitdem gesuchten Element aus Item markiert ist, so soll isMember einen Verweisauf diesen Knoten liefern, ansonsten die Referenz null.Algorithmus 3.2.4. Wir verwenden die Implementierung binärer Bäume ausDefinition 2.2.3(b), bei der jeder Knoten aus drei Komponenten besteht:• dem gespeicherten Inhalt (hier: die Knotenmarkierung)• dem Verweis auf das linke Kind,• dem Verweis auf das rechte Kind.Die Operationen empty <strong>und</strong> isEmpty sind dann trivial. Das Suchen nach einemKnoten (Operation isMember) kann durch einfaches Absteigen im Baum realisiertwerden, wobei das gesuchte Element jeweils mit dem Inhalt des aktuellenKnotens verglichen wird.Die Operation insert arbeitet wie das Suchen. Entweder ist schon ein Knotenmit dem entsprechenden Inhalt vorhanden, so dass der Baum nicht verändertwird, oder aber es wird die Stelle gef<strong>und</strong>en, an die der entsprechende Knotenals Blatt eingefügt werden muss.Etwas komplizierter ist die Realisierung der Operation delete. Ist kein Knotenmit dem gesuchten Schlüssel vorhanden, dann geschieht nichts. Ist der gesuchteKnoten ein Blatt, so kann er einfach entfernt werden. Ist er aber ein innererKnoten, so müssen wir die Struktur des Suchbaums nach dem Löschen wiederherstellen.1. Fall: delete(b) . . . b a . . . . . .


3.2. SUCHBÄUME 93In diesem Fall wird einfach der Knoten b entfernt. Der Verweis auf b wirdumgesetzt auf a . Falls b nur ein rechtes Kind hat, verfährt man entsprechend.2. Fall: delete(b). . .✗✔ ✪b✖✕✗✔ ✧ ✧ ◗ ◗◗ ✗✔ad✖✕ ✖✕✱✪✪ ❡ ✱ ❅ ✂❇✂❇. . . . . .✂ ❇ ✂✂✂❇❇T 1 ✂ T 2❇ ✂❇❇Um die Struktur eines Suchbaums auch nach dem Löschen von b zu erhalten,müssen wir in den Knoten, der jetzt b enthält, einen geeigneten Wert schreiben.Hierzu bietet sich der Knoten aus dem linken direkten Teilbaum von b an, derden größten Schlüsselwert enthält (d.h. der dort bzgl. Zwischenordnung letzteKnoten).Fall 2.(a):✗✔✖✕✗✔ ✧ ✧✧ ◗ ◗◗ ✗✔ad✖✕ ✖✕✱✪✪ ❡ ✱ ❅ ✂❇♠. . . . . .✂ ❇✂T 1✂❇❇. . .♠✔✗✔. . . c✖✕✗✔c✖✕✗✔ ✧ ✧✧ ◗ ◗◗ ✗✔ad✖✕ ✖✕✱✪✪ ❡ ✱ ❅ ✂❇♠. . . . . .✂ ❇✂T 1✂❇❇. . .♠✔. . .Fall 2.(b):✗✔✗✔c✖✕✖✕✗✔ ✗✔✗✔ ✧ ✧✧ ◗✧ ✧✧ ◗ ◗◗◗◗ ✗✔adad✖✕ ✖✕✖✕ ✖✕✱✱✪✪ ❡ ✱ ❅ ✪✪ ❡ ✱ ❅ ✂❇♠. . . . . . ✂❇♠. . . . . .✂ ❇✂ ❇✂T 1 ✂✂T❇. . .1♠✂❇. . .♠✔✗✔✔ ❅. . . c. . . ✂❇✖✕✂ ❇✓✂T ✓✂3❇✂❇✂ ❇✂T ✂3❇


94 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENMan kann leicht zeigen, dass jeder so entstandene Baum ein Suchbaum für dieverbliebenen Schlüssel ist.Wir sehen also, dass jede der Operationen isMember, insert <strong>und</strong> delete imschlechtesten Fall so viele Schritte braucht, wie der aktuelle Suchbaum hochist. Enthält der Baum n Knoten, dann kann er die Höhe n − 1 haben, d.h. imschlechtesten Fall kostet jede der obigen Operationen O(n) Schritte. Fügt manin den anfänglich leeren Baum n Knoten ein, so kostet dies im schlechtestenFalln∑c · i ∈ Θ(n 2 )i=1Schritte. Wenn man annimmt, dass alle Reihenfolgen für das Einfügen der nElemente gleich wahrscheinlich sind, kann man aber zeigen, dass im MittelO(log n) Schritte pro Operation ausreichen.Programm 3.2.5. Eine Implementierung von Suchbäumen:wie in Programm 2.2.45 class Binaerknoten {wie in Programm 2.2.463 /** Zu Testzwecken: Gibt den hier wurzelnden Baum in Vorordnung64 * mit Klammern <strong>und</strong> Kommata aus.65 */66 public void druckeVorordnung( ) { druckeVorordnung67 System.out.print( this + "(" );68 if( linkesKind != null )69 linkesKind.druckeVorordnung( );70 System.out.print( "," );71 if( rechtesKind != null )72 rechtesKind.druckeVorordnung( );73 System.out.print( ")" );74 }7576 } // class Binaerknoten777879 /** Die Klasse Suchbaum implementiert binaere Suchbaeume mittels Referenzen. */80 public class Suchbaum {8182 /** Der Wurzelknoten. */83 private Binaerknoten wurzel;8485 /** Konstruiert einen Baum, der nur einen Wurzelknoten86 * mit Schluessel key besitzt.87 */88 public Suchbaum( Object key ) { Suchbaum89 wurzel = new Binaerknoten( key );90 }


3.2. SUCHBÄUME 959192 /** Konstruiert den leeren Baum. */93 public Suchbaum( ) { Suchbaum94 }9596 /** Gibt den Wurzelknoten zurueck. */97 public Binaerknoten wurzel( ) { wurzel98 return wurzel;99 }100101 /** Test, ob der Baum leer ist. */102 boolean istLeer( ) { istLeer103 return wurzel == null;104 }105106 /** Gibt den Elternknoten des Knotens mit maximalem Schluessel107 * im linken direkten Teilbaum von k zurueck. Wir setzen voraus,108 * dass dieser linke Teilbaum nicht leer ist.109 */110 private Binaerknoten predMaximum( Binaerknoten k ) { predMaximum111 Binaerknoten pred = k;112 Binaerknoten node = k.linkesKind( ); // existiert nach Voraussetzung113 while( node.rechtesKind( ) != null ) {114 pred = node;115 node = node.rechtesKind( );116 }117 return pred;118 }119120 /** Diese Klasse dient als Rueckgabetyp der Methode searchKey. */121 private class SearchKeyResult {122 Binaerknoten pred, node;123 int direction;124 SearchKeyResult( Binaerknoten p, Binaerknoten n, int d ) { SearchKeyResult125 pred = p;126 node = n;127 direction = d;128 }129 }130131 /** Gibt ein Tripel (pred, node, direction) vom Typ SearchKeyResult zurueck.132 * Wenn pred <strong>und</strong> node beide null sind, dann ist der Baum leer.133 * Wenn node nicht null ist:134 * key ist (aequivalent zum) Schluessel in Knoten node,135 * pred ist dessen Elternknoten, falls vorhanden, sonst null.136 * Wenn node null ist, pred nicht null ist <strong>und</strong> direction < 0:137 * key kommt nicht als Schluessel (bzgl. Aequivalenz) vor;138 * die korrekte Position fuer das Blatt mit Schluessel key139 * ist das (noch nicht vorhandene) linke Kind von pred.140 * Wenn node null ist, pred nicht null ist <strong>und</strong> direction > 0:141 * analog mit rechtem Kind von pred.142 */


96 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN143 private SearchKeyResult searchKey( Comparable key ) { searchKey144 if( istLeer( ) )145 return new SearchKeyResult( null, null, 0 );146 Binaerknoten pred = null;147 Binaerknoten node = wurzel;148 int direction = 0;149 do {150 int vergleich = key.compareTo( node.inhalt( ) );151 if( vergleich == 0 )152 return new SearchKeyResult( pred, node, direction );153 pred = node;154 if( vergleich < 0 ) {155 node = node.linkesKind( );156 direction = −1;157 }158 else { // vergleich > 0159 node = node.rechtesKind( );160 direction = +1;161 }162 }163 while( node != null );164 return new SearchKeyResult( pred, node, direction );165 }166167 /** Gibt einen Knoten mit einem zu key aequivalenten Schluessel168 * zurueck, falls ein solcher Knoten vorhanden ist, sonst null.169 */170 public Binaerknoten isMember( Comparable key ) { isMember171 return searchKey( key ).node;172 }173174 /** Fuegt einen Knoten mit Schluessel key in den Suchbaum ein. */175 public void insert( Comparable key ) { insert176 SearchKeyResult r = searchKey( key );177 if( r.pred == null && r.node == null ) // Baum ist leer178 wurzel = new Binaerknoten( key );179 else if( r.direction < 0 )180 r.pred.linkesKindAendern( new Binaerknoten( key ) );181 else if( r.direction > 0 )182 r.pred.rechtesKindAendern( new Binaerknoten( key ) );183 }184185 /** Entfernt den Knoten mit zu key aequivalentem Schluessel, falls vorhanden.186 */187 public void delete( Comparable key ) { delete188 SearchKeyResult r = searchKey( key );189 Binaerknoten k = r.node;190 if( k != null ) { // k hat Schluessel key191 if( k.linkesKind( ) == null | | k.rechtesKind( ) == null ) { // Fall 1192 Binaerknoten einzigesKind =193 k.linkesKind( ) == null ? k.rechtesKind( ) : k.linkesKind( );194 if( r.pred == null ) // k ist Wurzel195 wurzel = einzigesKind;


3.2. SUCHBÄUME 97196 else // r.pred ist Elternknoten von k197 if( r.direction < 0 )198 r.pred.linkesKindAendern( einzigesKind );199 else // r.direction > 0200 r.pred.rechtesKindAendern( einzigesKind );201 }202 else { // Fall 2203 Binaerknoten predMax = predMaximum( k );204 Binaerknoten maximum =205 predMax == k ? k.linkesKind( ) : predMax.rechtesKind( );206 k.inhaltAendern( maximum.inhalt( ) );207 if( predMax == k )208 k.linkesKindAendern( maximum.linkesKind( ) );209 else210 predMax.rechtesKindAendern( maximum.linkesKind( ) );211 }212 }213 }214215 /** Gibt die Schluessel des Suchbaums in Zwischenordnung aus,216 * also als aufsteigend geordnete Folge.217 */218 public void druckeZwischenordnung( ) { druckeZwischenordnung219 if( istLeer( ) )220 System.out.println( "Der Baum ist leer." );221 else {222 wurzel.druckeZwischenordnung( );223 System.out.println( );224 }225 }226227 /** Gibt die Schluessel des Suchbaums in Vorordnung mit Klammern228 * <strong>und</strong> Kommata aus.229 */230 public void druckeVorordnung( ) { druckeVorordnung231 if( istLeer( ) )232 System.out.println( "Der Baum ist leer." );233 else {234 wurzel.druckeVorordnung( );235 System.out.println( );236 }237 }238239 public static void main( String[ ] args ) { main240 Suchbaum b = new Suchbaum( );241 b.insert( "JAN" ); b.insert( "FEB" ); b.insert( "APR" );242 b.insert( "AUG" ); b.insert( "DEZ" ); b.insert( "MAR" );243 b.insert( "MAI" ); b.insert( "JUN" ); b.insert( "JUL" );244 b.insert( "SEP" ); b.insert( "OKT" ); b.insert( "NOV" );245 b.druckeZwischenordnung( ); b.druckeVorordnung( );246 b.delete( "FEB" );247 b.druckeZwischenordnung( ); b.druckeVorordnung( );248 b.delete( "JAN" );


98 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN249 b.druckeZwischenordnung( ); b.druckeVorordnung( );250 b.delete( "MAR" );251 b.druckeZwischenordnung( ); b.druckeVorordnung( );252 }253254 } // class Suchbaum3.3 Gewichtsbalancierte BäumeWie wir gesehen haben, sind Suchbäume im schlechtesten Fall genauso ineffizientwie lineare Listen. Andererseits kann man n Schlüssel in einem binären Suchbaumder Höhe ⌈log(n+1)⌉−1 unterbringen (Lemma 2.1.6.). Für einen solchenSuchbaum kosten die Operationen isMember, insert <strong>und</strong> delete nur O(log n)Schritte. Damit stellt sich die Frage, ob man nicht mit solchen Suchbäumenauskommen kann, die für n Schlüssel nur Höhe ≤ c · log n für eine festeKonstante c haben. Solche Suchbäume wollen wir ausgeglichen oder balanciertnennen. Es gibt verschiedene Möglichkeiten, Suchbäume zu balancieren. In diesemAbschnitt wollen wir uns die sogenannten BB[α]-Bäume anschauen, diegewichtsbalanciert sind.Für einen binären Baum T bezeichne |T | die Anzahl seiner Knoten, T l seinenlinken <strong>und</strong> T r seinen rechten direkten Teilbaum.T :✗✔✁✁✁✁✖✕ ◗◗◗✑ ✑✑✑ ◗✁❆✁❆❆✁❆❆✁✁❆ ✁T l❆❆❆❆Definition 3.3.1. Die Wurzelbalance eines binären Baums T ist der Wertρ(T ) = |T (l| + 1= 1 − |T )r| + 1.|T | + 1 |T | + 1Ein Baum ist von beschränkter Balance α, falls für jeden seiner Teilbäume T ′α ≤ ρ(T ′ ) ≤ 1 − αgilt. BB[α] ist die Menge aller binären Bäume von beschränkter Balance α; wirnennen diese Bäume auch BB[α]-Bäume.In diesem Abschnitt bezeichne α eine festgewählte reelle Zahl mitT r14 < α ≤ 1 − 1 √2≈ 0, 29.


3.3. GEWICHTSBALANCIERTE BÄUME 99Bemerkung 3.3.2. Wenn wir in jedem Knoten k zusätzlich zum Inhalt <strong>und</strong> zuden Verweisen auf die beiden Kinder noch die Anzahl der Knoten im Teilbaummit Wurzel k speichern, so können wir ρ in Zeit O(1) ausrechnen.Beispiel 3.3.3. T :✗✔5✟✖✕✗✔ ✟✟❍ ❍❍ ✗✔2 7✖✕ ✖✕✗✔ ✗✔ ❅ ✗✔ ✗✔ ❅1 4 6 8✖✕ ✖✕ ✖✕ ✖✕✗✔ ✔3✖✕Sei T i der Teilbaum mit Wurzel i. Dann gilt:i 1 2 3 4 5 6 7 8ρ(T i ) 1/2 2/5 1/2 2/3 5/9 1/2 1/2 1/2Für alle i gilt also 1/3 ≤ ρ(T i ) ≤ 1 − 1/3, daher ist T ein BB[α]-Baum.Satz 3.3.4. Sei T ein BB[α]-Baum mit n Knoten <strong>und</strong> Höhe h. Dann gilth ≤log(n + 1) − 1− log(1 − α) ,d.h. BB[α]-Bäume haben nur logarithmische Höhe.Beweis: Sei v ein Knoten von T der Tiefe h <strong>und</strong> sei v 0 , v 1 , . . . , v h = v der Wegvon der Wurzel v 0 zum Knoten v. Für 0 ≤ i ≤ h sei T i der Teilbaum mit Wurzelv i , <strong>und</strong> es sei w i = |T i |.Behauptung: w i+1 + 1 ≤ (1 − α)(w i + 1) für 1 ≤ i < h.• 1. Fall: v i+1 ist das linke Kind von v i , d.h. T i+1 ist der linke direkteTeilbaum von T i . Da T ∈ BB[α] ist, folgt|T i+1 | + 1|T i | + 1 = w i+1 + 1w i + 1 = ρ(T i) ≤ 1 − α.• 2. Fall: v i+1 ist das rechte Kind von v i , d.h. T i+1 ist der rechte direkteTeilbaum von T i . Dann folgtα ≤ ρ(T i ) = 1 − |T i+1| + 1|T i | + 1 = 1 − w i+1 + 1w i + 1 .


100 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENIn beiden Fällen gilt also die Behauptung. Damit erhalten wir2 ≤ w h + 1 ≤ (1 − α)(w h−1 + 1) ≤ · · · ≤ (1 − α) h (w 0 + 1) = (1 − α) h (n + 1),also 1 ≤ h log(1 − α) + log(n + 1), d.h. h ≤ (log(n + 1) − 1)/ − log(1 − α).Für α = 1 − 1/ √ 2 ergibt Satz 3.3.4 folgende Abschätzung für die Höhe h:log(n + 1) − 1h ≤log √ = 2(log(n + 1) − 1).2Da immer h ≥ ⌈log(n + 1)⌉ − 1 gilt (Lemma 2.1.5), ist ein BB[α]-Baum alsoimmer höchstens doppelt so hoch wie überhaupt minimal möglich.Durch das Einfügen oder das Löschen von Knoten kann ein BB[α]-Baum ausder Balance geraten. Wir werden daher im folgenden Operationen betrachten,die einen aus der Balance geratenen Baum wieder in einen BB[α]-Baum transformieren.Dies sind die folgenden vier Operationen:Rotation nach links:✗✔T :✖✕x ρ x✟ ◗✟✟ ◗◗ ✗✔✂❇✂ ❇y ρ y✂ a✖✕✂❇❇ ✪✪ ❡ ✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇✂❇✂ ❇✂ a✂❇❇ρ x= Teilbaum mit a Knoten= Wurzelbalance im Knoten x in T✗✔T ′ : y ρ ′ y✟✖✕◗✗✔ ✟✟ ◗◗ρ ′ ✂❇x x✂✖✕✪✪ ❡ ✂❇c✂❇❇✂❇✂❇✂ ❇ ✂✂✂❇❇a ✂ b❇ ✂❇❇ρ ′ x = Wurzelbalance im Knoten x in T ′Rotation nach rechts:✗✔T :y ρ y✟✖✕◗✗✔ ✟✟ ◗◗ρ ✂❇x x✂ ❇✖✕✓❙ ✂ c✂❇❇✂❇✂❇✂ ❇✂ a✂❇✂ ❇✂ b❇ ✂❇❇T ′ :✗✔ρ ′ x x✧✖✕❜✧✧ ❜❜ ✗✔✂❇y ρ✂ ❇′ y✂ a✂❇✖✕❇ ✪ ❡ ✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇


3.3. GEWICHTSBALANCIERTE BÄUME 101Doppelrotation nach links:T :✗✔✗✔x ρ xT ′ : z ρ ′ z✖✕ ◗ ✖✕✟✟ ◗◗✑ ❜✟ ✗✔✗✔ ✑✑ ❜❜ ✗✔✂❇✂ ❇y ρ yρ ′✂✖✕✖✕ ✖✕✂❇x x y ρ ′ ya❇ ✗✔ ❡ ✓ ❙ ✪ ❡ ρ z z ✂❇✂❇✂❇✂❇✂❇✂✖✕✂✂❇❇✂✂ ❇ ✂✂❇✂❇ ✂✂❇❇✂✂❇❇ ✂✂❇✔❏ da b ❇ c d❇ ❇ ✂❇❇✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇Doppelrotation nach rechts:✗✔T : ρ y y✟✖✕◗✗✔ ✟✟ ◗◗ρ x x✂❇✂ ❇✖✕ ✂ d✱❧✗✔✂❇❇✂❇✂ ❇z ρ z✂ a ✖✕✂❇❇✔❏ ✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇✗✔T ′ : z ρ ′ z✑✖✕❜✗✔ ✑✑ ❜❜ ✗✔ρ ′ x x y ρ ′ y✖✕ ✖✕✓❙ ✪ ❡ ✂❇✂❇✂❇✂❇✂ ❇ ✂ ✂ ❇ ✂✂✂❇❇✂✂✂❇❇a ✂ b❇ ✂❇ c d❇ ❇ ✂❇❇Bemerkung 3.3.5. (1) Ist T ein Suchbaum für gewisse Schlüssel, so ist auchT ′ wieder ein Suchbaum für diese Schlüssel.(2) Eine Doppelrotation nach links ist keine doppelte Rotation nach links! Einesolche Operation im Knoten x entspricht stattdessen einer Rotation nach rechtsim rechten Kind von x gefolgt von einer Rotation nach links in x.Lemma 3.3.6. (a) Entsteht T ′ aus T durch Rotation nach links, so giltρ ′ x =ρ xρ x + (1 − ρ x )ρ y<strong>und</strong> ρ ′ y = ρ x + (1 − ρ x )ρ y .(b) Entsteht T ′ aus T durch Doppelrotation nach links, so giltρ ′ x =ρ xρ x + (1 − ρ x )ρ y ρ z, ρ ′ y = ρ y(1 − ρ z )1 − ρ y ρ z<strong>und</strong> ρ ′ z = ρ x + (1 − ρ x )ρ y ρ z .


102 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENBeweis: (a)T :✗✔✖✕x ρ x✟ ◗✟✟ ◗◗ ✗✔✂❇✂ ❇y ρ y✂ a✖✕✂❇❇ ✪✪ ❡ ✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇✗✔T ′ : y ρ ′ y✟✖✕✗✔ ✟✟ ◗ ◗ρ ′ ✂❇x x✂✖✕✪✪ ❡ ✂❇c✂❇❇✂❇✂❇✂ ❇ ✂✂✂❇❇a ✂ b❇ ✂❇❇Mit ρ x = (a + 1)/(a + b + c + 3) <strong>und</strong> ρ y = (b + 1)/(b + c + 2) erhalten wiralso(b)T :ρ x + (1 − ρ x )ρ y =a + 1a + b + c + 3 + b + c + 2a + b + c + 3 · b + 1b + c + 2 = a + b + 2a + b + c + 3 = ρ′ y,ρ xρ ′ y=a + 1a + b + c + 3 · a + b + c + 3 = a + 1a + b + 2 a + b + 2 = ρ′ x.✗✔✗✔x ρ xT ′ : z ρ ′ z✖✕ ◗ ✖✕✟✟ ◗◗✑ ❜✟ ✗✔✗✔ ✑✑ ❜❜ ✗✔✂❇✂ ❇y ρ yρ ′✂✖✕✖✕ ✖✕✂❇x x y ρ ′ ya❇ ✗✔ ❡ ✓ ❙ ✪ ❡ ρ z z ✂❇✂❇✂❇✂❇✂❇✂✖✕✂✂❇❇✂✂ ❇ ✂✂❇✂❇ ✂✂❇❇✂✂❇❇ ✂✂❇✔❏ da b ❇ c d❇ ❇ ✂❇❇✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇Mit ρ x = (a + 1)/(a + b + c + d + 4), ρ y = (b + c + 2)/(b + c + d + 3) <strong>und</strong>ρ z = (b + 1)/(b + c + 2) erhalten wirρ y ρ z =b + 1b + c + d + 3<strong>und</strong> (1 − ρ x )ρ y ρ z =alsoa + b + 2ρ x + (1 − ρ x )ρ y ρ z =a + b + c + d + 4 = ρ′ z,ρ y (1 − ρ z )=1 − ρ y ρ zρ xρ ′ z=c + 1b + c + d + 3 · b + c + d + 3c + d + 2b + 1a + b + c + d + 4= c + 1c + d + 2 = ρ′ y,a + 1a + b + c + d + 4 · a + b + c + d + 4 = a + 1a + b + 2 a + b + 2 = ρ′ x.


3.3. GEWICHTSBALANCIERTE BÄUME 103Lemma 3.3.7. (a) Entsteht T ′ aus T durch Rotation nach rechts, so giltρ ′ x = ρ x ρ y <strong>und</strong> ρ ′ y = (1 − ρ x)ρ y1 − ρ x ρ y.(b) Entsteht T ′ aus T durch Doppelrotation nach rechts, so giltρ ′ x =ρ xρ x + (1 − ρ x )ρ z, ρ ′ y =(1 − ρ x )ρ y (1 − ρ z )(1 − ρ y ) + (1 − ρ x )ρ y (1 − ρ z ) ,ρ ′ z = ρ y ρ z + ρ x ρ y (1 − ρ z ).Beweis: als Übung.Lemma 3.3.8. SeiT :✗✔✖✕x ρ x✟ ◗✟✟ ◗◗ ✗✔✂❇✂ ❇y ρ y✂ a✖✕✂❇❇ ✪✪ ❡ ✂❇✂❇✂ ❇ ✂✂✂❇❇b ✂ c❇ ✂❇❇ein Baum, für den jeder echte Teilbaum in BB[α] ist, er selbst aber nicht wegenDann entsteht daraus ein BB[α]-Baumα · (1 − α) ≤ ρ x < α.(a) für ρ y > (1 − 2α)/(1 − α) durch Doppelrotation nach links,(b) für ρ y ≤ (1 − 2α)/(1 − α) durch Rotation nach links.Bemerkung: Bedingung (∗) besagt, dass die Wurzelbalance zu klein gewordenist, entweder dadurch, dass im linken direkten Teilbaum ein Knoten entferntwurde, oder dadurch, dass in den rechten direkten Teilbaum ein Knoten eingefügtwurde. Lemma 3.3.8 besagt, dass durch eine Rotation oder Doppelrotationnach links wieder ein BB[α]-Baum entsteht.Beweis: (a) Es ist ρ y = (b + 1)/(b + c + 2) > (1 − 2α)/(1 − α). Die Funktionf(t) = (1−2t)/(1−t) ist im Intervall [0, 1/2] monoton fallend. Mit α ≤ 1−1/ √ 2folgt1 − 2α1 − α = f(α) ≥ f(1 − 1/√ 2) = 1 − 2 + √ 21 − 1 + 1/ √ 2 = 2 − √ 2 > 1 2 ,(∗)also b + 1 > (b + c + 2)/2, was b > c ≥ 0 liefert. Daher ist der Teilbaum✂✂ ❇b✂❇ ❇nicht leer, hat also die Gestalt✂❇✂d✂❇ ❇z♥✂❇✂✂❇ .e❇


104 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN✗✔T :✖✕x ρ x✟ ◗✟✟ ◗◗ ✗✔✂❇✂ ❇y ρ y✂ a✖✕✂❇❇ ✗✔ ❡ ρ z z ✂❇✂ ❇✖✕✂✔❏ c✂❇❇✂❇✂❇✂ ❇ ✂✂✂❇❇d ✂ e❇ ✂❇❇Doppelrotationnach links✗✔T ′ : z ρ ′ z✑✖✕❜✗✔ ✑✑ ❜❜ ✗✔x ρ ′ xy ρ ′ y✖✕ ✖✕✓❙ ✪ ❡ ✂❇✂❇✂❇✂❇✂ ❇ ✂ ✂ ❇ ✂✂✂❇❇✂✂✂❇❇a ✂ d❇ ✂❇ e c❇ ❇ ✂❇❇✂❇✂❇Nach Voraussetzung sind die Teilbäume✂✂❇ ✂❇✂✂✂❇ ✂❇a , d , ✂ e❇ ❇ ✂❇ <strong>und</strong>c❇ ✂❇ ❇ BB[α]-Bäume.Damit bleibt α ≤ ρ ′ x, ρ ′ y, ρ ′ z ≤ 1 − α zu zeigen. Einerseits giltρ xρ ′ x =ρ x + (1 − ρ x )ρ y ρ z= 1/(1 + 1 − ρ xρ x· ρ y · ρ z )< 1/(1 + 1 − αα· 1 − 2α1 − α(nach Lemma 3.3.6(b))· α) = 1/(2(1 − α)) ≤ 1 − α.Die erste Ungleichung gilt, weil (1 − t)/t monoton fallend auf [0, 1] ist <strong>und</strong> mitρ x < α, wegen ρ y > (1 − 2α)/(1 − α) <strong>und</strong> wegen ρ z ≥ α; die zweite Ungleichunggilt wegen α ≤ 1 − 1/ √ 2. Andererseits giltρ ′ x = 1/(1 + 1 − ρ x· ρ y · ρ z )ρ x1 − α(1 − α)≥ 1/(1 + · (1 − α) 2 )α · (1 − α)α=α + (1 − α) − α(1 − α) 2 = α1 − α(1 − α) 2 ≥ α.Hier gilt die Ungleichung, weil (1 − t)/t monoton fallend auf [0, 1] ist <strong>und</strong> mitρ x ≥ α(1 − α), wegen ρ y ≤ 1 − α <strong>und</strong> wegen ρ z ≤ 1 − α. Analog können dieentsprechenden Aussagen für ρ ′ y <strong>und</strong> ρ ′ z gezeigt werden.(b)✗✔✗✔T : ✖✕x ρ xT ′ : y ρ ′ y◗ ✖✕✟✟✟◗◗◗✟ ✗✔✗✔ ✟✟ ◗◗✂❇✂ ❇y ρ ✂❇y✂✖✕✖✕✂❇Rotationx ρ ′ x ✂❇ ✪✪ ❡ ✪✪ ❡ ✂❇anach linksc✂❇❇✂❇✂❇✂❇✂❇✂ ❇ ✂✂✂❇❇✂❇ ✂❇✂ ❇ ✂✂❇✂❇❇b ca ✂ b❇ ✂❇❇Analog muss α ≤ ρ ′ x, ρ ′ y ≤ 1 − α nachgewiesen werden.


3.3. GEWICHTSBALANCIERTE BÄUME 105Lemma 3.3.8 behandelt den Fall, dass die Balance in einem Knoten zu kleingeworden ist. Im folgenden Lemma betrachten wir den anderen Fall, nämlichdass die Balance in einem Knoten zu groß geworden ist.✗✔Lemma 3.3.9. Sei T :x ρ x✖✕✗✔ ✑ ✑✑ ❍ ❍❍y ρ y✖✕❡✂ ✂✂❇ ❇❇❇c✪✪✂ ❇❇✂ ✂✂❇ ❇❇❇✂ ✂✂a ❇❇ b ✂ ✂ein Baum, für den jeder echte Teilbaum in BB[α] ist, er selbst aber nicht wegenDann entsteht daraus ein BB[α]-Baumρ x > 1 − α.(a) für ρ y > α/(1 − α) durch Rotation nach rechts,(b) für ρ y ≤ α/(1 − α) durch Doppelrotation nach rechts.Beweis: Analog zum Beweis von Lemma 3.3.8.Beispiel 3.3.10. Sei α = 0, 29 <strong>und</strong> damit 1 − α = 0, 71. Aus dem SuchbaumT 0 : 2 1 5 4 6entstehe durch Einfügen eines Knotens mit Schlüssel 7 der SuchbaumT 1 : 2 1 5 4 6 7Der Suchbaum war zuerst in BB[α], durch das Einfügen ist er aber aus derBalance geraten wegen 2/7 < α:i 1 2 4 5 6 7T 0 : ρ i 1/2 1/3 1/2 1/2 1/2 —T 1 : ρ i 1/2 2/7 1/2 2/5 1/3 1/2


106 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENEs ist α(1 − α) < 2/7 <strong>und</strong> ρ 5 = 2/5 ≤ (1 − 2α)/(1 − α) (= 0, 42/0, 71 ≈ 0, 59).Wir wenden nach Lemma 3.3.8(b) auf T 1 eine Rotation nach links an <strong>und</strong>erhalten einen BB[α]-Baum: 5 2 6 1 4 7i 1 2 4 5 6 7ρ i 1/2 1/2 1/2 4/7 1/3 1/2Algorithmus 3.3.11. Einfügen in BB[α]-Bäume:1 void insert( Comparable key ) { insert2 (1.) Bestimme die Position des einzufügenden Knotens im Baum.3 (2.) Füge einen neuen Knoten mit Schlüssel key ein.4 (3.) Gehe den Weg von diesem neuen Knoten zurück zur Wurzel;5 für jeden Knoten k auf dem Weg:6 k.groesseAendern( +1 );7 if( ρ(k) < α )8 if( ρ(k.rechtesKind( )) > (1 − 2α)/(1 − α) )9 Doppelrotation nach links in k;10 else11 Rotation nach links in k;12 if( ρ(k) > 1 − α )13 if( ρ(k.linkesKind( )) > α/(1 − α) )14 Rotation nach rechts in k;15 else16 Doppelrotation nach rechts in k;17 }Zeitbedarf von insert: O(Höhe von T ) = O(log |T |) (nach Satz 3.3.4).Lemma 3.3.12. Sei T ein BB[α]-Suchbaum <strong>und</strong> sei key ein Schlüssel, der inT nicht vorkommt. Dann erzeugt insert( key ) aus T einen BB[α]-Suchbaum,der alle Schlüssel aus T <strong>und</strong> den Schlüssel key enthält.Beweis: Sei v 0 , v 1 , . . . , v k der Weg in T von der Wurzel zu dem Knoten v k , alsdessen Kind der Knoten mit Schlüssel key in Schritt (2.) eingefügt wird, <strong>und</strong>sei S der entstehende Baum. Dann ist S ein Suchbaum, der alle Schlüssel ausT <strong>und</strong> den Schlüssel key enthält.Sei T i der Teilbaum von T mit Wurzel v i , sei S i der Teilbaum von S mit Wurzelv i , <strong>und</strong> seien S (l)i<strong>und</strong> S (r)ider linke bzw. rechte direkte Teilbaum von S i . Allevon S 0 , . . . , S k verschiedenen Teilbäume von S sind Teilbäume von T <strong>und</strong> damitBB[α]-Bäume. Gilt nun stets α ≤ ρ(S i ) ≤ 1 − α, so sind alle Teilbäume von SBB[α]-Bäume, also auch S. Andernfalls sei j maximal mit ρ(S j ) /∈ [α, 1 − α].


3.3. GEWICHTSBALANCIERTE BÄUME 107Dann ist der neue Knoten offensichtlich in diesem Teilbaum S j . Angenommen,der neue Knoten ist in S (r)j. Dann ist S (l)jein Teilbaum von T <strong>und</strong> es giltdamitρ(S j ) = |S(l) j| + 1|S j | + 1 < |S(l) j| + 1|S j | − 1 + 1 = ρ(T j),1ρ(S j ) = |S j| + 1|S (l)j| + 1 = |S j ||S (l)j| + 1 + 1|S (l)j| + 1= 1ρ(T j ) + 1|S (l)j| + 1 ≤ 1 α + 11 − α = 1α(1 − α) .Also ist α(1 − α) ≤ ρ(S j ) < ρ(T j ) ≤ 1 − α <strong>und</strong> wegen ρ(S j ) /∈ [α, 1 − α] giltα(1 − α) ≤ ρ(S j ) < α.Nach Lemma 3.3.8 liefert eine Rotation oder Doppelrotation nach links einen zuS j äquivalenten BB[α]-Suchbaum. Analog zeigt man, dass eine Rotation oderDoppelrotation nach rechts einen zu T j äquivalenten BB[α]-Suchbaum liefert,wenn der neue Knoten im Teilbaum S (l)jist.Indem man in Schritt (3.) alle Knoten v k , v k−1 , . . . , v 0 besucht <strong>und</strong> die entsprechendenTeilbäume gegebenenfalls rebalanciert, erhält man also einen BB[α]-Baum.Das Löschen eines Knotens aus einem BB[α]-Baum (die Operation delete) geschiehtin ähnlicher Weise wie das Einfügen:(1.) Knoten wird gesucht.(2.) Knoten wird entfernt.(3.) Der Weg zurück zur Wurzel wird durchlaufen, wobei die Teilbäume, derenWurzeln auf diesem Weg liegen, gegebenenfalls rebalanciert werden.Satz 3.3.13. Werden Suchbäume als BB[α]-Bäume mit 1/4 ≤ α ≤ 1 − 1/ √ 2implementiert, so sind die Operationen insert <strong>und</strong> delete in Zeit O(log n) realisierbar.Bemerkung 3.3.14. Wird ein kleines solches α gewählt, dann ist das Intervall[α, 1 − α] relativ groß, <strong>und</strong> somit sind weniger Rebalancierungen nötig. Dafürist nach Satz 3.3.4 die Höhe der BB[α]-Bäume aber auch größer. Also ist einesolche Wahl günstig, wenn viele Änderungen <strong>und</strong> relativ wenige Suchoperationenauszuführen sind. Andernfalls ist es günstiger, α möglichst groß zu wählen,wodurch die Höhe der BB[α]-Bäume stärker beschränkt wird.


108 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENProgramm 3.3.15. Eine Implementierung der BB[α]-Bäume:Hier verwenden wir die Klassen Stack <strong>und</strong> Queue, um den Weg, der beim Suchennach einem Knoten zurückgelegt wird, für das Rebalancieren zu speichern.Beim Einfügen besteht dieser Weg aus nur einem Stück, das in einem Stack gespeichertwird. Beim Löschen kommt noch ein zweites Stück hinzu, nämlichdas vom zu löschenden Knoten zum Knoten mit maximalem Inhalt im linkendirekten Teilbaum; hierfür verwenden wir eine Queue.In jedem Baumknoten wird zusätzlich zum Inhalt noch die aktuelle Größe desin diesem Knoten wurzelnden Teilbaums gespeichert, siehe Bemerkung 3.3.2.1 /** Die Klasse BBAlphaKnoten implementiert Knoten eines binaeren Baums.2 * Jeder Knoten speichert ein beliebiges Objekt vom Typ Object,3 * je eine Referenz auf das linke <strong>und</strong> das rechte Kind4 * <strong>und</strong> die Groesse des hier wurzelnden Teilbaums.5 */6 class BBAlphaKnoten {78 /** Die Konstanten aus Definition 3.3.1, Lemma 3.3.8 <strong>und</strong> Lemma 3.3.9. */9 public static final double ALPHA = 0.275;10 public static final double ALPHA LEFT = (1−2*ALPHA)/(1−ALPHA);11 public static final double ALPHA RIGHT = ALPHA/(1−ALPHA);1213 /** Der Knoteninhalt. */14 private Object inhalt;1516 /** Das linke <strong>und</strong> das rechte Kind. */17 private BBAlphaKnoten linkesKind;18 private BBAlphaKnoten rechtesKind;1920 /** Die Groesse des hier wurzelnden Baums. */21 private int groesse;2223 /** Konstruiert einen Blattknoten mit Inhalt inhalt. */24 public BBAlphaKnoten( Object inhalt ) { BBAlphaKnoten25 this.inhalt = inhalt;26 groesse = 1;27 }die Methoden inhalt( ), linkesKind( ) <strong>und</strong> rechtesKind( ) analog zu Programm 2.2.444 /** Gibt die Groesse des hier wurzelnden Baums zurueck. */45 public double groesse( ) { groesse46 return groesse;47 }die Methoden inhaltAendern( ), linkesKindAendern( ) <strong>und</strong> rechtesKindAendern( ) analogzu Programm 2.2.464 /** Die Groesse wird um i inkrementiert. Gibt den neuen Wert zurueck. */65 public int groesseAendern( int i ) { groesseAendern66 return groesse += i;67 }68


3.3. GEWICHTSBALANCIERTE BÄUME 10969 /** Gibt die Hoehenbalance des hier wurzelnden Teilbaums zurueck. */70 public double balance( ) { balance71 return ( ( linkesKind == null ? 0 : linkesKind.groesse ) + 1 ) /72 (double)( groesse + 1 );73 }7475 /** Rotation des hier wurzelnden Baums nach links.76 * Wir setzen voraus, dass die Rotation moeglich ist.77 * Gibt die neue Wurzel zurueck.78 */79 public BBAlphaKnoten rotationLinks( ) { rotationLinks80 BBAlphaKnoten x = this;81 BBAlphaKnoten y = x.rechtesKind; // existiert nach Annahme82 int a = x.linkesKind == null ? 0 : x.linkesKind.groesse;83 int b = y.linkesKind == null ? 0 : y.linkesKind.groesse;84 int c = y.rechtesKind == null ? 0 : y.rechtesKind.groesse;85 x.groesseAendern( −c−1 );86 y.groesseAendern( a+1 );87 x.rechtesKindAendern( y.linkesKind );88 y.linkesKindAendern( x );89 return y;90 }9192 /** Rotation des hier wurzelnden Baums nach rechts.93 * Wir setzen voraus, dass die Rotation moeglich ist.94 * Gibt die neue Wurzel zurueck.95 */96 public BBAlphaKnoten rotationRechts( ) { rotationRechts97 BBAlphaKnoten y = this;98 BBAlphaKnoten x = y.linkesKind; // existiert nach Annahme99 int a = x.linkesKind == null ? 0 : x.linkesKind.groesse;100 int b = x.rechtesKind == null ? 0 : x.rechtesKind.groesse;101 int c = y.rechtesKind == null ? 0 : y.rechtesKind.groesse;102 x.groesseAendern( c+1 );103 y.groesseAendern( −a−1 );104 y.linkesKindAendern( x.rechtesKind );105 x.rechtesKindAendern( y );106 return x;107 }108109 /** Doppelrotation des hier wurzelnden Baums nach links.110 * Wir setzen voraus, dass die Rotation moeglich ist.111 * Gibt die neue Wurzel zurueck.112 */113 public BBAlphaKnoten doppelRotationLinks( ) { doppelRotationLinks114 BBAlphaKnoten x = this;115 BBAlphaKnoten y = x.rechtesKind; // existiert nach Annahme116 BBAlphaKnoten z = y.linkesKind; // existiert nach Annahme117 int a = x.linkesKind == null ? 0 : x.linkesKind.groesse;118 int b = z.linkesKind == null ? 0 : z.linkesKind.groesse;119 int c = z.rechtesKind == null ? 0 : z.rechtesKind.groesse;120 int d = y.rechtesKind == null ? 0 : y.rechtesKind.groesse;121 x.groesseAendern( −c−d−2 );


110 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN122 y.groesseAendern( −b−1 );123 z.groesseAendern( a+d+2 );124 x.rechtesKindAendern( z.linkesKind );125 y.linkesKindAendern( z.rechtesKind );126 z.linkesKindAendern( x );127 z.rechtesKindAendern( y );128 return z;129 }130131 /** Doppelrotation des hier wurzelnden Baums nach rechts.132 * Wir setzen voraus, dass die Rotation moeglich ist.133 * Gibt die neue Wurzel zurueck.134 */135 public BBAlphaKnoten doppelRotationRechts( ) { doppelRotationRechts136 BBAlphaKnoten y = this;137 BBAlphaKnoten x = y.linkesKind; // existiert nach Annahme138 BBAlphaKnoten z = x.rechtesKind; // existiert nach Annahme139 int a = x.linkesKind == null ? 0 : x.linkesKind.groesse;140 int b = z.linkesKind == null ? 0 : z.linkesKind.groesse;141 int c = z.rechtesKind == null ? 0 : z.rechtesKind.groesse;142 int d = y.rechtesKind == null ? 0 : y.rechtesKind.groesse;143 x.groesseAendern( −c−1 );144 y.groesseAendern( −a−b−2 );145 z.groesseAendern( a+d+2 );146 x.rechtesKindAendern( z.linkesKind );147 y.linkesKindAendern( z.rechtesKind );148 z.linkesKindAendern( x );149 z.rechtesKindAendern( y );150 return z;151 }die Methoden toString( ) <strong>und</strong> druckeZwischenordnung( ) wie in Programm 2.2.4, die MethodedruckeVorordnung( ) wie in Programm 3.2.5180 } // class BBAlphaKnoten181182183 /** Die Klasse BBAlphaBaum implementiert BB[alpha]-Baeume. */184 public class BBAlphaBaum {185186 /** Der Wurzelknoten. */187 private BBAlphaKnoten wurzel;188189 /** Konstruiert einen Baum, der nur einen Wurzelknoten190 * mit Schluessel key besitzt.191 */192 public BBAlphaBaum( Object key ) { BBAlphaBaum193 wurzel = new BBAlphaKnoten( key );194 }195196 /** Konstruiert den leeren Baum. */197 public BBAlphaBaum( ) { } BBAlphaBaum198


3.3. GEWICHTSBALANCIERTE BÄUME 111199 /** Gibt den Wurzelknoten zurueck. */200 public BBAlphaKnoten wurzel( ) { wurzel201 return wurzel;202 }203204 /** Test, ob der Baum leer ist. */205 boolean istLeer( ) { istLeer206 return wurzel == null;207 }208209 /** Gibt den Pfad vom linken direkten Kind von k zum Knoten mit210 * maximalem Schluessel im linken direkten Teilbaum von k zurueck.211 * Wir setzen voraus, dass dieser linke Teilbaum nicht leer ist.212 * Dabei sei der Weg als Queue analog zu der fuer die Methode213 * searchKey beschriebenen Weise repraesentiert.214 */215 private Queue pathToMaximum( BBAlphaKnoten k ) { pathToMaximum216 Queue path = new Queue( );217 BBAlphaKnoten node = k.linkesKind( ); // existiert nach Voraussetzung218 path.enqueue( node );219 path.enqueue( new Integer( +1 ) );220 while( node.rechtesKind( ) != null ) {221 node = node.rechtesKind( );222 path.enqueue( node );223 path.enqueue( new Integer( +1 ) );224 }225 return path;226 }227228 /** Gibt einen Stack path mit 2n Elementen (n >= 0) zurueck,229 * der einen Pfad im Baum auf folgende Weise repraesentiert.230 * (Das Stack-Element an Position i, bezeichnet mit path[i], sei dasjenige,231 * das durch i-maliges pop( ) gefolgt von einem top( ) erreichbar ist.)232 * Der leere Stack repraesentiert den leeren Pfad. Ist der Stack nicht leer, so gilt:233 * (1) path[2n-1] ist die Wurzel des Baums.234 * (2) Fuer 0 0: analog mit rechtem Kind von path[1].242 */243 private Stack searchKey( Comparable key ) { searchKey244 Stack path = new Stack( );245 if( istLeer( ) )246 return path;247 BBAlphaKnoten node = wurzel;248 do {249 int vergleich = key.compareTo( node.inhalt( ) );250 if( vergleich == 0 ) {251 path.push( node );


112 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN252 path.push( new Integer( 0 ) );253 return path;254 }255 if( vergleich < 0 ) {256 path.push( node );257 path.push( new Integer( −1 ) );258 node = node.linkesKind( );259 }260 else { // vergleich > 0261 path.push( node );262 path.push( new Integer( +1 ) );263 node = node.rechtesKind( );264 }265 }266 while( node != null );267 return path;268 }269270 /** Entlang des im Stack gespeicherten Weges wird der Baum rebalanciert.271 * Dabei sei der Weg im Stack auf die bei der Methode searchKey beschriebene272 * Weise repraesentiert.273 * Die Groesse der Knoten aendert sich um i. (Beim Rebalancieren nach274 * insert ist daher i == +1, beim Rebalancieren nach delete ist i == -1.)275 */276 private void rebalance( Stack path, int i ) { rebalance277 if( !path.isEmpty( ) )278 path.pop( ); // diese Zahl ist fuer das Rebalancieren nicht relevant279 while( !path.isEmpty( ) ) { // path enthaelt noch 2i+1 > 0 Eintraege280 BBAlphaKnoten k = (BBAlphaKnoten)path.topAndPop( );281 BBAlphaKnoten wurzelDesTeilbaums = k;282 k.groesseAendern( i );283 if( k.balance( ) < BBAlphaKnoten.ALPHA ) // Balance in k ist zu klein284 if( k.rechtesKind( ).balance( ) > BBAlphaKnoten.ALPHA LEFT )285 wurzelDesTeilbaums = k.doppelRotationLinks( );286 else287 wurzelDesTeilbaums = k.rotationLinks( );288 else if( k.balance( ) > 1−BBAlphaKnoten.ALPHA ) // Balance in k ist zu gross289 if( k.linkesKind( ).balance( ) > BBAlphaKnoten.ALPHA RIGHT )290 wurzelDesTeilbaums = k.rotationRechts( );291 else292 wurzelDesTeilbaums = k.doppelRotationRechts( );293 if( !path.isEmpty( ) ) {294 int direction = ( (Integer)path.topAndPop( ) ).intValue( );295 BBAlphaKnoten parent = (BBAlphaKnoten)path.top( );296 if( direction < 0 )297 parent.linkesKindAendern( wurzelDesTeilbaums );298 else // direction > 0299 parent.rechtesKindAendern( wurzelDesTeilbaums );300 }301 else302 wurzel = wurzelDesTeilbaums;303 }304 }305


3.3. GEWICHTSBALANCIERTE BÄUME 113306 /** Gibt einen Knoten mit einem zu key aequivalenten Schluessel307 * zurueck, falls ein solcher Knoten vorhanden ist, sonst null.308 */309 public BBAlphaKnoten isMember( Comparable key ) { isMember310 Stack path = searchKey( key );311 if( path.isEmpty( ) | | ( (Integer)path.top( ) ).intValue( ) != 0 )312 return null;313 else { // im Stack steht oben 0, also ist darunter der gesuchte Knoten314 path.pop( );315 return (BBAlphaKnoten)path.top( );316 }317 }318319 /** Fuegt einen Knoten mit Schluessel key in den BB[alpha]-Baum ein. */320 public void insert( Comparable key ) { insert321 Stack path = searchKey( key );322 if( path.isEmpty( ) ) { // Baum ist leer323 wurzel = new BBAlphaKnoten( key );324 return;325 }326 int vergleich = ( (Integer)path.topAndPop( ) ).intValue( );327 if( vergleich == 0 ) // nicht zu tun328 return;329 if( vergleich < 0 )330 ( (BBAlphaKnoten)path.top( ) ).331 linkesKindAendern( new BBAlphaKnoten( key ) );332 else // vergleich > 0333 ( (BBAlphaKnoten)path.top( ) ).334 rechtesKindAendern( new BBAlphaKnoten( key ) );335 ( (BBAlphaKnoten)path.topAndPop( ) ).groesseAendern( +1 );336 rebalance( path, +1 );337 }338339 /** Entfernt den Knoten mit zu key aequivalentem Schluessel, falls vorhanden.340 */341 public void delete( Comparable key ) { delete342 Stack path = searchKey( key );343 if( !path.isEmpty( ) && ( (Integer)path.topAndPop( ) ).intValue( ) == 0 ) {344 BBAlphaKnoten k = (BBAlphaKnoten)path.topAndPop( );345 // k hat Schluessel key346 if( k.linkesKind( ) == null | | k.rechtesKind( ) == null ) { // Fall 1347 BBAlphaKnoten einzigesKind =348 k.linkesKind( ) == null ? k.rechtesKind( ) : k.linkesKind( );349 if( path.isEmpty( ) ) // k ist Wurzel350 wurzel = einzigesKind;351 else {352 if( ( (Integer)path.topAndPop( ) ).intValue( ) < 0 )353 ( (BBAlphaKnoten)path.top( ) ).linkesKindAendern( einzigesKind );354 else355 ( (BBAlphaKnoten)path.top( ) ).rechtesKindAendern( einzigesKind );356 path.push( new Integer( 0 ) ); // dieser Wert ist nicht relevant357 rebalance( path, −1 );358 }359 }


114 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN360 else { // Fall 2361 Queue pathToMaximum = pathToMaximum( k );362 // den Pfad pathToMaximum an den Pfad path haengen:363 path.push( k );364 path.push( new Integer( −1 ) );365 while( !pathToMaximum.isEmpty( ) ) {366 // zuerst push fuer Typ BBAlphaKnoten, dann fuer Typ Integer367 path.push( pathToMaximum.frontAndDequeue( ) );368 path.push( pathToMaximum.frontAndDequeue( ) );369 }370 path.pop( ); // nicht relevante Zahl entfernen371 BBAlphaKnoten maximum = (BBAlphaKnoten)path.topAndPop( );372 // maximum ist der Knoten mit maximalem Schluessel373 // im linken direkten Teilbaum von k374 path.pop( ); // nicht relevante Zahl entfernen375 BBAlphaKnoten predMax = (BBAlphaKnoten)path.top( );376 // predMax ist der Elternknoten von maximum377 k.inhaltAendern( maximum.inhalt( ) );378 if( predMax == k )379 k.linkesKindAendern( maximum.linkesKind( ) );380 else381 predMax.rechtesKindAendern( maximum.linkesKind( ) );382 path.push( new Integer( 0 ) ); // dieser Wert ist nicht relevant383 rebalance( path, −1 );384 }385 }386 }die Methoden druckeZwischenordnung( ) <strong>und</strong> druckeVorordnung( ) wie in Programm 3.2.5412 public static void main( String[ ] args ) { main413 BBAlphaBaum b = new BBAlphaBaum( );414 String[ ] monat = { "JAN", "FEB", "APR", "AUG", "DEZ", "MAR",415 "MAI", "JUN", "JUL", "SEP", "OKT", "NOV" };416 for( int i = 0; i < 12; i++ ) {417 b.insert( monat[i] );418 b.druckeVorordnung( );419 }420 b.delete( "FEB" ); b.druckeVorordnung( );421 b.delete( "JAN" ); b.druckeVorordnung( );422 b.delete( "MAR" ); b.druckeVorordnung( );423 }424425 } // class BBAlphaBaum3.4 Höhenbalancierte BäumeEs gibt viele Arten höhenbalancierter Bäume, etwa (a, b)-Bäume, B-Bäume,rot-schwarze Bäume <strong>und</strong> AVL-Bäume. Wir wollen uns hier zunächst mit denletzteren befassen. Im Anschluss schauen wir uns noch die (2,4)-Bäume an.


3.4. HÖHENBALANCIERTE BÄUME 1153.4.1 AVL-BäumeDie Höhe eines nicht-leeren Baums T , ab jetzt mit h(T ) bezeichnet, wurde inAbschnitt 2.1 definiert. Zur Vereinfachung der folgenden Definition legen wirzusätzlich die Höhe des leeren Baums mit −1 fest.Definition 3.4.1. Die Höhenbalance eines binären Baums mit direkten linkenbzw. rechten Teilbäumen T l <strong>und</strong> T r ist der Werth(T l ) − h(T r ).Ein Baum heißt höhenbalanciert, falls jeder seiner Teilbäume eine Höhenbalanceaus {−1, 0, 1} hat. Höhenbalancierte Suchbäume werden AVL-Bäume genanntnach ihren Erfindern Adelśon-Velśkiǐ <strong>und</strong> Landis (1962).Beispiel 3.4.2. Die Höhenbalance der Teilbäume steht hier an ihrer Wurzel:Juli 0 Febr 1 März 0 Aug 0 Jan 0 Mai 1Okt 0 Apr 0 Dez 0 Juni 0 Nov 0 Sept 0ein AVL-BaumJuli 1 Jan 2März −1 Febr 1 Juli 0 Mai 0 Okt 0 Aug 1Dez 0 Nov 0 Sept 0Apr 0kein AVL-BaumLemma 3.4.3. Für AVL-Bäume mit n Knoten <strong>und</strong> Höhe h giltF (h + 3) − 1 ≤ n ≤ 2 h+1 − 1.Dabei ist F (k) die k-te Fibonacci 1 -Zahl, definiert durch F (0) = 0, F (1) = 1<strong>und</strong> F (k + 2) = F (k + 1) + F (k).Beweis: Für binäre Bäume gilt n ≤ 2 h+1 −1 nach Lemma 2.1.5(b). Wir müssenalso nur noch die untere Schranke für n nachweisen. Für m > 0 sei dazu F m einAVL-Baum mit Höhe m, der die minimal notwendige Anzahl von Knoten hat 2 .Dann ist h(F l ) = m−1 <strong>und</strong> h(F r ) = m−2 oder h(F l ) = m−2 <strong>und</strong> h(F r ) = m−1.1 Leonardo Pisano Fibonacci (1170–1250)2 Solche Bäume werden auch Fibonacci-Bäume genannt.


116 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENAlso gilt |F m | = 1+|F m−1 |+|F m−2 |, <strong>und</strong> wir haben |F 0 | = 1 <strong>und</strong> |F 1 | = 2. Also|F 0 | + 1 = 2, |F 1 | + 1 = 3 <strong>und</strong> |F m | + 1 = (|F m−1 | + 1) + (|F m−2 | + 1), damitWir erhalten n ≥ |F h | = F (h + 3) − 1.Für Fibonacci-Zahlen weiß man|F m | + 1 = F (m + 3).F (k) = √ 1 (φ k − ̂φk)5mit φ = (1 + √ 5)/2 ≈ 1, 618 (der goldene Schnitt) <strong>und</strong> ̂φ = (1 − √ 5)/2 ≈−0, 618. Wegen |̂φ k / √ 5| < 1/2 gilt F (k) > φ k / √ 5 − 1, also mit obigem Lemman + 1 ≥ F (h + 3) > φ h+3 / √ 5 − 1.Das liefert log(n + 2) > (h + 3) log φ − log √ 5, d.h.h < log(n + 2) + log √ 5log φ− 3 < 1 log(n + 2) ≈ 1, 44 · log(n + 2).log φSatz 3.4.4. Für AVL-Bäume mit n Knoten <strong>und</strong> Höhe h giltlog(n + 1) − 1 ≤ h < 1, 441 · log(n + 2).AVL-Bäume sind also höchstens um circa 44% höher als minimal möglich. Insbesonderesind Suchen, Einfügen <strong>und</strong> Löschen in Zeit O(log n) realisierbar.Leider kann sowohl beim Einfügen als auch beim Löschen die Höhenbalanciertheitzerstört werden. Wie bei den BB[α]-Bäumen müssen wir in einem solchenFall durch Rebalancierungsoperationen versuchen, den entstandenen Baum ineinen äquivalenten umzuformen, der wieder höhenbalanciert ist.Einfügen in AVL-Bäume mit anschließender Rebalancierung:Operation LL:✓✏1 x h(x) = h + 2✧✒✑◗✓✏ ✧ ◗h(y) = h + 1 y 0 ✂❇h 3 = h✒✑ ✂B ❙❙ ✂❇3✪❇✂❇✂❇✂ ✂B✂❇✂❇1 B 2❇ ❇h 1 = h 2 = hEinfügen in B 1✓✏✓✏2 x h ′ (x) = h + 30 y h ′′ (y) = h + 2✧✒✑◗✒✑ ❜✓✏ ✧ ◗ Rotation LL ✑ ✑✑ ❜❜ ✓✏h ′ (y) = h + 2 y 1 ✂❇✒✑ ✂✒✑✓✓ ❙❙ ✂❇✂❇✂❇✂❇x h ′′ (x) = h + 1h03 = hB 3Bh ′ 1′ ❇ ✪ ❡h ′ 1 = h + 1✂❇✂❇✂✂❇✂❇✂❇✂❇✂❇1 = h + 1✂✂B❇ ✂❇✂❇ h 2 = hh 2 = h h 3 = h′ B 1 ❇ 2B 2 B 3❇


3.4. HÖHENBALANCIERTE BÄUME 117✓✏Operation RR: −1 x h(x) = h + 2✒✑ ❜✧ ✧✧ ❜❜ ✓✏h 1 = h ✂❇✂✒✑✂❇ 0 y h(y) = h + 1B 1❇ ✪ ❡✂❇✂✂❇✂❇✂❇ ✂❇ h 2 = h 3 = h❇B 2 B 3B 1Einfügen in B 3✓✏✓✏−2 x h ′ (x) = h + 30 y h ′′ (y) = h + 2✒✑✒✑✧ ✧✧❜ ❜❜ ◗✓✏Rotation RR ✓✏ ✧ ✧✧ ◗h 1 = h ✂❇✂✒✑✒✑✂❇ −1 y h ′ (y) h ′′ (x) x 0 ✂❇h ′B✂❇ ✓✓ ❙❙✓✓ ❙❙ ✂❇3 = h + 1= h + 2 = h + 11B 3′ ❇✂❇✂❇✂❇✂✂❇✂❇h 2 = hB ✂✂ ✂❇ ✂❇❇✂❇✂❇2 B ′ h ′ 3 = h + 13B 2❇ ❇h 1 = h h 2 = h✓✏✓✏✓✏Operation LR(i): 1 xh(x) = 12 x h ′ (x) = 2 0 z h ′′ (z) = 1✒✑✒✑✒✑✓✏✓✏ ✧ ✧✧◗✧ ✧✧ EinfügenRotation LR(i) ✓✏ ✧ ✧✧ ◗ ✓✏0 y von z -1 y h yx✒✑✒✑′ (y) = 1 00◗✒✑ ✒✑h(y) = 0◗ ✓✏ h ′′ (y) = 0 h ′′ (x) = 00 z h ′ (z) = 0✒✑✓✏Operation LR(ii): 1 x h(x) = h + 2✒✑◗✓✏ ✧ ✧✧ ◗h(y)y 0 ✂❇✒✑✱✱ ❧ ✂✓✏✂❇ h 4 = h= h + 1B 4❇✂❇✂B ✒✑✂❇ 0 z h(z) = h1❇ ✔✔ ❏h 1 = h ✂❇✂✂❇✂❇B ✂❇ ✂❇2 B 3❇h 2 = h 3 = h − 1Einfügen in B 2✓✏✓✏2 xh ′ (x) = h + 3 h0 z′′ (z) = h + 2✒✑◗✒✑✓✏✓✏ ✑ ✑✑ ❜✧ ✧✧ ◗ Rotation LR(ii)❜❜ ✓✏h ′ (y) y -1 ✂❇✒✑✒✑ ✒✑✱✱ ❧ ✂✓✏✂❇ h 4 = h h ′′ h ′′ (x)(y) y 0 -1 x= h + 2= h + 1B 4= h + 1❇✓✓ ❙❙ ✪ ❡✂❇✂❇✂❇✒✑✂✂❇✂❇✂✂✂✂❇1 z h ′ (z) = h + 1❇ ✔✔ ❏✂❇✂❇B ✂❇ ✂❇❇ ❇ ✂❇1 B 1 B ′ B 3 B 42 ❇h 1 = h ✂❇✂❇✂B ✂✂′ ❇ B❇ ✂❇h 1 = hh 3 h 4 = hh ′ 2 = h = h − 12 3❇h 3 = h − 1h ′ 2 = h


118 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENOperation LR(iii):✓✏1 x h(x) = h + 2✒✑◗✓✏ ✧ ✧✧ ◗h(y) y 0 ✂❇✒✑✱✱ ❧ ✂✓✏✂❇h 4 = h= h + 1B 4❇✂❇✂B ✒✑✂❇0 z h(z) = h1❇ ✔✔ ❏h 1 = h ✂❇✂✂❇✂❇B ✂❇ ✂❇2 B 3❇h 2 = h 3 = h − 1Einfügen in B 3✓✏✓✏2 x h ′ h(x) = h + 30 z′′ (z) = h + 2✒✑◗✒✑✓✏✓✏ ✑ ✑✑ ❜✧ ✧✧ ◗ Rotation LR(iii)❜❜ ✓✏h✂❇′′ (x)h ′ (y) y -1✒✑✒✑ ✒✑✱✱ ❧ ✂✓✏✂❇ h 4 = h h ′′ (y) y 1 0 x = h + 1= h + 2B 4= h + 1❇✓✓ ❙❙ ✪ ❡✂❇✂❇✂❇✂ ✒✑✂✂✂❇ -1 z h ′ (z) = h + 1❇ ✔✔ ❏✂❇✂❇✂❇ ✂❇✂❇B ✂❇ ✂❇❇ ✂❇1 B 1 B 2 B 3′ B 4❇h 1 = h ✂❇✂❇✂ ✂✂❇❇ ✂❇h 1 = h h 2h 4 = hB 2 B 3′ = h − 1 h ′ 3 = h❇h 2= h − 1h ′ 3 = hOperation RL(i):✓✏✓✏✓✏-1 xh(x) = 1 h ′ (x) = 2h ′′ (z) = 1-2 x0 z✒✑ ❜ ✒✑ ❜ ✒✑❜❜ ◗✓✏❜❜Rotation RL(i)Einfügen ✓✏ ✓✏ ✧ ✧✧ ◗ ✓✏0 y von z h ′ (y) = 1 y 1 0 xy 0✒✑✑✒✑ ✒✑ ✒✑h(y) = 0✓✏ ✑h ′′ (x) = 0 h ′′ (y) = 0h ′ (z) = 0 z 0✒✑RL(ii) <strong>und</strong> RL(iii) analog zu LR(ii) <strong>und</strong> LR(iii).Bemerkung 3.4.5. (1) Durch solche Rebalancierungen entstehen aus Suchbäumenstets wieder Suchbäume.(2) Wird in einen AVL-Baum T ein Knoten eingefügt, so ist der resultierendeBaum T ′ entweder wieder ein AVL-Baum, oder der kleinste Teilbaum ˆT vonT ′ , der den neu eingefügten Knoten enthält <strong>und</strong> der selbst kein AVL-Baummehr ist, hat eine der Formen, wie sie in obiger Definition jeweils nach einerEinfüge-Operation auftreten. Insbesondere ist die Wurzel von ˆT der untersteKnoten auf dem Weg von der Wurzel von T ′ zu dem neu eingefügten Knoten,der Höhenbalance ±2 hat.


3.4. HÖHENBALANCIERTE BÄUME 119(3) Wird ein Teilbaum ˆT rebalanciert, so entsteht ein Teilbaum ˆT ′ , der dieselbeHöhe hat wie der Teilbaum ˆT vor dem Einfügen des neuen Knotens:T : T ′ : T ′′ :✓❙❀ ✓❙ ❀ ✓❙✓ ❙✓ ❙✓✡❚❙Einfügen ✓ ❙❙❙❙✡ ❚✓✓❙✓Rebalancieren✓✡❚❙✡✡❚✡✓✡❚❚ˆT✓❚ ❙✡ ❚✡✓✓❙✡❙✡❚✡❚0ˆT ′ˆT❚❚Also ist T ′′ ein AVL-Baum, d.h. zum Rebalancieren reicht stets eine einzigeRotation des entsprechenden Typs aus.Beispiel 3.4.6.Neuer Schlüssel Baum nach Einfügen Baum nach Rebalancieren(1.) Mai Mai 0 —(2.) März Mai −1März 0(3.) November Mai −2 März −1Nov 0(4.) August 1 März 1 MaiNov 00 AugRR:—0 März 0 Mai Nov 0—(5.) April 2 März 2 MaiNov 01 Aug0 AprLL:1 März 0 AugNov 0 0 Apr Mai 0(6.) Januar 2 März −1 Aug Nov 00 Apr Mai 10 JanLR:0 Mai 0 Aug −1 März 0 Apr Jan 0 Nov 0


120 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN(7.) Dezember 1 Mai −1 Aug März −1 0 Apr Jan 1Nov 00 Dez—(8.) Juli 1 Mai −1 Aug März −1 0 Apr Jan 0 Nov 00 Dez Juli 0—(9.) Februar 2 Mai −2 Aug März −1 0 Apr Jan 1 Nov 0−1 Dez Juli 0Febr 0RL:1 Mai 0 Dez März −1 1 Aug Jan 0 Nov 00 April 0 Febr Juli 0(10.) Juni 2 Mai −1 Dez März −1 1 AugJan −1Nov 00 April 0 Febr Juli −1Juni 0LR:0 Jan 1 DezMai 0 1 Aug Febr 0 −1 Juli März −1 0 April Juni 0 Nov 0(11.) Oktober −1 Jan 1 Dez Mai −1 1 Aug Febr 0−1 Juli März −2 0 April Juni 0 Nov −1 Okt 0


3.4. HÖHENBALANCIERTE BÄUME 121(12.) SeptemberRR:0 Jan 1 DezMai 0 1 Aug Febr 0 −1 JuliNov 0 0 April Juni 0 0 März Okt 0−1 Jan 1 Dez Mai −1 1 Aug Febr 0 −1 JuliNov −1 0 April Juni 0 0 März Okt −1Sept 0Gerät ein AVL-Baum T durch das Löschen eines Knotens k aus der Balance,so müssen durch eventuell mehrere Rotationen die Teilbäume, deren Wurzelnauf dem Weg von der Wurzel von T zum Knoten k liegen, rebalanciert werden.Dabei geht man ausgehend vom Elternknoten von k zurück zur Wurzel von T .Beispiel 3.4.7. Im Baum✓✏T :130✓✏ ✒✑ ✓✏1✥✥✥✥✥✥✥ ❵❵❵❵❵❵❵1✓✏ ✒✑✓✏✓✏✒✑✓✏✦ ✦✦✦20 40 ❜✟✟ ❜❜1 1025 11 35 45 1✧✒✑✓✏✓✏✑✒✑◗✓✏✓✏✒✑✓✏✓✏✒✑✧✓✏✧✧ 1 15 22 -1 26 0 32 1 0 38 42 01 5 ✓✏✒✑✒✑✒✑✓✏✒✑✒✑ ✒✑✚✒✑❙✓✏✓✏ ✚ ✓✏✚❙ 0 1231 0✒✑ 0 23 ✒✑✒✑-1 17✒✑✒✑0✓✏ ❏2✒✑0ergibt das Löschen von 42 den Baum✓✏130✓✏ ✒✑ ✓✏1✥✥✥✥✥✥✥ ❵❵❵❵❵❵❵2✓✏ ✒✑✓✏✓✏✒✑✓✏✦ ✦✦✦20❛✧ 40❛❛1 1025 11 35 45 0✧✒✑✓✏✓✏✑✒✑◗✓✏✓✏✒✑✓✏✒✑✧✓✏✧✧ 1 15 22 -1 26 0 32 1 0 381 5 ✓✏✒✑✒✑✒✑✓✏✒✑✒✑★✒✑❙✓✏✓✏★★✓✏01231 0✒✑ 0 23 ✒✑✒✑-1 17✒✑✒✑0✓✏ ❏2✒✑0


122 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENRotation nach rechts im Knoten 40 ergibt:✓✏230✓✏ ✒✑ ✓✏1✥✥✥✥✥✥✥ ❵❵❵❵❵❵❵0✓✏ ✒✑✓✏✓✏✒✑✓✏✦ ✦✦✦20❍✧ ✧ 35❍❍1 1025 11 3240 0✧✒✑✓✏✓✏✒✑✓✏✓✏✒✑✓✏✒✑✧✑ ◗◗✓✏✓✏✧✧ 1 15 22 -1 26 0 0 310 38 0 451 5 ✓✏✒✑✒✑✒✑ ✒✑ ✒✑ ✒✑✒✑❙✓✏✓✏✓✏012✱✒✑ 0 23✒✑-1 17✒✑✒✑0✓✏ ❏2✒✑0Erst eine Rotation nach rechts im Knoten 30 ergibt einen AVL-Baum:✓✏0 20✒✑✓✏✓✏❛❛❛✦ ✦✦✦ 30 01 10✓✏✒✑✧✓✏✒✑✓✏✱❵❵❵❵❵❵❵✧0✓✏✧25 135 ❍✧ 1 15 ✓✏✒✑✓✏✒✑ ✓✏✓✏✒✑✑ ◗✓✏ ❍❍1 5✚✒✑22 -1 26 040 0✒✑ ✒✑✓✏✒✑✓✏✓✏✚ ✓✏ ▲0 121 32✒✑✓✏✒✑ ◗✚❙✓✏0 38 0 45-1 17✒✑ ✒✑✒✑✒✑00 310 23 ✒✑✓✏ ❏✒✑2✒✑0Bemerkung 3.4.8. Bei der Implementierung speichern wir in jedem Knotendie Höhenbalance des dort wurzelnden Teilbaums (statt dessen Höhe). DieRebalancierungsoperationen geben direkt an, wie sich diese Werte ändern.Satz 3.4.9. Werden Suchbäume durch AVL-Bäume implementiert, so ist Einfügen,Suchen <strong>und</strong> Löschen in Zeit O(log n) realisierbar.3.4.2 (2,4)-BäumeAls zweite Variante der höhenbalancierten Bäume betrachten wir noch die (2,4)-Bäume. Die folgenden Begriffe können ganz allgemein für Zahlenpaare (a, b) mita ≥ 2 <strong>und</strong> b ≥ 2a − 1 formuliert werden, was dann die (a, b)-Bäume ergibt. AusGründen der Übersichtlichkeit beschränken wir uns hier aber auf den Spezialfall(2,4). Bei diesen Bäumen wird alle zu speichernde Information in den Blätternuntergebracht, d.h. für n Schlüssel brauchen wir einen Baum mit n Blättern<strong>und</strong> einigen inneren Knoten, die der Verwaltung der Information dienen.


3.4. HÖHENBALANCIERTE BÄUME 123Definition 3.4.10. Ein nicht-leerer geordneter Baum ist ein (2,4)-Baum, wennalle Blätter dieselbe Tiefe haben <strong>und</strong> wenn für alle inneren Knoten k gilt2 ≤ d(k) ≤ 4.Wie wird nun eine Schlüsselmenge S = {x 1 , . . . , x n } mit x 1 < · · · < x n in einem(2,4)-Baum gespeichert? Dazu brauchen wir einen (2,4)-Baum mit n Blättern,in denen von links nach rechts die Werte x 1 , . . . , x n gespeichert werden. Ist nunk ein innerer Knoten dieses (2,4)-Baums mit d(k) = r ∈ {2, 3, 4}, so wird ink eine Folge von r − 1 Elementen aus dem Universum, aus dem S stammt,gespeichert. Ist diese Folge gerade y 1 < · · · < y r−1 , so gilt für alle Blätter imi-ten direkten Teilbaum unterhalb von kInhalt des Blattes ≤ y 1 für i = 1,y i−1 < Inhalt des Blattes ≤ y i für 1 < i < r,y r−1 < Inhalt des Blattes für i = r.Beispiel 3.4.11. Ein (2,4)-Baum für S = {1, 3, 6, 8, 9, 10} ⊆ N:✗✔4✑✖✕✗✔✑✑❳❳❳❳❳❳❳ ✓27, 8, 9✖✕✒❅ ✧✧✧ ✔ ❚✏❜ ✑ ❜❜1 3 6 8 9 10Das Suchen in einem (2,4)-Baum ist recht einfach. Anhand der in einem innerenKnoten k gespeicherten Werte y 1 < · · · < y d(k)−1 kann man feststellen, inwelchem direkten Teilbaum unterhalb von k die Suche fortgesetzt werden muss.Wegen d(k) ≤ 4 kostet das Suchen nur O(Höhe von T ) Schritte.Lemma 3.4.12. Für einen (2,4)-Baum mit n Blättern <strong>und</strong> Höhe h gilt2 h ≤ n ≤ 4 h , also (log n)/2 ≤ h ≤ log n.Beweis: Da jeder innere Knoten k die Ungleichung 2 ≤ d(k) ≤ 4 erfüllt, hatein (2, 4)-Baum der Höhe h höchstens 4 h <strong>und</strong> mindestens 2 h Blätter, da ja alleBlätter auf derselben Stufe h liegen.In einem (2,4)-Baum mit n Blättern kostet die Operation Suchen also nurO(log n) Schritte.Wie kann man das Einfügen in (2,4)-Bäumen realisieren? Durch die OperationSuchen wird ein innerer Knoten gef<strong>und</strong>en, der als Kind ein Blatt mit demeinzufügenden Schlüssel hat, wenn er schon im Baum vorhanden ist. Ist diesder Fall, so wird kein Knoten neu erzeugt. Andernfalls wird ein neues Blatt


124 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENerzeugt <strong>und</strong> als neues Kind an der richtigen Stelle an diesen Knoten angefügt.Im Knoten selbst wird auch ein neuer Wert eingetragen, der die Blattinhaltekorrekt voneinander trennt. Falls der Knoten durch dieses Anfügen den Gradd(k) = 5 erhält, wird er in zwei Knoten k ′ <strong>und</strong> k ′′ aufgespalten. Dadurch erhältder Elternknoten von k nun ein zusätzliches Kind, d.h. das Aufspalten kannsich entlang des Suchwegs bis zur Wurzel fortsetzen.Beispiel 3.4.13. In den Baum aus Beispiel 3.4.11 soll ein Knoten mit Inhalt7 eingefügt werden.✗✔4✧✖✕✗✔✧✧❳❳❳❳❳❳❳❳ ✓ ✏26, 7, 8, 9 k✖✕✒ ❛ ✑✪ ❡ ❛❛❛❛✪ ❡ ✦✦ ✦✦✦ ❝ 1 3 6 7 8 9 10Knoten k hat nun den Grad d(k) = 5, muss also geteilt werden.✓ ✏T 1 : 4, 7✒ ✑✗✔ ✏ ✗✔❳❳❳❳❳❳✏✏✏✏✓ ✏26 k ′ 8, 9 k ′′✖✕ ✖✕ ✒ ✑✔ ❚ ❚✔✔ ❚ ☞❅ 1 3 6 78910Beim Löschen kann es passieren, dass ein innerer Knoten k nur noch ein Blattals Kind hat. Dann muss k entweder ein weiteres Kind von einem seiner direktenNachbarn erhalten, oder k muss mit einem seiner direkten Nachbarnverschmolzen werden.Beispiel 3.4.14. Wenn wir im Baum T 1 aus Beispiel 3.4.13 den Knoten 7löschen, dann kann k ′ von k ′′ das Kind 8 übernehmen:✓ ✏T 2 :4, 8✒ ✑✗✔ ✏ ✏✏✏✏ ✗✔ ✗✔2 6 9✔✖✕ ✖✕❚✖✕✔ ❚ ✔ ❚ ✪❡ 1 3 68 9 10Wenn wir im Baum T 1 den Knoten 3 löschen, dann wird dessen Elternknotenmit seinem Nachbarn verschmolzen:


3.5. HASHING (STREUSPEICHERUNG) 125✗✔T 3 :7✖✕ ❛ ❛❛❛❛✓ ✦ ✦✦✦✦ ✏✓2, 68, 9✒ ✑✒ ❅ ✪✏✑❩ ❩16 789 10Natürlich kann sich die Wiederherstellung des (2,4)-Baums nach einer Lösch-Operation auch wieder bis zur Wurzel fortpflanzen.Satz 3.4.15. Werden Suchbäume durch (2,4)-Bäume implementiert, so ist Einfügen,Suchen <strong>und</strong> Löschen in Zeit O(log n) realisierbar.3.5 Hashing (Streuspeicherung)In diesem Abschnitt lernen wir eine weitere Realisierung für den Datentyp ”Dictionary“kennen: die Hash-Tabelle. Die Gr<strong>und</strong>idee der Hashing-Verfahren bestehtdarin, aus dem zu speichernden Schlüsselwert die Adresse im Speicherzu berechnen, an der dieses Element untergebracht wird. Sei etwa U ( ”Universum“)der Bereich, aus dem die Schlüsselwerte stammen, <strong>und</strong> sei S ⊆ U einezu speichernde Menge mit |S| = n. Zur Speicherung der Elemente von S stellenwir eine Menge von Behältern B 0 , B 1 , . . . , B m−1 zur Verfügung. Natürlich giltim allgemeinen |U| ≫ m (d.h. |U| ist sehr viel größer als m).Definition 3.5.1. Eine Hash-Funktion ist eine (totale) Funktion h : U →{0, . . . , m − 1}. Für a ∈ U gibt h(a) den Behälter an, in dem der Schlüssela untergebracht werden soll. Der Wert n/|U| ist die Schlüsseldichte, <strong>und</strong> derWert n/m ist der Belegungsfaktor der Hash-Tabelle B 0 , . . . , B m−1 .Eine Hash-Funktion sollte die folgenden Eigenschaften haben:• Sie sollte surjektiv sein, d.h. alle Behälter sollten erfasst werden.• Sie sollte die zu speichernden Schlüssel möglichst gleichmäßig über alleBehälter verteilen, d.h. jeder Behälter sollte mit gleicher Wahrscheinlichkeitgetroffen werden.• Sie sollte möglichst einfach zu berechnen sein.Beispiel 3.5.2. Wir wollen die Monatsnamen auf 13 Behälter B 0 , . . . , B 12verteilen. Als einfaches Beispiel einer Hash-Funktion verwenden wir dabei diefolgende: Ist w = a 1 a 2 a 3 . . . a k ein Wort der Länge ≥ 3 über dem Alphabet{a, . . . , z}, so wählen wirh(w) = d(a 1 ) + d(a 2 ) + d(a 3 ) mod 13


126 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENmit d(a) = 1, d(b) = 2, . . . , d(z) = 26. Wir erhalten folgende Zuordnung:februar ↦→ 0 oktober ↦→ 7september ↦→ 1 ↦→ 8↦→ 2 april, dezember ↦→ 9august ↦→ 3 mai ↦→ 10maerz, juni ↦→ 6juli ↦→ 4 ↦→ 11↦→ 5 januar, november ↦→ 12Obwohl noch Behälter da sind, die nichts enthalten, werden mehrfach Schlüsselauf denselben Behälter abgebildet, d.h. sogenannte Kollisionen treten auf.Die verschiedenen Hashverfahren unterscheiden sich nun dadurch, wie sie mitauftretenden Kollisionen umgehen. Beim offenen Hashing kann ein Behälterbeliebig viele Elemente aufnehmen, während beim geschlossenen Hashing jederBehälter nur eine kleine konstante Anzahl b von Elementen aufnehmen kann.Falls mehr als b Elemente auftreten, die alle auf denselben Behälter abgebildetwerden, so entsteht ein Überlauf.Bemerkung 3.5.3. Wie groß ist die Wahrscheinlichkeit, dass Kollisionen auftreten?Sei h : U → {0, . . . , m − 1} eine ideale Hash-Funktion, die die Schlüsselwertegleichmäßig auf alle m Behälter verteilt, d.h. für 0 ≤ i < m istP r ( h(s) = i ) = 1 m .Wir wollen die Wahrscheinlichkeit dafür bestimmen, dass bei einer zufälligenFolge von n Schlüsseln (n < m) eine Kollision auftritt. Es giltP Kollision = 1 − P keine Kollision ,P keine Kollision = P (1) · P (2) · · · P (n),wobei P (i) die Wahrscheinlichkeit dafür ist, dass der i-te Schlüssel auf einenfreien Platz kommt, wenn die Schlüssel 1, . . . , i − 1 auch alle auf freie Plätzegekommen sind. Nun ist P (1) = 1, P (2) = (m − 1)/m, <strong>und</strong> allgemein P (i) =(m − i + 1)/m, alsoP Kollision = 1 −m(m − 1) · · · (m − n + 1)m n .Für m = 365 ergeben sich die folgenden Kollisionswahrscheinlichkeiten:n = 22 : P Kollision ≈ 0, 475n = 23 : P Kollision ≈ 0, 507n = 50 : P Kollision ≈ 0, 970Dies ist das sogenannte ”Geburtstagsparadoxon“: Sind mehr als 23 Personenzusammen, so haben mit mehr als 50% Wahrscheinlichkeit mindestens zwei vonihnen am selben Tag Geburtstag. Für das Hashing bedeutet die obige Analyse,dass Kollisionen praktisch unvermeidbar sind.


3.5. HASHING (STREUSPEICHERUNG) 127Definition 3.5.4. Hashing mit Verkettung: Für jeden Behälter wird eine verketteteListe angelegt, in die alle Schlüssel eingefügt werden, die auf diesenBehälter abgebildet werden. Für die Tabelle aus Beispiel 3.5.2 erhalten wirdamit die folgende Darstellung:01❜❛❜februar ✪ ✪september ✪ ✪234❜❜augustjuli✪ ✪✪ ✪567❜❜maerzoktober❜✪ ✪juni✪ ✪8910❜❜aprilmai❜✪ ✪dezember✪ ✪1112❜januar❜november✪ ✪Programm 3.5.5. Unter Verwendung der Hash-Funktion aus Beispiel 3.5.2<strong>und</strong> dem Java-Typ LinkedList erhalten wir folgende Implementierung für dieOperationen Suchen, Einfügen <strong>und</strong> Löschen:1 import java.util.LinkedList;23 /** Die Klasse OpenHashTable implementiert offenes Hashing.4 * Die Hash-Tabelle enthaelt Strings.5 */6 class OpenHashTable {78 /** Die Anzahl der Behaelter. */9 private static final int ANZAHL BEHAELTER = 13;1011 /** Ein Array von ANZAHL BEHAELTER vielen Listen. */12 private LinkedList[ ] hashTable = new LinkedList[ANZAHL BEHAELTER];1314 /** Konstruiert eine leere Hash-Tabelle. */15 public OpenHashTable( ) { OpenHashTable16 for( int i = 0; i < ANZAHL BEHAELTER; i++ )17 hashTable[i] = new LinkedList( );18 }19


128 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN20 /** Implementiert die Abbildung d(’a’) = 1, . . ., d(’z’) = 26. */21 private static int d( char c ) { d22 return c − ’a’ + 1;23 }2425 /** Ordnet einem Wort w = a 1 a 2 a 3 v mit a i aus {’a’,. . .,’z’} den Wert26 * (d(a 1) + d(a 2) + d(a 3)) mod ANZAHL BEHAELTER zu. Dafuer27 * muss w mindestens die Laenge 3 haben, sonst wird eine Ausnahme ausgeloest.28 */29 private static int hash( String w ) { hash30 if( w.length( ) < 3 )31 throw new IllegalArgumentException( "String zu kurz." );32 return33 ( d( w.charAt( 0 ) ) + d( w.charAt( 1 ) ) + d( w.charAt( 2 ) ) )34 % ANZAHL BEHAELTER;35 }3637 /** Test, ob der String key in der Hash-Tabelle enthalten ist. */38 public boolean isMember( String key ) { isMember39 // Test, ob key in der Liste mit Index hash( key ) enthalten ist:40 return hashTable[ hash( key ) ].contains( key );41 }4243 /** Fuegt den String key in die Hash-Tabelle ein. */44 public void insert( String key ) { insert45 int h = hash( key );46 if( !hashTable[ h ].contains( key ) )47 hashTable[ h ].add( key );48 }4950 /** Entfernt den String key aus der Hash-Tabelle. */51 public void delete( String key ) { delete52 hashTable[ hash( key ) ].remove( key );53 }5455 /** Gibt eine String-Darstellung der Hash-Tabelle zurueck. */56 public String toString( ) { toString57 StringBuffer ausgabe = new StringBuffer( );58 for( int i = 0; i < ANZAHL BEHAELTER; i++ )59 ausgabe.append( "Behaelter " + i + ":\t" + hashTable[ i ] + "\n" );60 return ausgabe.toString( );61 }6263 public static void main( String[ ] args ) { main64 OpenHashTable ht = new OpenHashTable( );65 String[ ] monat = { "januar", "februar", "maerz", "april", "mai", "juni",66 "juli", "august", "september", "oktober", "november", "dezember" };67 for( int i = 0; i < monat.length; i++ )68 ht.insert( monat[ i ] );69 System.out.println( ht );70 }7172 } // class OpenHashTable


3.5. HASHING (STREUSPEICHERUNG) 129Ein Testlauf:Behaelter 0: [februar]Behaelter 1: [september]Behaelter 2: []Behaelter 3: [august]Behaelter 4: [juli]Behaelter 5: []Behaelter 6: [maerz, juni]Behaelter 7: [oktober]Behaelter 8: []Behaelter 9: [april, dezember]Behaelter 10: [mai]Behaelter 11: []Behaelter 12: [januar, november]Sei S = {x 1 , . . . , x n } ⊆ U eine zu speichernde Menge, <strong>und</strong> sei HT eine offeneHash-Tabelle mit Hash-Funktion h. Wir wollen annehmen, dass h in konstanterZeit ausgewertet werden kann. Wieviel Rechenzeit wird dann für die OperationenSuchen, Einfügen <strong>und</strong> Löschen gebraucht?Für 0 ≤ i < m sei HT [i] die Liste der Schlüssel x j , für die h(x j ) = i gilt.Mit |HT [i]| bezeichnen wir die Länge der i-ten Liste. Dann kostet jede Operationim schlechtesten Fall O(|HT [h(x)]|) viele Schritte. Ist nämlich immerh(x j ) = h(x 1 ) für 1 ≤ j ≤ n, dann enthält HT [h(x 1 )] alle n Elemente. Damiterhalten wir die folgende Aussage.Satz 3.5.6. Ist eine Menge S mit n Elementen in einer offenen Hash-Tabellemit Verkettung gespeichert, so braucht die Ausführung einer Operation Suchen,Einfügen oder Löschen im schlechtesten Fall Rechenzeit O(n).Das Verhalten im Mittel ist aber wesentlich besser. Wir betrachten eine beliebigeFolge von n Operationen Suchen, Einfügen <strong>und</strong> Löschen, wobei wir mit derleeren Menge beginnen. Anfänglich sind also alle Listen HT [0], . . . , HT [m − 1]leer. Wir wollen dabei die folgenden Annahmen machen:• Die Hash-Funktion h : U → I = {0, . . . , m − 1} streut die Schlüssel aus Ugleichmäßig über das Intervall I: Für i, j ∈ I gilt|h −1 (i)| = |h −1 (j)| = |U|m .• Alle Schlüssel treten mit derselben Wahrscheinlichkeit als Argument einerOperation auf: Für s ∈ U <strong>und</strong> 1 ≤ i ≤ n giltP r(das Argument der i-ten Operation ist der Schlüssel s) = 1|U| .


130 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENAus diesen beiden Annahmen folgtP r(h liefert für das Argument der i-ten Operation den Wert j) = 1 m ,d.h. h(x 1 ), . . . , h(x n ) kann als eine Folge von n unabhängigen Ausführungeneines Zufallsexperiments mit Gleichverteilung über {0, . . . , m − 1} aufgefasstwerden.Satz 3.5.7. Unter obigen Annahmen hat eine Folge von n Operationen Suchen,Einfügen <strong>und</strong> Löschen im Mittel den Zeitbedarf(O (1 + n )2m ) · n .Ist der Belegungsfaktor n/m klein, so wird also in einer Folge von n solchenOperationen im Mittel jede Operation in konstanter Zeit durchgeführt!Beweis: Zuerst bestimmen wir die mittlere Länge der Liste HT [i] (0 ≤ i < m)nach Ausführung von k ≥ 1 Operationen. Es istP r(während der ersten k Operationen wird j-mal auf HT [i] zugegriffen)( ) ( k 1 j (=1 −j m)m) 1 k−j. (∗)Nachdem j-mal auf die Liste HT [i] zugegriffen worden ist, hat sie höchstensdie Länge j. Sei nun l i (k) die mittlere Länge der Liste HT [i] nach den erstenk Operationen. Dann gilt:l i (k) ≤k∑( ) ( k 1 j (1 −j m)m) 1 k−j· jj=0= k m ·k∑j=1= k k−1m ·∑( k − 1jj=0( ( ) k − 1 1 j−1 (1 −j − 1) 1 k−j ( kdenn =m m)j)k j) ( 1m) j (1 − 1 m) (k−1)−j} {{ }=1 mit (∗)= k m .( ) k − 1j − 1Da dies für alle i gilt, bedeutet dies, dass die (k + 1)-ste Operation im Mittel inZeit O(1 + k/m) ausgeführt werden kann. Damit erhalten wir für die mittlereRechenzeit einer Folge von n Operationen die folgende Abschätzung:n−1∑T (P ) (n) ≤ c ·k=0(1 + k ) (= c · n +m)n(n − 1)(∈ O (1 + n )2m2m ) · n .


3.5. HASHING (STREUSPEICHERUNG) 131Wir wenden uns nun dem geschlossenen Hashing zu, bei dem jeder Behälter nureine konstante Anzahl b ≥ 1 von Schlüsseln aufnehmen kann. Wir betrachtendabei im folgenden den Spezialfall b = 1 <strong>und</strong> behandeln Hash-Tabellen, dieSchlüssel/Wert-Paare mit Schlüsseln vom Typ String <strong>und</strong> Werten vom TypObject speichern. Diese Paarmengen sollen partielle Funktionen sein, d.h. jedemSchlüssel ist jeweils höchstens ein Wert zugeordnet. Die Paare werden durch denTyp Content realisiert, der zwei Felder vorsieht, key vom Typ String <strong>und</strong> valuevom Typ Object. Die Hash-Tabelle besteht dann aus einem Array über demTyp Content.Den Typ Content versehen wir noch mit einem weiteren Feld vom Typ boolean,um zwischen den beiden folgenden Fällen unterscheiden zu können:• Ein Behälter ist leer (empty), also noch nie gefüllt gewesen.• Ein Behälter ist zwar schon gebraucht worden, aber wegen einer Löschoperationzur Zeit nicht aktiv (deleted).Beim geschlossenen Hashing ist die Behandlung auftretender Kollisionen vongroßer Bedeutung. Die gr<strong>und</strong>legende Idee des Rehashing besteht darin, nebender Funktion h = h 0 auch noch weitere Hash-Funktionen h 1 , . . . , h m−1 zu benutzen.Für einen Schlüssel x werden dann die Zellen h 0 (x), h 1 (x), . . . , h m−1 (x)der Reihe nach angeschaut. Sobald eine freie oder als gelöscht markierte Zellemit Schlüssel x gef<strong>und</strong>en wird, kann ein Paar mit Schlüssel x eingefügt werden.Andererseits besagt das erste Auftreten einer freien Zelle, dass x nicht inder Hash-Tabelle enthalten ist. Hier sehen wir auch, warum gelöschte Paaremarkiert werden müssen; diese Technik bezeichnet man als ”lazy deletion“.Die Folge der Funktionen h 0 , . . . , h m−1 sollte so gewählt werden, dass für jedenSchlüsselwert sämtliche Behälter HT [i] (0 ≤ i < m) erreicht werden. Dieeinfachste Strategie hierfür ist das lineare Sondieren (engl. linear probing):h i (x) = (h(x) + i) mod m.Beispiel 3.5.8. (Fortsetzung von Beispiel 3.5.2) Auflösung von Kollisionenmittels linearem Sondieren:0 : februar1 : september2 : november3 : august4 : juli5 :7 : juni8 : oktober9 : april10 : mai11 : dezember12 : januar6 : märz


132 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENProgramm 3.5.9. Geschlossenes Hashing mit linearem Sondieren:1 import java.util.LinkedList;23 /** Die Klasse ClosedHashTableLinearProbing implementiert geschlossenes4 * Hashing mit linearem Sondieren.5 * Die Hash-Tabelle enthaelt Schluessel/Wert-Paare mit Schluesseln vom Typ6 * String <strong>und</strong> Werten vom Typ Object. Diese Paarmenge ist eine partielle Funktion.7 */8 class ClosedHashTableLinearProbing {910 /** Die Anzahl der Behaelter. */11 private static final int ANZAHL BEHAELTER = 13;1213 /** Die Klasse Content implementiert Schluessel/Wert-Paare mit einem14 * zusaetzlichen Feld zur Markierung geloeschter Eintraege in der15 * Hash-Tabelle (‘lazy deletion’). Ist fuer eine im Array gespeicherte16 * Instanz deleted == false, dann sagen wir, dass der Schluessel in der17 * Hash-Table vorhanden ist.18 */19 private class Content {2021 /** Der Schluessel. */22 String key;2324 /** Der Wert zum Schluessel. */25 Object value;2627 /** Gibt an, ob der Eintrag geloescht ist. */28 boolean deleted;2930 /** Konstruiert das Schluessel/Wert-Paar (key, value).31 * Das Feld deleted bekommt den Wert false.32 */33 Content( String key, Object value ) { Content34 this.key = key;35 this.value = value;36 }3738 } // class Content3940 /** Ein Array ueber dem Typ Content.41 * Positionen, die noch nicht gefuellt worden sind, enthalten null.42 * Positionen, die bereits gefuellt waren, aber aktuell leer sind, enthalten43 * ein Objekt content mit content.deleted == true (‘lazy deletion’).44 */45 private Content[ ] hashTable = new Content[ANZAHL BEHAELTER];4647 /** Der parameterlose Konstruktor liefert eine leere Hash-Tabelle. */4849 /** Implementiert die Abbildung d(’a’) = 1, . . ., d(’z’) = 26. */50 private static int d( char c ) { d51 return c − ’a’ + 1;52 }


3.5. HASHING (STREUSPEICHERUNG) 1335354 /** Ordnet einem Wort w = a 1 a 2 a 3 v mit a i aus {’a’,. . .,’z’} den Wert55 * (d(a 1) + d(a 2) + d(a 3)) mod ANZAHL BEHAELTER zu. Dafuer56 * muss w mindestens die Laenge 3 haben, sonst wird eine Ausnahme ausgeloest.57 */58 private static int hash( String w ) { hash59 if( w.length( ) < 3 )60 throw new IllegalArgumentException( "String zu kurz." );61 return62 ( d( w.charAt( 0 ) ) + d( w.charAt( 1 ) ) + d( w.charAt( 2 ) ) )63 % ANZAHL BEHAELTER;64 }6566 /** Gibt den naechsten Behaelter nach dem i-ten Behaelter an. */67 private static int rehash( int i ) { rehash68 return ( i+1 ) % ANZAHL BEHAELTER;69 }7071 /** Test, ob der i-te Behaelter leer ist, d.h. noch nie gefuellt war. */72 private boolean isEmpty( int i ) { isEmpty73 return hashTable[ i ] == null;74 }7576 /** Test, ob der Inhalt des i-ten Behaelters geloescht ist. */77 private boolean isDeleted( int i ) { isDeleted78 return hashTable[ i ].deleted;79 }8081 /** Test, ob i >= 0 gilt <strong>und</strong> der i-te Behaelter weder leer noch geloescht ist. */82 private boolean isActive( int i ) { isActive83 return i != −1 && !isEmpty( i ) && !isDeleted( i );84 }8586 /** Gibt die Position des im Array gespeicherten Eintrags mit Schluessel key87 * zurueck, falls er existiert; dabei wird nicht zwischen geloeschten <strong>und</strong> nicht88 * geloeschten Eintraegen unterschieden. Andernfalls wird die Position des ersten89 * gef<strong>und</strong>enen freien Behaelters zurueckgegeben, so vorhanden, sonst -1.90 */91 private int findPosition( String key ) { findPosition92 int h = hash( key );93 int i = 0;94 while( !isEmpty( h ) && i < ANZAHL BEHAELTER ) {95 if( hashTable[ h ].key.equals( key ) )96 return h;97 i++;98 h = rehash( h );99 }100 if( i < ANZAHL BEHAELTER )101 return h;102 return −1;103 }104


134 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN105 /** Test, ob der Schluessel key in der Hash-Tabelle vorhanden ist. */106 public boolean isMember( String key ) { isMember107 return isActive( findPosition( key ) );108 }109110 /** Gibt den zum Schluessel key in der Hash-Tabelle gespeicherten Wert zurueck,111 * falls der Schluessel vorhanden ist, sonst null.112 */113 public Object value( String key ) { value114 int pos = findPosition( key );115 return isActive( pos ) ? hashTable[ pos ].value : null;116 }117118 /** Der zum Schluessel key in der Hash-Tabelle gespeicherte Wert wird zu v,119 * falls der Schluessel vorhanden ist.120 */121 public void changeValue( String key, Object v ) { changeValue122 int pos = findPosition( key );123 if( isActive( pos ) )124 hashTable[ pos ].value = v;125 }126127 /** Fuegt das Schluessel/Wert-Paar (key, value) in die Hash-Tabelle ein,128 * falls der Schluessel key nicht in der Hash-Tabelle vorhanden ist.129 * Gibt false zurueck, wenn das Paar eingefuegt werden muesste, aber kein130 * freier Behaelter mehr vorhanden ist, sonst true.131 */132 public boolean insert( String key, Object value ) { insert133 int pos = findPosition( key );134 if( pos == −1 ) // kein freier Behaelter vorhanden135 return false;136 if( !isActive( pos ) )137 hashTable[ pos ] = new Content( key, value ); // Paar einfuegen138 return true;139 }140141 /** Entfernt das Paar mit Schluessel key aus der Hash-Tabelle. */142 public void delete( String key ) { delete143 int pos = findPosition( key );144 if( isActive( pos ) ) {145 hashTable[ pos ].deleted = true;146 hashTable[ pos ].value = null; // fuer Garbage-Collection147 }148 }149150 /** Gibt eine String-Darstellung der Hash-Tabelle zurueck. */151 public String toString( ) { toString152 StringBuffer ausgabe = new StringBuffer( );153 for( int i = 0; i < ANZAHL BEHAELTER; i++ )154 ausgabe.append( "Behaelter " + i + ":\t" +155 ( hashTable[ i ] == null | | hashTable[ i ].deleted ? "--\n" :156 "(" + hashTable[ i ].key + ", " + hashTable[ i ].value + ")\n" ) );157 return ausgabe.toString( );158 }159


3.5. HASHING (STREUSPEICHERUNG) 135160 public static void main( String[ ] args ) { main161 ClosedHashTableLinearProbing ht = new ClosedHashTableLinearProbing( );162 String[ ] monat = { "januar", "februar", "maerz", "april", "mai", "juni",163 "juli", "august", "september", "oktober", "november", "dezember" };164 String[ ] tage = { "31", "28", "31", "30", "31", "30", "31",165 "31", "30", "31", "30", "31" };166 for( int i = 0; i < monat.length; i++ )167 ht.insert( monat[ i ], tage[ i ] );168 System.out.println( ht );169 }170171 } // class ClosedHashTableLinearProbingEin Testlauf:Behaelter 0: (februar, 28)Behaelter 1: (september, 30)Behaelter 2: (november, 30)Behaelter 3: (august, 31)Behaelter 4: (juli, 31)Behaelter 5: --Behaelter 6: (maerz, 31)Behaelter 7: (juni, 30)Behaelter 8: (oktober, 31)Behaelter 9: (april, 30)Behaelter 10: (mai, 31)Behaelter 11: (dezember, 31)Behaelter 12: (januar, 31)Jedes Element, das mit der Hash-Funktion auf einen bereits belegten Behälterabgebildet wird, wird durch lineares Sondieren bis zum nächsten unbelegtenBehälter ”verschoben“. Sind also k Behälter hintereinander belegt, so ist dieWahrscheinlichkeit, dass im nächsten Schritt der erste freie Behälter nach diesenk Behältern belegt wird, mit (k + 1)/m wesentlich größer als die Wahrscheinlichkeit,dass ein Behälter im nächsten Schritt belegt wird, dessen Vorgängernoch frei ist. Dadurch entstehen beim linearen Sondieren regelrechte Ketten.Satz 3.5.10. [Knuth 1973] Sei α = n/m der Belegungsfaktor einer Hash-Tabelle der Größe m, die mit n Elementen gefüllt ist. Beim Hashing mit linearemSondieren entstehen für eine Operation durchschnittlich folgende Kosten:(1.) ( 1 + 1/(1 − α) ) /2 beim erfolgreichen Suchen <strong>und</strong>(2.) ( 1 + 1/(1 − α) 2) /2 beim erfolglosen Suchen.Für verschiedene Belegungsfaktoren α erhalten wir die folgenden Werte:


136 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENα (1.) (2.)20% 1,125 1,2850% 1,5 2,570% 2,17 6,0680% 3 1390% 5,5 50,595% 10,5 200,5Bei dicht besetzten Tabellen wird Hashing mit linearem Sondieren also sehrineffizient.Definition 3.5.11. Weitere Kollisionsstrategien(a) Verallgemeinertes lineares Sondieren (1 ≤ i < m):h i (x) = (h(x) + c · i) mod m.Dabei sollten c <strong>und</strong> m teilerfremd sein, um alle Behälter zu erreichen.(b) Quadratisches Sondieren (1 ≤ i < m):oder (1 ≤ i ≤ (m − 1)/2)h i (x) = (h(x) + i 2 ) mod m,h 2i−1 (x) = (h(x) + i 2 ) mod m,h 2i (x) = (h(x) − i 2 ) mod m.Wählt man bei der zweiten Variante m = 4j + 3 als Primzahl, so wirdjeder Behälter getroffen.(c) Doppel-Hashing: Seien h, h ′ : U → {0, . . . , m − 1} zwei Hash-Funktionen.Dabei seien h <strong>und</strong> h ′ so gewählt, dass für beide eine Kollision nur mitWahrscheinlichkeit 1/m auftritt, d.h.P r ( h(x) = h(y) ) = P r ( h ′ (x) = h ′ (y) ) = 1 m .Die Funktionen h <strong>und</strong> h ′ heißen unabhängig, wenn eine Doppelkollisionnur mit Wahrscheinlichkeit 1/m 2 auftritt, d.h.P r ( h(x) = h(y) <strong>und</strong> h ′ (x) = h ′ (y) ) = 1 m 2 .Wir erhalten nun eine Folge von Hash-Funktionen wie folgt (i ≥ 1):h i (x) = (h(x) + h ′ (x) · i 2 ) mod m.Dies ist eine gute Methode, bei der die Schwierigkeit aber darin liegt,Paare von Funktionen zu finden, die wirklich unabhängig sind.


3.5. HASHING (STREUSPEICHERUNG) 137Programm 3.5.12. Eine Variante von Programm 3.5.9 mit quadratischemSondieren:69 private static int rehash( int h, int i ) { rehash70 int j = ( i+1 )/2;71 if( i%2 == 0 )72 j = ( h−j*j ) % ANZAHL BEHAELTER;73 else74 j = ( h+j*j ) % ANZAHL BEHAELTER;75 if( j < 0 )76 j += ANZAHL BEHAELTER;77 return j;78 }79. . .100 private int findPosition( String key ) { findPosition101 int hInitial = hash( key );102 int h = hInitial;103 int i = 0;104 while( !isEmpty( h ) && i < ANZAHL BEHAELTER ) {105 if( hashTable[ h ].key.equals( key ) )106 return h;107 i++;108 h = rehash( hInitial, i );109 }110 if( i < ANZAHL BEHAELTER )111 return h;112 return −1;113 }Definition 3.5.13. Einige Hash-Funktionen:Sei nat : U → N eine Funktion, die jedem möglichen Schlüssel eine natürlicheZahl zuordnet. Durch h(x) = nat(x) mod m erhalten wir dann eine Hash-Funktion. Wir schauen uns im folgenden verschiedene Funktionen U → N an.(a) Sei U = Σ ∗ die Menge der Wörter über dem Alphabet Σ = {a, b, . . . , z}.Dann kann ein Wort w = a 1 a 2 . . . a n (a i ∈ Σ) als eine Zahlnat(w) = a 1 · 26 n−1 + a 2 · 26 n−2 + · · · + a n−1 · 26 + a naufgefasst werden, wenn man a mit 0, b mit 1, . . . , z mit 25 identifiziert.(b) Die Mittel-Quadrat-Methode: Sei U ⊆ N, <strong>und</strong> sei k = ∑ li=0 z i · 10 i , d.h.k wird durch die Ziffernfolge z l z l−1 . . . z 0 beschrieben. Den Wert h(k)erhält man nun dadurch, dass man aus der Mitte der Ziffernfolge vonk 2 einen hinreichend großen Block nimmt. Da die mittleren Ziffern vonk 2 von allen Ziffern von k abhängen, ergibt dies eine gute Streuung vonaufeinanderfolgenden Werten.


138 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENBeispiel: Sei m = 100.k k mod 100 k 2 h(k)127 27 16129 12128 28 16384 38129 29 16641 64(c) Shift-Folding: Jedes Wort w wird als eine Binärzahl bin(w) aufgefasst.Diese Binärzahl wird in Teile der Länge k zerlegt, wobei das letzte Teileventuell kürzer sein kann. Diese Teile werden nun als Binärzahlen aufgefasst<strong>und</strong> addiert. Dies ergibt eine Zahl nat(w). Beispiel w = emin:w ↦→ (5, 13, 9, 14) ↦→ 0000 0101 0000 1101 0000 1001 0000 1110.Für k = 5 erhalten wir die folgenden Teile mit Summe nat(w) = 65:000001010000110100001001000011101000001Wie wir in Satz 3.5.7 gesehen haben, kostet eine Operation Suchen, Einfügenoder Löschen beim Hashing im Mittel O(1) Schritte. Andererseits kann eine solcheOperation im schlechtesten Fall aber auch O(n) Schritte kosten. Für jedeHash-Funktion h : U → {0, . . . , m − 1} sind es gewisse Schlüsselmengen S ⊆ U,die dieses schlechte Verhalten bewirken. Würde man S kennen, könnte maneine Hash-Funktion auswählen, die für diese Schlüsselmenge möglichst wenigeKollisionen <strong>und</strong> damit ein gutes Laufzeitverhalten ergibt. Leider kennt man dieMenge S in den meisten Anwendungen aber nicht. Die Idee des universellenHashing ist nun die folgende. Man hat eine Klasse H von Hash-Funktionen vonU nach {0, . . . , m − 1}. Aus dieser Klasse wählt man zur Laufzeit eine Funktiondurch ein Zufallsexperiment aus. Ist die Klasse H geeignet gewählt, so kannman zeigen, dass für jede Schlüsselmenge S ⊆ U im Mittel ein gutes Laufzeitverhaltenerreicht wird. Wir haben es hier also mit einem probabilistischenAlgorithmus zu tun.Definition 3.5.14. Sei H eine endliche Menge von Hash-Funktionen, die dieMenge U auf {0, . . . , m − 1} abbilden. Die Menge H heißt universell, falls füralle Elemente x, y ∈ U mit x ≠ y folgendes gilt:P r ( h ∈ H | h(x) = h(y) ) = 1 m ,d.h. die Wahrscheinlichkeit dafür, dass eine aus H zufällig ausgewählte Funktionx <strong>und</strong> y auf denselben Behälter abbildet, ist mit 1/m genauso groß wie die


3.5. HASHING (STREUSPEICHERUNG) 139Wahrscheinlichkeit dafür, dass eine Kollision auftritt, wenn man die Werte h(x)<strong>und</strong> h(y) zufällig <strong>und</strong> unabhängig voneinander aus {0, . . . , m − 1} auswählt.Satz 3.5.15. Sei S ⊆ U eine beliebige Schlüsselmenge mit n Elementen, <strong>und</strong>sei H eine universelle Menge von Hash-Funktionen von U nach {0, . . . , m − 1}.Sei h ∈ H zufällig ausgewählt. Für jeden Schlüssel x ∈ S ist dann die mittlereAnzahl der Kollisionen |{y ∈ S \ {x} | h(y) = h(x)}| höchstens n/m.Beweis: Für x, y ∈ S mit x ≠ y bezeichne c xy die folgende Zufallsvariable:{ 1, falls h(x) = h(y),c xy =0, falls h(x) ≠ h(y).Dann giltE(c xy ) = ∑ h∈Hc xy · P r(h) = P r ( h ∈ H | h(x) = h(y) ) = 1 m .Für die Zufallsvariable C x = |{y ∈ S \ {x} | h(y) = h(x)}| gilt damit( ∑ )E(C x ) = E c xy ≤∑E(c xy ) =∑ 1m ≤ n m .y∈S\{x}y∈S\{x}y∈S\{x}Ist also n ≤ m, so ist die mittlere Anzahl der zu erwartenden Kollisionenhöchstens 1. Wir beschließen diesen Abschnitt mit einem Beispiel für eine universelleMenge von Hash-Funktionen.Definition 3.5.16. Alle Schlüssel x ∈ U seien durch Bitstrings der Länge lgegeben. Wir zerlegen jeden dieser Bitstrings in Blöcke derselben Länge k, sodass 2 k ≤ m gilt. Dann kann jeder Block als eine Adresse aus {0, . . . , m − 1}aufgefasst werden. Sei nun x ∈ U, <strong>und</strong> sei (x 0 , . . . , x r ) die Blockzerlegung vonx. Die Klasse H besteht aus allen Hash-Funktionen h = h a , die durch ein Tupela = (a 0 , . . . , a r ) mit a i ∈ {0, . . . , m − 1} spezifiziert werden, wobei h a durchh a (x) =r∑a i · x i mod mi=0definiert ist. Es gibt also m r+1 viele verschiedene Funktionen in H.Satz 3.5.17. Ist m eine Primzahl, so ist H eine universelle Klasse von Hash-Funktionen.Beweis: Seien x = (x 0 , . . . , x r ) <strong>und</strong> y = (y 0 , . . . , y r ) zwei verschiedene Schlüsselaus U mit x i , y j ∈ {0, . . . , m−1}. Da x ≠ y ist, gibt es einen Index s mit x s ≠ y s .Ohne Beschränkung der Allgemeinheit sei s = 0, d.h. x 0 ≠ y 0 . Dann isth a (x) =r∑a i x i mod m, h a (y) =i=0r∑a i y i mod m,i=0


140 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENalso gilth a (x) = h a (y)gdwgdwr∑a i x i ≡i=0a 0 (x 0 − y 0 ) ≡r∑a i y i mod mi=0r∑a i (y i − x i ) mod m.Da m eine Primzahl ist, gibt es eine Zahl (x 0 − y 0 ) −1 ∈ {1, . . . , m − 1} mit(x 0 − y 0 ) · (x 0 − y 0 ) −1 ≡ 1 mod m, d.h. die obige Gleichheit ist äquivalent zur∑a 0 = (x 0 − y 0 ) −1 · a i (y i − x i ) mod m.i=1Für jede Wahl von a 1 , . . . , a r ∈ {0, . . . , m − 1} gibt es also genau einen Werta 0 ∈ {0, . . . , m − 1}, so dass für a = (a 0 , a 1 , . . . , a r ) die Gleichheit h a (x) =h a (y) gilt. Von den m r+1 vielen Funktionen aus H erfüllen also genau m r vieleFunktionen diese Gleichheit, d.h. für alle x, y ∈ U mit x ≠ y isti=1P r(h ∈ H | h(x) = h(y)) = 1 m .Also ist H eine universelle Klasse von Hash-Funktionen.Um diese universelle Klasse zu benutzen, braucht man einen Prozess, der dier + 1 Zahlen a 0 , . . . , a r ∈ {0, . . . , m − 1}, die den Schlüssel a = (a 0 , . . . , a r )bilden, zufällig <strong>und</strong> unabhängig auswählt. Daher ist die Aufgabe, (Pseudo-)Zufallszahlen effektiv zu bestimmen, von großer Wichtigkeit. Leider können wirhier nicht darauf eingehen.3.6 Partitionen von Mengen mit UNION <strong>und</strong> FINDUnsere Betrachtungen zur Darstellung von Mengen <strong>und</strong> Operationen auf Mengenwollen wir mit einem Datentyp abschließen, der es erlaubt, Partitionen vonendlichen Mengen zu beschreiben. Sei S eine endliche Menge. Wir betrachtender Form”Äquivalenz-Anweisungen“a 1 ≡ b 1 , a 2 ≡ b 2 , . . . , a m ≡ b m ,wobei die a i , b j ∈ S sind. Die Anweisung a i ≡ b i besagt, dass die Äquivalenzklassenvon a i <strong>und</strong> b i zu einer Klasse verschmolzen werden sollen. Außerdemwollen wir die Äquivalenzklasse eines gegebenen Elements aus S bestimmenkönnen, etwa indem wir einen Repräsentanten dieser Klasse angeben.Beispiel 3.6.1. Sei S = {1, 2, 3, 4, 5, 6}. Anfänglich sei jedes Element für sicheine Klasse der betrachteten Partition P , d.h. P = {{1}, {2}, {3}, {4}, {5}, {6}}.Die Anweisungen unten bewirken nun die folgenden Veränderungen von P :1 ≡ 4 ❀ P = {{1, 4}, {2}, {3}, {5}, {6}}2 ≡ 5 ❀ P = {{1, 4}, {2, 5}, {3}, {6}}2 ≡ 4 ❀ P = {{1, 2, 4, 5}, {3}, {6}}


3.6. PARTITIONEN VON MENGEN MIT UNION UND FIND 141Auf einer Partition P wollen wir also folgende Operationen ausführen:• Vereinigen (UNION): Ersetze S 1 ∈ P <strong>und</strong> S 2 ∈ P durch S 1 ∪ S 2 .• Finden (FIND): Bestimme zu i ∈ S die Teilmenge S j ∈ P mit i ∈ S j .Definition 3.6.2. Eine Implementierung von Partitionen als Bäume:Sei {S 1 , . . . , S m } eine Partition von {1, . . . , n}. Ist |S i | = l i , so stellen wir dieseMenge durch einen Baum mit l i Knoten dar, wobei jeder Knoten ein Elementvon S i als Inhalt hat. Bei der Implementierung dieser Bäume soll von jedemKnoten, der nicht Wurzel ist, ein Verweis auf seinen Elternknoten zeigen.Beispiel 3.6.3. Für S = {1, . . . , 10} <strong>und</strong> P = {S 1 , S 2 , S 3 } mit S 1 = {1, 7, 8, 9},S 2 = {2, 5, 10} <strong>und</strong> S 3 = {3, 4, 6} ergibt sich folgende Baummenge: 1 7 8 9 5 2 10 3 4 6S 1 S 2 S 3Haben wir die Mengennamen S 1 , . . . , S m (z.B. in einer Liste) gespeichert, sokann mit jedem Namen ein Verweis auf die Wurzel des zugehörigen Baums gespeichertwerden, <strong>und</strong> umgekehrt kann in der Wurzel des Baums ein Verweis aufden Mengennamen gespeichert sein. Zur Vereinfachung wollen wir im folgendendaher die Mengen S 1 , . . . , S m mit den Elementen in den Wurzeln der darstellendenBäume identifizieren, d.h. diese Elemente dienen als Repräsentanten derMengen S 1 , . . . , S m .Um zwei disjunkte Mengen S i <strong>und</strong> S j zu vereinigen, kann man nun einfachdie Wurzel des einen Baums zu einem Kind der Wurzel des anderen Baumsmachen. Wählen wir für die Darstellung ein Array ganzer Zahlen parent, wobeiwir in parent[i] einfach das Element angeben, das als Elternknoten von Elementi auftritt, so erhalten wir folgende einfachen <strong>Algorithmen</strong> für die Realisierungder obigen beiden Operationen.1 /** Ersetze die Mengen mit Wurzeln i <strong>und</strong> j durch ihre Vereinigung. */2 void union’( int i, int j) { union’3 if( i != j )4 parent[i] = j;5 }67 /** Liefert die Menge (d.h. den Repräsentaten der Menge), die i enthält. */8 int find’( int i ) { find’9 int h = i;10 while( parent[h] > 0 )11 h = parent[h];12 return h;13 }


142 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENBeispiel 3.6.4. (Fortsetzung von Beispiel 3.6.3) Führt man die Operationunion’(1, 5) aus, dann entsteht der untenstehende Baum. Die Operation find’(8)liefert darauf das Resultat 5. 5 1 2 10 7 8 9Diese <strong>Algorithmen</strong> sind zwar einfach, aber leider haben sie im Allgemeinen einsehr ungünstiges Laufzeitverhalten.Beispiel 3.6.5. Für die Partition {S 1 , . . . , S n } mit S i = {i} führen wir folgendeOperationen in der angegebenen Reihenfolge durch (p ≤ n):union’(1, 2), find’(1), union’(2, 3), find’(1), . . . , union’(p − 1, p), find’(1). p . 2 1 p + 1 . . . nDadurch entsteht die nebenstehende Partition.Die p−1 union’-Operationen kosten Zeit O(p).Jede find’-Operation kostet soviel Zeit, wie derzu durchlaufende Weg lang ist, der i-te Aufrufkostet also Zeit O(i). Als Gesamtzeit erhaltenwir damit O(p 2 ).Die Gesamtzeit für die Ausführung einer Folge von Vereinigungs- <strong>und</strong> Finde-Operationen kann erheblich verbessert werden, wenn wir bei der Vereinigungdie folgende Gewichtungsregel benutzen: Ist die Anzahl der Knoten im Baumi kleiner als die Anzahl der Knoten im Baum j, so wird i zum Kind von j,andernfalls wird j zum Kind von i.Beispiel 3.6.6. (Fortsetzung von Beispiel 3.6.5) Diesmal führen wir die obigeFolge von Operationen aus, indem wir bei der Vereinigung die Gewichtungsregelbenutzen. Wir erhalten dann folgende Partition in Gesamtzeit O(p): 1 p + 1 . . . n 2 3 . . . pUm die Vereinigung auf diese Weise zu realisieren, müssen wir mit jedem Baumdie Anzahl seiner Knoten speichern. Dies können wir in der Wurzel tun, dadie Wurzel ja keinen Verweis auf einen Elternknoten hat. Um die Fälle, ob inparent[i] nun ein Verweis oder die Anzahl n der Knoten steht, d.h. ob i Wurzel


3.6. PARTITIONEN VON MENGEN MIT UNION UND FIND 143ist oder nicht, unterscheiden zu können, speichern wir in der Wurzel die Zahl−n statt n. Damit erhalten wir folgenden Algorithmus.1 /** Ersetze die Mengen mit Wurzeln i <strong>und</strong> j durch ihre2 * Vereinigung, wobei die Gewichtungsregel benutzt wird.3 * Wir setzen parent[i] = -(Groesse des Baums mit Wurzel i) <strong>und</strong>4 * parent[j] = -(Groesse des Baums mit Wurzel j) voraus.5 */6 void union( int i, int j ) { union7 if( i != j ) {8 if( parent[i] > parent[j] ) { // Baum i ist kleiner als Baum j9 parent[j] += parent[i];10 parent[i] = j;11 }12 else { // parent[i]


144 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGENFür eine Finde-Operation brauchen wir also höchstens O(log m) Schritte, wennsie auf einem Baum mit m Knoten ausgeführt wird, der durch den Algorithmusunion aufgebaut worden ist.Folgerung 3.6.8. Eine beliebige Folge von p Vereinigungs-Operationen <strong>und</strong> qFinde-Operationen kann in Zeit O(p + q · log p) ausgeführt werden.Beweis: Ein Baum mit m Knoten kann nur mit m − 1 Anwendungen derVereinigungs-Operation aufgebaut werden, da wir immer mit der Partition{{1}, {2}, . . . , {n}} beginnen.Die Schranke aus Lemma 3.6.7 ist scharf, denn mit m − 1 Vereinigungen kanntatsächlich ein Baum mit m Knoten <strong>und</strong> Höhe ⌊log m⌋ erzeugt werden.Eine weitere Verbesserung können wir erzielen, wenn bei der Finde-Operationdie folgende Kompressionsregel benutzen wird: Mache alle Knoten auf dem Wegvom Knoten i zur Wurzel (einschließlich Knoten i) zu Kindern der Wurzel.Unter Benutzung dieser Regel erhalten wir folgenden Algorithmus.1 /** Gibt die Wurzel des Baums zurueck, der i enthaelt,2 * wobei die Kompressionsregel benutzt wird.3 */4 int find( int i ) { find5 int w = i;6 while( parent[w] >= 0 ) // solange w nicht die Wurzel ist7 w = parent[w];8 // w ist nun Repraesentant der Teilmenge, die i enthaelt.9 // Realisiere noch die Kompressionsregel:10 int tmp;11 while( i != w ) {12 tmp = parent[i];13 parent[i] = w;14 i = tmp;15 }16 return w;17 }Zeitbedarf von find: O(Tiefe des Knotens i).Beispiel 3.6.9. Aus der Partition {{1}, . . . , {8}} entsteht durch die Operationenfolgeunion(1,2), union(3,4), union(5,6), union(7,8), union(1,3), union(5,7),union(1,5) der Baum 1 2 3 5 4 6 7 8


3.6. PARTITIONEN VON MENGEN MIT UNION UND FIND 145Durch Operation find(8) (mit Ausgabe 1) entsteht daraus der neue Baum 1 2 3 5 7 8 4 6Um die Gesamtzeit für eine Folge von q Vereinigungs-Operationen <strong>und</strong> p ≥ qFinde-Operationen abzuschätzen, brauchen wir die Ackermannsche Funktion 3A : N × N → N, die so definiert ist (p, q ≥ 0):A(0, q) = 2q,A(p + 1, 0) = 1,A(p + 1, q + 1) = A(p, A(p + 1, q)).Die folgende Funktion ist vom Wachstum her gerade umgekehrt:α(m, n) = min{z ≥ 1 | A (z, 4 · ⌈m/n⌉) > log n}.Lemma 3.6.10. A ist im ersten Argument monoton wachsend <strong>und</strong> im zweitenstreng monoton wachsend: A(p + 1, q) ≥ A(p, q) <strong>und</strong> A(p, q + 1) > A(p, q).Die Funktion A wächst extrem schnell. So gilt z.B. A(3, 4) = 2 2..2 , 65536-malgeschachtelt. Und für m > 0 <strong>und</strong> 1 ≤ n < 2 A(3,4) , also log n < A(3, 4), istA(3, 4 · ⌈m/n⌉) ≥ A(3, 4) > log n, damit α(m, n) ≤ 3. Also wächst die Funktionα extrem langsam; in der Praxis ist sie von einer konstanten Funktion nicht zuunterscheiden.Satz 3.6.11. Die Gesamtzeit, die maximal benötigt wird, eine Folge von qVereinigungs-Operationen <strong>und</strong> p ≥ q Finde-Operationen mit den <strong>Algorithmen</strong>union <strong>und</strong> find auszuführen, ist in Θ(p · α(p, q)).Beweis: Siehe R. Tarjan, Efficiency of a good but not linear set union algorithm,Journal of the ACM 22(2) (1975), S. 215–225, oder auch K. Mehlhorn, Datastructures and algorithms, Vol. 1, Springer 1984, S. 300–304.Programm 3.6.12. Eine Implementierung der UNION-FIND-<strong>Algorithmen</strong>:1 /** Die Klasse Partition implementiert Partitionen endlicher Mengen. */2 public class Partition {34 /** Die Groesse des Universums {0,. . .,cardinality-1}. */5 private final int cardinality;63 Wilhelm Ackermann (1896–1962)


146 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN7 /** Das Array zur Speicherung von Verweisen auf Elternknoten8 * bzw. von Baumgroessen. Die Knoten sind 0 bis cardinality-1.9 * Alle Werte >= 0 sind Verweise auf Elternknoten,10 * alle Werte < 0 sind negierte Baumgroessen.11 */12 private int[ ] parent;1314 /** Konstruiert die feinste Partition des Universums mit n Elementen:15 * jede Menge enthaelt genau ein Element.16 */17 public Partition( int n ) { Partition18 cardinality = n;19 parent = new int[cardinality];20 for( int i = 0; i < cardinality; i++ )21 parent[i] = −1;22 }2324 /** Ersetze die Mengen mit Wurzeln i <strong>und</strong> j durch ihre25 * Vereinigung, wobei die Gewichtungsregel benutzt wird.26 * Wir setzen parent[i] = -(Groesse des Baums mit Wurzel i) <strong>und</strong>27 * parent[j] = -(Groesse des Baums mit Wurzel j) voraus.28 * Wir setzen ausserdem i,j < cardinality voraus.29 */30 public void union( int i, int j ) { union31 if( parent[i] >= 0 | | parent[j] >= 0 )32 throw new33 IllegalArgumentException( "Union erwartet zwei Repraesentanten.");34 if( i >= cardinality | | j >= cardinality )35 throw new36 IllegalArgumentException( "Union erwartet Elemente des Universums.");37 if( i != j ) {38 if( parent[i] > parent[j] ) { // Baum i ist kleiner als Baum j39 parent[j] += parent[i];40 parent[i] = j;41 }42 else { // parent[i] = 0 ) // solange w nicht die Wurzel ist59 w = parent[w];


3.6. PARTITIONEN VON MENGEN MIT UNION UND FIND 14760 // w ist nun Repraesentant der Teilmenge, die i enthaelt.61 // Realisiere noch die Kompressionsregel:62 int tmp;63 while( i != w ) {64 tmp = parent[i];65 parent[i] = w;66 i = tmp;67 }68 return w;69 }7071 /** Gibt eine String-Repraesentation der Partition zurueck. */72 public String toString( ) { toString73 StringBuffer result = new StringBuffer( );74 for( int i = 0; i < cardinality; i++ )75 if( parent[i] < 0 ) // falls i Repraesentant ist76 result.append( i + " ist Repraesentant.\n" );77 else // i hat einen Elternknoten78 result.append( i + " hat Elternknoten " + parent[i] + "\n" );79 return result.toString( );80 }8182 public static void main( String[ ] args ) { main83 Partition p = new Partition( 9 );84 p.union( 1, 2 );85 p.union( 3, 4 );86 p.union( 5, 6 );87 p.union( 7, 8 );88 p.union( 1, 3 );89 p.union( 5, 7 );90 p.union( 1, 5 );91 System.out.println( "Vor find(8):" );92 System.out.println( p );93 System.out.println( "find(8) liefert " + p.find( 8 ) );94 System.out.println( "Nach find(8):" );95 System.out.println( p );96 }97 }


148 KAPITEL 3. DATENTYPEN ZUR DARSTELLUNG VON MENGEN


Kapitel 4Graphen <strong>und</strong>Graph-<strong>Algorithmen</strong>Graphen sind eine mathematische Struktur, die eine Menge von Objekten zusammenmit einer Relation auf diesen Objekten darstellt. Beispiele für solcheMengen von Objekten <strong>und</strong> Relationen sind die folgenden:• Objekte: Personen, Relation: A kennt B.• Objekte: Flughäfen, Relation: es gibt einen Non-Stop-Flug von A nach B.• Objekte: Methoden eines Java-Programms, Relation: A ruft B auf.Üblicherweise wird ein Graph durch ein Diagramm beschrieben: Die Objektewerden als Knoten dargestellt, <strong>und</strong> die Relation zwischen zwei Objekten wirddurch eine Kante zwischen den entsprechenden Knoten beschrieben. Im allgemeinensind Kanten als Pfeile dargestellt, d.h. wir haben gerichtete Kanten. Indiesem Fall sprechen wir auch von einem gerichteten Graphen. Ist die betrachteteRelation E aber symmetrisch (d.h. wenn aus (a, b) ∈ E stets (b, a) ∈ E folgt)dann ersetzen wir die beiden gerichteten Kanten a → b <strong>und</strong> b → a durch eineungerichtete Kante a − b. In diesem Fall sprechen wir von einem ungerichtetenGraphen. 0 1 2ein gerichteter Graph 0 1 2 3ein ungerichteter Graph149


150 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN4.1 Gerichtete GraphenDefinition 4.1.1. Ein gerichteter Graph G = (V, E) besteht aus einer endlichenMenge V von Knoten <strong>und</strong> einer Menge E ⊆ V × V von Kanten.Beispiel 4.1.2. Der Graph G 1 = ({0, 1, 2}, {(0, 1), (1, 0), (1, 2)}) ist gerade derdurch das obige Diagramm beschriebene gerichtete Graph.Eine Kante (u, v) ∈ E hat den Startknoten u <strong>und</strong> den Zielknoten v. Die Kante(u, v) ist mit den Knoten u <strong>und</strong> v inzident, <strong>und</strong> die Knoten u <strong>und</strong> v sind adjazentoder benachbart.Definition 4.1.3. Sei G = (V, E) ein gerichteter Graph.• Ein Weg in G ist eine Folge (v 0 , v 1 , . . . , v r ) von Knoten mit (v i , v i+1 ) ∈ Efür 0 ≤ i < r. Die Länge dieses Wegs ist die Anzahl r der durchlaufenenKanten. Ein Weg heißt einfach, wenn alle auftretenden Knoten paarweiseverschieden sind, wobei die Ausnahme v 0 = v r zugelassen ist.• Ein Teilgraph von G ist ein Graph G ′ = (V ′ , E ′ ) mit V ′ ⊆ V <strong>und</strong> E ′ ⊆ E.• Zwei Knoten u, v ∈ V heißen stark verb<strong>und</strong>en, wenn es einen Weg von unach v <strong>und</strong> einen Weg von v nach u gibt. Eine stark verb<strong>und</strong>ene Komponente(eine starke Komponente) ist ein Teilgraph mit maximaler Knotenzahl,in dem alle Knoten paarweise stark verb<strong>und</strong>en sind. Hat G nur einestarke Komponente, so heißt G stark verb<strong>und</strong>en.Beispiel 4.1.4. (Fortsetzung von Beispiel 4.1.2) Der Graph G 1 hat die beidenfolgenden starken Komponenten, ist also nicht stark verb<strong>und</strong>en: 0 1<strong>und</strong> 2Definition 4.1.5. Der Grad d(v) eines Knotens v ∈ V ist die Anzahl derKanten, mit denen v inzident ist. Der Eingangsgrad d + (v) ist die Anzahl derKanten, die v als Zielknoten haben, <strong>und</strong> der Ausgangsgrad d − (v) ist die Anzahlder Kanten, die v als Startknoten haben. Offensichtlich gilt d(v) = d + (v) +d − (v).Lemma 4.1.6. Sei G = (V, E) ein gerichteter Graph mit V = {v 0 , . . . , v n−1 }.Dann gilt n−1 ∑d + (v i ) = n−1 ∑d − (v i ) = |E| <strong>und</strong> insbesondere n−1 ∑d(v i ) = 2|E|.i=0i=0In vielen Anwendungen werden Graphen betrachtet, deren Kanten gewisse ”Kosten“zugeordnet sind.Definition 4.1.7. Eine Gewichtungsfunktion c : E → R + ordnet jeder Kantee eines Graphen G = (V, E) ein Gewicht c(e) (die Kosten der Kante e) zu.i=0


4.1. GERICHTETE GRAPHEN 151Als erstes wollen wir uns nun der Implementierung von gerichteten Graphen(mit oder ohne Gewichtungsfunktion) zuwenden. Hierfür gibt es mehrere Möglichkeiten.Definition 4.1.8. Implementierung von Graphen durch Adjazenzmatrizen: SeiG = (V, E) mit V = {v 0 , . . . , v n−1 } ein gerichteter Graph. Die Adjazenzmatrixfür G ist die boolesche (n × n)-Matrix (A i,j ) 0≤i,j


152 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN16 /** Konstruiert einen Graphen mit n Knoten <strong>und</strong> keiner Kante. */17 public DirectedGraph( int n ) { DirectedGraph18 N = n;19 matrix = new boolean[N][N];20 }2122 /** Konstruiert einen Zufallsgraphen mit n Knoten.23 * Jede Kante ist mit Wahrscheinlichkeit p vorhanden.24 */25 public DirectedGraph( int n, double p ) { DirectedGraph26 this( n );27 for( int v = 0; v < N; v++ )28 for( int w = 0; w < N; w++ )29 if( Math.random( ) < p )30 insertEdge( v, w );31 }3233 /** Gibt die Knotenzahl zurueck. */34 public int nodeSize( ) { nodeSize35 return N;36 }3738 /** Gibt die Kantenzahl zurueck. */39 public int edgeSize( ) { edgeSize40 int e = 0;41 for( int v = 0; v < N; v++ )42 for( int w = 0; w < N; w++ )43 e += isEdge( v, w ) ? 1 : 0;44 return e;45 }4647 /** Gibt true zurueck, wenn eine Kante zwischen Knoten v <strong>und</strong> w vorhanden ist,48 * sonst false. Falls nicht 0 = N | | w >= N )52 throw new IllegalArgumentException( "Knoten nicht vorhanden." );53 return matrix[v][w];54 }5556 /** Fuegt eine Kante zwischen Knoten v <strong>und</strong> w ein.57 * Falls nicht 0 = N | | w >= N )61 throw new IllegalArgumentException( "Knoten nicht vorhanden." );62 matrix[v][w] = true;63 }6465 /** Entfernt die Kante zwischen Knoten v <strong>und</strong> w, falls vorhanden.66 * Falls nicht 0


4.1. GERICHTETE GRAPHEN 15368 public void deleteEdge( int v, int w ) { deleteEdge69 if( v < 0 | | w < 0 | | v >= N | | w >= N )70 throw new IllegalArgumentException( "Knoten nicht vorhanden." );71 matrix[v][w] = false;72 }7374 /** Gibt den String s n-fach konkateniert zurueck. */75 private String conc( String s, int n ) { conc76 StringBuffer out = new StringBuffer( );77 for( int i = 0; i < n; i++ )78 out.append( s );79 return out.toString( );80 }8182 /** Gibt die Zahl i als String der Laenge n rechtsbuendig zurueck. */83 private String print( int i, int n ) { print84 return conc( " ", n − Integer.toString( i ).length( ) ) + i;85 }8687 /** Gibt eine String-Darstellung des Graphen zurueck. */88 public String toString( ) { toString89 StringBuffer out = new StringBuffer( );90 int n = Integer.toString( N ).length( ); // die Laenge der Darstellung von N91 // Die Spaltenueberschrift:92 out.append( conc( " ", n+2 ) + "|" );93 for( int v = 0; v < N; v++ )94 out.append( print( v, n+1 ) );95 out.append( "\n" + conc( "_", n+2 ) + "|" + conc( "_", N*(n+1) ) + "\n" );96 // Die Matrix:97 for( int v = 0; v < N; v++ ) {98 out.append( print( v, n+1 ) + " |" );99 for( int w = 0; w < N; w++ )100 out.append( print( ( isEdge( v, w ) ? 1 : 0 ), n+1 ) );101 out.append( "\n" );102 }103 return out.toString( );104 }105106 } // class DirectedGraphViele Probleme für Graphen lassen sich leicht lösen, wenn die betrachtetenGraphen durch Adjazenzmatrizen dargestellt sind. Allerdings benötigt man fürdas Lesen der Eingabe dann schon O(n 2 ) Schritte, unabhängig davon, wievieleKanten der jeweilige Graph enthält. Für Graphen mit wenigen Kanten empfiehltsich daher eine andere Art der Speicherung.Definition 4.1.11. Implementierung von Graphen durch Adjazenzlisten: SeiG = (V, E) mit V = {v 0 , . . . , v n−1 } ein gerichteter Graph. Wir stellen nun dieZeilen der Adjazenzmatrix für G durch verkettete Listen dar. Für 0 ≤ i < n gibtes eine Liste, die für jede von v i ausgehende Kante einen Knoten enthält, welcher


154 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENdas Ziel der jeweiligen Kante angibt. Außerdem gibt es für jede dieser Listeneinen Kopfknoten der auf diese Liste verweist. Die Kopfknoten sind sequentiellangeordnet.Beispiel 4.1.12. (Fortsetzung von Beispiel 4.1.2) Die Adjazenzlisten für G 1 :01❜❛❜1✧ ✧✧0 2✧ ✧✧2✦ ✦✦✦✦✦Bemerkung 4.1.13. • Bei Graphen mit n Knoten <strong>und</strong> e Kanten entstehenn + e Listenknoten, der Platzbedarf ist daher O((n + e) · log n) Bits. Füre ≪ n 2 spart man mit der Darstellung durch Adjazenzlisten also Platz.• Für 0 ≤ i < n ist d − (v i ) die Länge der v i zugeordneten Liste. Die Bestimmungvon d + (v i ) ist wesentlich aufwändiger!• Um zu prüfen, ob die Kante (v i , v j ) im Graphen G enthalten ist, mussman die v i zugeordnete Liste durchsuchen: Zeitbedarf O(d − (v i )).• Die Repräsentation von Graphen durch Adjazenzlisten erlaubt auch Graphenmit Mehrfachkanten, ebenso allgemeinere Gewichtungsfunktionenc : E → R.Programm 4.1.14. Eine Implementierung gerichteter Graphen mit Gewichtsfunktionüber Adjazenzlisten:1 import java.util.LinkedList;2 import java.util.Iterator;3 import java.util.Random;45 /** Die Klasse WeightedDirectedGraph implementiert gerichtete Graphen mit6 * Gewichtungsfunktion ueber Adjazenzlisten. Knoten sind Zahlen >= 0 vom7 * Typ int, Gewichte sind Zahlen >= 0 vom Typ double.8 * Mehrfachkanten sind zugelassen (auch mit demselben Gewicht).9 */10 public class WeightedDirectedGraph {1112 /** Die Anzahl der Knoten des Graphen.13 * Knoten sind Zahlen v mit 0


4.1. GERICHTETE GRAPHEN 15524 public class Edge implements Comparable {2526 /** Der Zielknoten der Kante. */27 private int destination;2829 /** Das Gewicht der Kante. */30 private double weight;3132 /** Konstruiert eine Kante mit Zielknoten destination <strong>und</strong> Gewicht weight.33 * Falls nicht 0 = 0 gilt, wird eine34 * Ausnahme ausgeloest.35 */36 public Edge( int destination, double weight ) { Edge37 if( destination < 0 | | destination >= nodeSize( ) )38 throw new IllegalArgumentException( "Knoten nicht vorhanden." );39 if( weight < 0 )40 throw new IllegalArgumentException( "Gewicht ist negativ." );41 this.destination = destination;42 this.weight = weight;43 }4445 /** Kanten werden bezueglich ihres Gewichts verglichen. */46 public int compareTo( Object otherEdge ) { compareTo47 double otherWeight = ( (Edge)otherEdge ).weight;48 return weight < otherWeight ? −1 : weight > otherWeight ? +1 : 0;49 }5051 /** Gibt eine String-Darstellung der Kante zurueck. */52 public String toString( ) { toString53 return "(" + destination + ", " + weight + ")";54 }5556 } // class Edge5758 /** Die Adjazenzlisten des Graphen. Sie enthalten Objekte vom Typ Edge. */59 private LinkedList[ ] adjacencyList;6061 /** Konstruiert einen Graphen mit n Knoten <strong>und</strong> keiner Kante. */62 public WeightedDirectedGraph( int n ) { WeightedDirectedGraph63 N = n;64 adjacencyList = new LinkedList[N];65 for( int v = 0; v < N; v++ )66 adjacencyList[v] = new LinkedList( );67 }6869 /** Konstruiert einen Zufallsgraphen mit n Knoten ohne Mehrfachkanten.70 * Jede Kante ist mit Wahrscheinlichkeit p vorhanden. Das Gewicht einer71 * Kante ist als Zahl vom Typ int gleichverteilt aus [0, maxWeight) gewaehlt.72 * (Beachte, dass die entstehenden Adjazenzlisten bzgl. Knotennummern73 * aufsteigend sortiert sind.)74 */


156 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN75 public WeightedDirectedGraph( int n, double p, double maxWeight ) { WeightedDirectedGraph76 this( n );77 for( int v = 0; v < N; v++ )78 for( int w = 0; w < N; w++ )79 if( Math.random( ) < p )80 insertEdge( v, w, (int)( Math.random( ) * maxWeight ) );81 }8283 /** Gibt die Knotenzahl zurueck. */84 public int nodeSize( ) { nodeSize85 return N;86 }8788 /** Gibt die Kantenzahl zurueck. */89 public int edgeSize( ) { edgeSize90 int e = 0;91 for( int v = 0; v < N; v++ )92 e += adjacencyList[v].size( );93 return e;94 }9596 /** Fuegt eine Kante zwischen Knoten v <strong>und</strong> w mit Gewicht weight ein. Falls97 * nicht 0 = 0 gilt, wird eine Ausnahme ausgeloest.98 */99 public void insertEdge( int v, int w, double weight ) { insertEdge100 if( v < 0 | | v >= N )101 throw new IllegalArgumentException( "Knoten nicht vorhanden." );102 adjacencyList[v].add( new Edge( w, weight ) );103 }die Methoden conc <strong>und</strong> print wie in Programm 4.1.10118 /** Gibt eine String-Darstellung des Graphen zurueck. */119 public String toString( ) { toString120 StringBuffer out = new StringBuffer( );121 int n = Integer.toString( N ).length( ); // die Laenge der Darstellung von N122 for( int v = 0; v < N; v++ ) {123 out.append( print( v, n+1 ) + " : " );124 Iterator it = adjacencyList[v].iterator( );125 while( it.hasNext( ) )126 out.append( it.next( ) + " -> " );127 out.append( "null\n" );128 }129 return out.toString( );130 }131132 public static void main( String[ ] args ) { main133 WeightedDirectedGraph g = new WeightedDirectedGraph( 12, 0.2, 100 );134 System.out.println( "\nEin Zufallsgraph:\n\n" + g );135 }136137 } // class WeightedDirectedGraph


4.1. GERICHTETE GRAPHEN 157Ein Testlauf:Ein Zufallsgraph:0 : (1, 71.0) -> (7, 98.0) -> (10, 78.0) -> null1 : (6, 55.0) -> (7, 33.0) -> null2 : (0, 17.0) -> (10, 86.0) -> (11, 27.0) -> null3 : (3, 66.0) -> (7, 75.0) -> (10, 33.0) -> null4 : null5 : (1, 81.0) -> null6 : (2, 61.0) -> (8, 86.0) -> null7 : (2, 88.0) -> (9, 40.0) -> null8 : (1, 71.0) -> (2, 62.0) -> (3, 60.0) -> null9 : (2, 4.0) -> null10 : (6, 53.0) -> null11 : (1, 24.0) -> (5, 26.0) -> (8, 13.0) -> (9, 0.0) -> nullMuss man oft auf alle die Knoten zugreifen, von denen aus eine Kante zu einemfesten Knoten führt, so speichert man für jeden Knoten eine zusätzliche Liste.Definition 4.1.15. Implementierung von Graphen durch Adjazenzlisten <strong>und</strong>inverse Adjazenzlisten: Sei G = (V, E) mit V = {v 0 , . . . , v n−1 } ein gerichteterGraph. Außer den Adjazenzlisten für G nehmen wir nun auch noch verketteteListen zur Darstellung der Spalten der Adjazenzmatrix von G. Für 0 ≤ i < ngibt es eine Liste, die für jede in v i endende Kante einen Knoten enthält, welcherden Start der jeweiligen Kante angibt. Außerdem gibt es für jede dieser Listeneinen Kopfknoten, welche wieder sequentiell angeordnet werden.Beispiel 4.1.16. (Forts. Beispiel 4.1.2) Die inversen Adjazenzlisten für G 1 :0❜11❛❜0✧ ✧✧ ✟✟✟✧✧✧2❜1Stellt man einen gerichteten Graphen durch Adjazenzlisten <strong>und</strong> inverse Adjazenzlistendar, so werden für jede auftretende Kante zwei Listenknoten angelegt.Oftmals will man beim Durchsuchen eines Graphen alle bereits benutzten Kantenmarkieren, was bei dieser Art der Darstellung aufwändig ist. Um diesemProblem abzuhelfen, kann man beide Listenstrukturen mit Hilfe gemeinsamerKnoten als Multilisten speichern.Definition 4.1.17. Implementierung von Graphen durch Adjazenzmultilisten:Sei G = (V, E) mit V = {v 0 , . . . , v n−1 } ein gerichteter Graph. Für jeden Knotenv i implementieren wir die Adjazenzliste <strong>und</strong> die inverse Adjazenzliste wie folgt.Wieder gibt es für jede dieser Listen einen Kopfknoten. Für jede Kante (v i , v j )wird nun ein Knoten angelegt, der dann sowohl in der Adjazenzliste von v i


158 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENals auch in der inversen Adjazenzliste von v j auftritt. Dazu führen wir einenKnotentyp mit vier Feldern ein, je ein Feld für Start- <strong>und</strong> Zielknoten <strong>und</strong> je einFeld für den Verweis auf den in der jeweiligen Liste nächsten Knoten:start ziel slink zlinkBeispiel 4.1.18. (Forts. von Beispiel 4.1.2) Die Adjazenzmultilisten für G 1 :0 1 20❜❜ ❜❜✪✪✪✪ 0 1 ✧ ✑ ✑✑12❛❜✦ ✦✦✦✦✦1 0 ✧ ✧✧1 2✑ ✚ ✚✚Tragen die Knoten eines Graphen zusätzlich Information, so kann man dieseentweder in den Kopfknoten der Adjazenzlisten abspeichern, oder man führt inden Kopfknoten noch zusätzliche Verweise auf die gespeicherte Information ein.Tragen die Kanten eines Graphen zusätzliche Information (z.B. Gewichte), sokann man diese analog entweder in den Listenknoten der Adjazenzlisten speichern,oder man hält in den Knoten Verweise auf die gespeicherte Information.4.1.1 Traversieren von GraphenAls erstes befassen wir uns mit der folgenden Aufgabe: Sei G ein gerichteterGraph, <strong>und</strong> sei v ein Knoten. Bestimme die Menge aller Knoten, die von v auserreichbar sind!Ausgehend vom Knoten v müssen wir den Graph G systematisch durchlaufen,um alle erreichbaren Knoten zu bestimmen. Für diese Aufgabe werden wir zweiLösungen betrachten, Tiefensuche (engl. depth-first search) <strong>und</strong> Breitensuche(engl. breadth-first search).Algorithmus 4.1.19. Traversieren eines Graphen mittels Tiefensuche:Die Strategie ist, ausgehend vom Startknoten so tief wie möglich in den Grapheneinzudringen. Besuchte Knoten werden markiert. Hat ein Knoten u nochunmarkierte Nachbarn, so besuchen wir als nächstes einen dieser Nachbarn; hatu keine unmarkierten Nachbarn mehr, so gehen wir den bisher zurückgelegtenWeg soweit zurück, bis wir zu einem Knoten gelangen, der noch unmarkierteNachbarn hat.


4.1. GERICHTETE GRAPHEN 159Zur Vereinfachung wählen wir V = {0, . . . , n−1} <strong>und</strong> erweitern die Darstellunggerichteter Graphen aus Programm 4.1.10. Zur Markierung besuchter Knotenverwenden wir das Array besucht über boolean, wobei besucht[i] = true bedeutet,dass der i-te Knoten schon besucht worden ist.Zuerst stellen wir eine rekursive Methode vor, die die Liste der von Knoten vaus erreichbaren Knoten in der durch eine Tiefensuche erzeugten Reihenfolgezurückgibt.1 LinkedList depthFirstSearchRec( int v ) { depthFirstSearchRec2 return dfs( v, new boolean[N], new LinkedList( ) );3 }45 private LinkedList dfs( int v, boolean[ ] besucht, LinkedList knotenListe ) { dfs6 besucht[ v ] = true;7 knotenListe.add( new Integer( v ) );8 for( int w = 0; w < N; w++ )9 if( isEdge( v, w ) && !besucht[ w ] )10 dfs( w, besucht, knotenListe );11 return knotenListe;12 }Beispiel 4.1.20. Der folgende Graph hat die Knotenmenge {0, . . . , 7}. 0 1 2 3 4 5 6 7Diese Knoten werden in der Reihenfolge 0, 1, 3, 4, 7, 5, 2 besucht <strong>und</strong> markiert;ferner sehen wir, dass Knoten 6 von Knoten 0 aus nicht erreichbar ist. 0 1 2 3 4 7 5


160 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENBemerkung 4.1.21. Der Aufwand der Tiefensuche hängt von der Darstellungdes Graphen ab.• Darstellung durch eine Adjazenzmatrix: Zur Bestimmung der Nachbarnvon v muss die v-te Zeile der Matrix durchlaufen werden; insgesamt sindhöchstens n Aufrufe von dfs möglich. Der Algorithmus bestimmt also inZeit O(n 2 ) die Menge der vom Startknoten aus erreichbaren Knoten.• Darstellung durch Adjazenzlisten: Zur Bestimmung der Nachbarn von vmuss die Adjazenzliste zu v durchlaufen werden, d.h. es werden alle eKanten angeschaut. Insgesamt sind min(n, e) Aufrufe von dfs möglich, derAlgorithmus bestimmt also in Zeit O(e) die Menge der vom Startknotenaus erreichbaren Knoten.Die folgende Variante der Tiefensuche ist nicht rekursiv <strong>und</strong> verwendet einenKeller zur Organisation der Suche. Man beachte, dass das Ergebnis wegen derunterschiedliche Markierungsstrategien vom Ergebnis der Methode depthFirst-SearchRec abweichen kann, vgl. den Testlauf zu Programm 4.1.26.1 LinkedList depthFirstSearch( int v ) { depthFirstSearch2 boolean[ ] besucht = new boolean[N];3 LinkedList knotenListe = new LinkedList( );4 Stack knotenKeller = new Stack( );5 knotenKeller.push( new Integer( v ) );6 besucht[ v ] = true;7 while( !knotenKeller.isEmpty( ) ) {8 int u = ( (Integer)knotenKeller.topAndPop( ) ).intValue( );9 knotenListe.add( new Integer( u ) );10 for( int w = N−1; w >= 0; w−− )11 if( isEdge( u, w ) && !besucht[ w ] ) {12 knotenKeller.push( new Integer( w ) );13 besucht[ w ] = true;14 }15 }16 return knotenListe;17 }Beispiel 4.1.22. (Fortsetzung von Beispiel 4.1.20) Die Knoten werden in diesemBeispiel tatsächlich in derselben Reihenfolge wie zuvor besucht:aktueller Knoten u als besucht markiert der Keller– {0} (0)0 {0, 1, 2} (1, 2)1 {0, 1, 2, 3, 4} (3, 4, 2)3 {0, 1, 2, 3, 4} (4, 2)4 {0, 1, 2, 3, 4, 7} (7, 2)7 {0, 1, 2, 3, 4, 7, 5} (5, 2)5 {0, 1, 2, 3, 4, 7, 5} (2)2 {0, 1, 2, 3, 4, 7, 5} ( )


4.1. GERICHTETE GRAPHEN 161Algorithmus 4.1.23. Traversieren eines Graphen mittels Breitensuche:Hier ist die Strategie, ausgehend vom Startknoten für jeden besuchten Knotenals nächstes alle seine Nachbarn zu besuchen. Besuchte Knoten werden wiedermarkiert. Ist u der aktuelle Knoten, so werden alle Nachbarn von u, die nochnicht besucht worden sind, in eine Schlange aufgenommen. Als nächstes wirddann der Knoten besucht, der an der Spitze der Schlange steht; dabei wirddieser aus der Schlange entfernt.Die Methode breadthFirstSearch ist nicht rekursiv <strong>und</strong> verwendet eine Schlangezur Organisation der Suche. Sie gibt die Liste der von Knoten v aus erreichbarenKnoten in der durch eine Breitensuche erzeugten Reihenfolge zurück.1 LinkedList breadthFirstSearch( int v ) { breadthFirstSearch2 boolean[ ] besucht = new boolean[N];3 LinkedList knotenListe = new LinkedList( );4 Queue knotenSchlange = new Queue( );5 knotenSchlange.enqueue( new Integer( v ) );6 besucht[ v ] = true;7 while( !knotenSchlange.isEmpty( ) ) {8 int u = ( (Integer)knotenSchlange.frontAndDequeue( ) ).intValue( );9 knotenListe.add( new Integer( u ) );10 for( int w = 0; w < N; w++ )11 if( isEdge( u, w ) && !besucht[ w ] ) {12 knotenSchlange.enqueue( new Integer( w ) );13 besucht[ w ] = true;14 }15 }16 return knotenListe;17 }Bemerkung 4.1.24. Der Aufwand der Breitensuche hängt wieder von derDarstellung des Graphen ab. Jeder Knoten wird höchstens einmal in die Schlangeaufgenommen. Wird ein Knoten entfernt, so werden alle seine Nachbarnüberprüft <strong>und</strong> dann gegebenenfalls in die Schlange aufgenommen. Die Mengeder erreichbaren Knoten kann also wie zuvor in Zeit O(n 2 ) bzw. Zeit O(e)bestimmt werden, je nachdem, ob Adjazenzmatrizen oder Adjazenzlisten verwendetwerden.Beispiel 4.1.25. (Fortsetzung von Beispiel 4.1.20) Im Beispiel werden die Knotenin der Reihenfolge 0, 1, 2, 3, 4, 5, 7 besucht:aktueller Knoten u als besucht markiert die Schlange– {0} (0)0 {0, 1, 2} (1, 2)1 {0, 1, 2, 3, 4} (2, 3, 4)2 {0, 1, 2, 3, 4, 5} (3, 4, 5)3 {0, 1, 2, 3, 4, 5} (4, 5)4 {0, 1, 2, 3, 4, 5, 7} (5, 7)5 {0, 1, 2, 3, 4, 5, 7} (7)7 {0, 1, 2, 3, 4, 5, 7} ( )


162 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENProgramm 4.1.26. Eine Implementierung obiger Traversierungsstrategien:wie in Programm 4.1.1075 /** Gibt die Liste der von Knoten v aus erreichbaren Knoten76 * in der durch eine Tiefensuche erzeugten Reihenfolge zurueck.77 * Falls nicht 0 = N )81 throw new IllegalArgumentException( "Knoten nicht vorhanden." );82 return dfs( v, new boolean[N], new LinkedList( ) );83 }8485 /** Eine rekursive Hilfsmethode fuer die Methode depthFirstSearchRec. */86 private LinkedList dfs( int v, boolean[ ] besucht, LinkedList knotenListe ) { dfs87 besucht[ v ] = true;88 knotenListe.add( new Integer( v ) );89 for( int w = 0; w < N; w++ )90 if( isEdge( v, w ) && !besucht[ w ] )91 dfs( w, besucht, knotenListe );92 return knotenListe;93 }9495 /** Gibt die Liste der von Knoten v aus erreichbaren Knoten mit96 * aufsteigenden Knotennummern zurueck.97 * Falls nicht 0 = N )101 throw new IllegalArgumentException( "Knoten nicht vorhanden." );102 boolean[ ] besucht = dfs( v, new boolean[N] );103 LinkedList knotenListe = new LinkedList( );104 for( int i = 0; i < N; i++ )105 if( besucht[i] )106 knotenListe.add( new Integer( i ) );107 return knotenListe;108 }109110 /** Eine rekursive Hilfsmethode fuer die Methode accessibleNodes. */111 private boolean[ ] dfs( int v, boolean[ ] besucht ) { dfs112 besucht[ v ] = true;113 for( int w = 0; w < N; w++ )114 if( isEdge( v, w ) && !besucht[ w ] )115 dfs( w, besucht );116 return besucht;117 }118119 /** Gibt die Liste der von Knoten v aus erreichbaren Knoten120 * in der durch eine Tiefensuche erzeugten Reihenfolge zurueck.121 * Falls nicht 0


4.1. GERICHTETE GRAPHEN 163127 public LinkedList depthFirstSearch( int v ) { depthFirstSearch128 if( v < 0 | | v >= N )129 throw new IllegalArgumentException( "Knoten nicht vorhanden." );130 boolean[ ] besucht = new boolean[N];131 LinkedList knotenListe = new LinkedList( );132 Stack knotenKeller = new Stack( );133 knotenKeller.push( new Integer( v ) );134 besucht[ v ] = true;135 while( !knotenKeller.isEmpty( ) ) {136 int u = ( (Integer)knotenKeller.topAndPop( ) ).intValue( );137 knotenListe.add( new Integer( u ) );138 for( int w = N−1; w >= 0; w−− )139 if( isEdge( u, w ) && !besucht[ w ] ) {140 knotenKeller.push( new Integer( w ) );141 besucht[ w ] = true;142 }143 }144 return knotenListe;145 }146147 /** Gibt die Liste der von Knoten v aus erreichbaren Knoten148 * in der durch eine Breitensuche erzeugten Reihenfolge zurueck.149 * Falls nicht 0 = N )155 throw new IllegalArgumentException( "Knoten nicht vorhanden." );156 boolean[ ] besucht = new boolean[N];157 LinkedList knotenListe = new LinkedList( );158 Queue knotenSchlange = new Queue( );159 knotenSchlange.enqueue( new Integer( v ) );160 besucht[ v ] = true;161 while( !knotenSchlange.isEmpty( ) ) {162 int u = ( (Integer)knotenSchlange.frontAndDequeue( ) ).intValue( );163 knotenListe.add( new Integer( u ) );164 for( int w = 0; w < N; w++ )165 if( isEdge( u, w ) && !besucht[ w ] ) {166 knotenSchlange.enqueue( new Integer( w ) );167 besucht[ w ] = true;168 }169 }170 return knotenListe;171 }die Methoden conc, print <strong>und</strong> toString wie in Programm 4.1.10205 public static void main( String[ ] args ) { main206 DirectedGraph b = new DirectedGraph( 8 );207 b.insertEdge( 0, 1 ); b.insertEdge( 0, 2 ); b.insertEdge( 1, 3 );208 b.insertEdge( 1, 4 ); b.insertEdge( 2, 5 ); b.insertEdge( 4, 7 );209 b.insertEdge( 6, 2 ); b.insertEdge( 6, 7 ); b.insertEdge( 7, 3 );210 b.insertEdge( 7, 5 );


164 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN211 System.out.println( "Der Graph aus Beispiel 4.1.20:\n\n" + b +212 "\nVon Knoten 0 aus erreichbare Knoten:" +213 "\nTiefensuche1: " + b.depthFirstSearchRec( 0 ) +214 "\nTiefensuche2: " + b.depthFirstSearch( 0 ) +215 "\nBreitensuche: " + b.breadthFirstSearch( 0 ) +216 "\nKnotenliste: " + b.accessibleNodes( 0 ) );217218 DirectedGraph g = new DirectedGraph( 5, 0.2 );219 System.out.println( "\nEin Zufallsgraph:\n\n" + g +220 "\nVon Knoten 0 aus erreichbare Knoten:" +221 "\nTiefensuche1: " + g.depthFirstSearchRec( 0 ) +222 "\nTiefensuche2: " + g.depthFirstSearch( 0 ) +223 "\nBreitensuche: " + g.breadthFirstSearch( 0 ) +224 "\nKnotenliste: " + g.accessibleNodes( 0 ) );225 }226227 } // class DirectedGraphEin Testlauf:Der Graph aus Beispiel 4.1.20:| 0 1 2 3 4 5 6 7___|________________0 | 0 1 1 0 0 0 0 01 | 0 0 0 1 1 0 0 02 | 0 0 0 0 0 1 0 03 | 0 0 0 0 0 0 0 04 | 0 0 0 0 0 0 0 15 | 0 0 0 0 0 0 0 06 | 0 0 1 0 0 0 0 17 | 0 0 0 1 0 1 0 0Von Knoten 0 aus erreichbare Knoten:Tiefensuche1: [0, 1, 3, 4, 7, 5, 2]Tiefensuche2: [0, 1, 3, 4, 7, 5, 2]Breitensuche: [0, 1, 2, 3, 4, 5, 7]Knotenliste: [0, 1, 2, 3, 4, 5, 7]Ein Zufallsgraph:| 0 1 2 3 4___|__________0 | 0 1 0 1 01 | 0 0 1 0 12 | 0 0 0 1 03 | 0 0 0 0 04 | 0 0 0 0 0Von Knoten 0 aus erreichbare Knoten:Tiefensuche1: [0, 1, 2, 3, 4]Tiefensuche2: [0, 1, 2, 4, 3]Breitensuche: [0, 1, 3, 2, 4]Knotenliste: [0, 1, 2, 3, 4]


4.1. GERICHTETE GRAPHEN 1654.1.2 Kürzeste WegeHier wollen wir uns dem Problem zuwenden, ”kürzeste Wege“ in einem gewichtetenGraphen zu bestimmen.Definition 4.1.27. Sei G = (V, E) ein gerichteter Graph, <strong>und</strong> sei c : E → R +eine Gewichtungsfunktion. Wir erweitern die Gewichtungsfunktion von Kantenauf Wege p = (u 0 , . . . , u r ) durch c(p) = ∑ r−1i=0 c((u i, u i+1 )). Ferner zeichnen wireinen Knoten v 0 ∈ V als Startknoten aus. Für v ∈ V sei dann die Distanz zuv 0 der Wertdist(v) = min{c(p) | p ist ein Weg von v 0 nach v},falls es einen Weg von v 0 nach v gibt; andernfalls setzen wir dist(v) = ∞. EinWeg p von v 0 nach v mit c(p) = dist(v) ist ein kürzester Weg von v 0 nach v.Wir suchen nun einen Algorithmus, der für alle Knoten v den Wert dist(v)bestimmt, <strong>und</strong> der für Knoten v mit dist(v) < ∞ einen kürzesten Weg von v 0nach v liefert.Bemerkung 4.1.28. Ist c(e) = 1 für alle e ∈ E, so ist dist(v) gerade die Anzahlder Kanten in einem kürzesten Weg von v 0 nach v. Da der AlgorithmusbreadthFirstSearch ausgehend von v 0 die Knoten von G gerade in der Reihenfolgeihres Abstands von v 0 besucht, können wir in diesem Fall eine Variantedes genannten Algorithmus zur Bestimmung der Abstände benutzen.Sei nun G = (V, E), c : E → R + <strong>und</strong> v 0 ∈ V wie oben. Wir definieren TeilmengenS, T ⊆ V , die in unserem Algorithmus zur Bestimmung der Abständeverwendet werden:S = {v ∈ V | dist(v) ist bereits bestimmt},T = {w ∈ V \ S | ∃v ∈ S : (v, w) ∈ E}.Am Anfang wird S = {v 0 } gesetzt, <strong>und</strong> T besteht gerade aus den Zielknotenaller Kanten mit Start v 0 . Seien nun zu einem Zeitpunkt S <strong>und</strong> T wie oben.Man wählt nun einen Knoten w ∈ T so aus, dass folgende Bedingung für alleKnoten z ∈ T erfüllt ist:minv∈S{dist(v) + c((v, w))} ≤ min{dist(v) + c((v, z))}.Dann kann w in S aufgenommen <strong>und</strong> aus T entfernt werden, <strong>und</strong> alle Zielknotenvon Kanten mit Start w, die nicht in S liegen, werden in T aufgenommen. DieKorrektheit dieser Wahl ist der Gegenstand des folgenden Lemmas.v∈SLemma 4.1.29. Mit den obigen Bezeichnungen giltdist(w) = min{dist(v) + c((v, w))}.v∈S


166 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENBeweis: Ist v ∈ S, so gibt es einen kürzesten Weg von v 0 nach v, der nurKnoten in S durchläuft. Sei nun v ∈ S mitdist(v) + c((v, w)) = min{dist(x) + c((x, w))},x∈S<strong>und</strong> sei (v 0 , v 1 , . . . , v m , v) ein kürzester Weg von v 0 nach v mit v 1 , . . . , v m ∈ S.Wir behaupten, dass (v 0 , v 1 , . . . , v m , v, w) ein kürzester Weg von v 0 nach w ist.Sei etwa (v 0 , x 1 , . . . , x k , w) ein weiterer Weg von v 0 nach w. Da v 0 ∈ S <strong>und</strong>w ∉ S sind, gibt es einen kleinsten Index i, 1 ≤ i ≤ k + 1, mit x i−1 ∈ S <strong>und</strong>x i ∉ S, wobei wir x 0 = v 0 <strong>und</strong> x k+1 = w setzen. Dann ist x i ∈ T , <strong>und</strong> es giltk∑c((x j , x j+1 )) = dist(x i−1 ) + c((x i−1 , x i )) +j=0≥ dist(x i−1 ) + c((x i−1 , x i ))≥ dist(v) + c((v, w))k∑c((x j , x j+1 ))j=i(nach Wahl von w).Also ist (v 0 , v 1 , . . . , v m , v, w) tatsächlich ein kürzester Weg von v 0 nach w, d.h.es gilt dist(w) = min{dist(v) + c((v, w))}.v∈SDieses Ergebnis liefert die Strategie für den folgenden Algorithmus. In jedemSchritt wird ein Knoten w wie beschrieben gewählt. Zur Bestimmung eines derartigenKnotens mit minimaler Distanz speichern wir Knoten zusammen mit ihremvorläufigen Distanzwert in einem Heap. Solche Paare werden bezüglich derDistanz geordnet; dafür eignet sich gerade der Typ Edge aus Programm 4.1.14.Zusätzlich wird für jeden Knoten w der aktuell bestimmte Wert dist(w) gespeichert,sowie der Knoten v = vorgänger(w) als Vorgänger von w auf dem aktuellgef<strong>und</strong>enen kürzesten Weg von v 0 nach w. Zu Beginn setzen wir vorgänger(v) =−1 <strong>und</strong> dist(v) = ∞ für alle Knoten v. Die Menge T ist nur indirekt dargestellt:w ∈ V \ S gehört genau dann zu T , wenn dist(w) < ∞ ist.Immer wenn sich der Wert von dist(w) verringert, wird das Paar (w, dist(w)) inden Heap eingefügt. Das hat zur Folge, dass der Heap mehrere Paare mit derselbenKnotenkomponente enthalten kann. Daher sind nicht immer alle Knotenkomponentenim Heap auch Elemente von T . (Alternativ kann man Heaps somodifizieren, dass nur Knoten statt derartige Paare abgelegt werden. Da abernach Distanzen geordnet wird, muss dann jedesmal bei Verringerung eines Distanzwertsder entsprechende Knoten wieder an die richtige Stelle im Heapgebracht werden.)Dijkstras Algorithmus ist ein typisches Beispiel für einen ”Greedy“-Algorithmus(greedy: engl. gierig, gefräßig). Solche <strong>Algorithmen</strong> werden meist zur Lösungvon Optimierungsproblemen eingesetzt, also zum Finden einer bezüglich einerZielfunktion optimalen (minimalen, maximalen, . . . ) Lösung des gegebenenProblems. Ein Greedy-Algorithmus nähert sich schrittweise einer Lösung, wobeiin jedem einzelnen Schritt die Zielfunktion optimiert wird; ein Zurücknehmenfrüherer Schritte (backtracking) ist nicht vorgesehen. Leider lassen sich mitdieser Lösungsstrategie nur wenige Optimierungsprobleme lösen; in anderenFällen kommt man nicht umhin, viele mögliche Lösungen mehr oder wenigergeschickt durchzuprobieren.


4.1. GERICHTETE GRAPHEN 167Algorithmus 4.1.30. Dijkstras Algorithmus zur Bestimmung kürzester Wege:1 shortestPaths( Knoten start ) { shortestPaths2 for all( v ∈ V ) {3 vorgänger(v) = −1;4 dist(v) = ∞;5 }6 dist(start) = 0;7 Heap heap = new Heap( |E| );8 heap.einfuegenInHeap( (start, 0) );910 while( !heap.istLeer( ) ) {11 Knoten v = heap.loeschenAusHeap( ).zielknoten;12 if( v ∉ S ) {13 S = S ∪ {v};14 for all( (v, w) ∈ E ) {15 if( dist(w) > dist(v) + c((v, w)) ) {16 dist(w) = dist(v) + c((v, w));17 vorgänger(w) = v;18 heap.einfuegenInHeap( (w, dist(w)) );19 }20 }21 }22 }23 }Beispiel 4.1.31. Gesucht sind kürzeste Wege zwischen Städten der USA:ChicagoBoston✓✏✗✔ ✓✏1500✓✏ S.F.Denver 12003 4✓✏✒✑ ❛ ✖✕ ✒✑800❛❛❛❛❛❛❛❛❛❛1 2 ✥✥✥✥✥✥✥✥✥✥1000✁✒✑ ✧✒✑✁ 250✧✓✏✧✁300 ✧✧ 10005 N.Y.✒✑✓✏✧✧✧✔01400✔✒✑1700✔L.A.✔ 900✓✏✔❳❳❳❳❳❳❳❳❳❳❳❳❳❳❳ ✦ ✦✦✦✦✦✦✦✦✦✦✦✦✦✦7✔✒✑ 1000 ✓✏ ✔New Orleans❤❤❤❤❤❤❤❤❤❤ 6✒✑Miamiausgew.Knoten S dist(0) dist(1) dist(2) dist(3) dist(4) dist(5) dist(6) dist(7){} ∞ ∞ ∞ ∞ 0 ∞ ∞ ∞4 {4} ∞ ∞ ∞ 1500 0 250 ∞ ∞5 {4, 5} ∞ ∞ ∞ 1250 0 250 1150 16506 {4, 5, 6} ∞ ∞ ∞ 1250 0 250 1150 16503 {4, 5, 6, 3} ∞ ∞ 2450 1250 0 250 1150 16507 {4, 5, 6, 3, 7} 3350 ∞ 2450 1250 0 250 1150 16502 {4, 5, 6, 3, 7, 2} 3350 3250 2450 1250 0 250 1150 16501 {4, 5, 6, 3, 7, 2, 1} 3350 3250 2450 1250 0 250 1150 16500 {4, 5, 6, 3, 7, 2, 1, 0} 3350 3250 2450 1250 0 250 1150 1650


168 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENDer Aufwand von Dijkstras Algorithmus hängt wieder von der Darstellung derGraphen ab.• Sind Graphen durch Adjazenzmatrizen implementiert, dann liefert eineeinfache Implementierung ohne Verwendung der Datenstrukur Heap eineLaufzeit von O(n 2 ).• Für dünne“ Graphen mit wenigen Kanten (|E| ≪ |V | 2 ) kann der Zeitbedarfnoch verringert werden. Dafür verwenden wir die Graphdarstellung”durch Adjazenzlisten. Außerdem speichern wir die Paare (w, dist(w)) wieoben beschrieben in einem Heap. Der Heap kann maximal die Größe |E|haben, <strong>und</strong> es werden maximal |E| Einfüge- bzw. Löschoperationen durchgeführt.Daher ist die Laufzeit in O(|E| log |E|); wegen |E| ≤ |V | 2 giltaber log |E| ≤ log |V | 2 = 2 log |V |, also erhalten wir eine Laufzeit inO(|E| log |V |). (Bemerkung: Damit ist die Laufzeit höchstens um einenkonstanten Faktor schlechter als bei Verwendung eines Heaps, der lediglichKnoten speichert, also maximal die Größe |V | hat.)Programm 4.1.32. Die folgende Implementierung von Dijkstras Algorithmuserweitert Programm 4.1.14, das gewichtete Graphen über Adjazenzlisten darstellt.Der Wert ∞ wird dabei durch double.MAX VALUE repräsentiert. DieFunktionen vorgänger <strong>und</strong> dist werden in den Arrays vorgaenger <strong>und</strong> distanzgespeichert. Dijkstras Algorithmus findet sich im Konstruktor der inneren KlasseShortestPaths; dort ist die Knotenmenge S durch ein Boolesches Array inSrealisiert. Heaps schließlich sind aus Programm 2.3.14 übernommen.wie in Programm 4.1.14105 /** Die Klasse implementiert die kuerzesten Wege im Graphen106 * von einem Startknoten aus.107 */108 public class ShortestPaths {109110 /** Der Startknoten. */111 private int start;112113 /** Nach der Konstruktion ist vorgaenger[v] der Knoten, der auf den114 * gef<strong>und</strong>enen kuerzesten Wegen vor Knoten v liegt. Fuer den Startknoten115 * <strong>und</strong> fuer unerreichbare Knoten ist der Wert -1.116 */117 private int[ ] vorgaenger = new int[ nodeSize( ) ]; nodeSize118119 /** Nach der Konstruktion ist distanz[v] der Abstand von Knoten v120 * zum Startknoten.121 */122 private double[ ] distanz = new double[ nodeSize( ) ];123124 /** Dijkstras Algorithmus bestimmt die kuerzesten Wege125 * vom Startknoten start aus.126 */


4.1. GERICHTETE GRAPHEN 169127 public ShortestPaths( int start ) {128 if( start < 0 | | start >= nodeSize( ) )129 throw new IllegalArgumentException( "Startknoten nicht vorhanden." );130 this.start = start;131 for( int v = 0; v < nodeSize( ); v++ ) {132 vorgaenger[v] = −1;133 distanz[v] = INFINITE;134 }135 // inS[v] ist true genau dann, wenn v in S ist; zu Beginn ist S leer.136 boolean[ ] inS = new boolean[ nodeSize( ) ];137 distanz[ start ] = 0;138139 VollstaendigerBinaererBaum heap =140 new VollstaendigerBinaererBaum( edgeSize( ) );141 heap.einfuegenInHeap( new Edge( start, 0 ) );142143 while( !heap.istLeer( ) ) {144 int v = ( (Edge)heap.loeschenAusHeap( ) ).destination;145 if( !inS[v] ) { // falls v nicht in S ist:146 inS[v] = true; // v wird neues Element in S147 Iterator it = adjacencyList[v].iterator( );148 while( it.hasNext( ) ) {149 Edge e = (Edge)it.next( );150 int w = e.destination;151 double c = e.weight;152 if( distanz[w] > distanz[v] + c ) {153 distanz[w] = distanz[v] + c;154 vorgaenger[w] = v;155 heap.einfuegenInHeap( new Edge( w, distanz[w] ) );156 }157 }158 }159 }160 }161162 /** Gibt eine String-Darstellung der kuerzesten Wege163 * mit Startknoten start zurueck.164 */165 public String toString( ) { toString166 StringBuffer out = new StringBuffer( );167 out.append( "Distanzen zum Startknoten " + start + ":\n\n" );168 for( int v = 0; v < nodeSize( ); v++ )169 if( distanz[v] == INFINITE )170 out.append( " " + v + " ist nicht erreichbar\n" );171 else172 out.append( " dist(" + v + ") = " + distanz[v] + "\n" );173 out.append( "\nKuerzeste Wege mit Startknoten " + start + ":\n\n" );174 for( int v = 0; v < nodeSize( ); v++ ) {175 Stack path = new Stack( );176 int w = v;177 do {178 path.push( new Integer( w ) );179 w = vorgaenger[w];180 }181 while( w != −1 );


170 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN182 if( distanz[v] == INFINITE )183 out.append( " Ziel " + v + ": ist nicht erreichbar\n" );184 else185 out.append( " Ziel " + v + ": " + path + "\n" );186 }187 return out.toString( );188 }189190 } // class ShortestPathsdie Methoden conc, print <strong>und</strong> toString wie in Programm 4.1.14220 public static void main( String[ ] args ) { main221 WeightedDirectedGraph usa = new WeightedDirectedGraph( 8 );222 usa.insertEdge( 1, 0, 300 ); usa.insertEdge( 2, 1, 800 );223 usa.insertEdge( 2, 0, 1000 ); usa.insertEdge( 3, 2, 1200 );224 usa.insertEdge( 4, 3, 1500 ); usa.insertEdge( 4, 5, 250 );225 usa.insertEdge( 5, 3, 1000 ); usa.insertEdge( 5, 6, 900 );226 usa.insertEdge( 5, 7, 1400 ); usa.insertEdge( 6, 7, 1000 );227 usa.insertEdge( 7, 0, 1700 );228 System.out.println( "Der Graph aus Beispiel 4.1.31:\n\n" +229 usa + "\n" + usa.new ShortestPaths( 4 ) );230231 WeightedDirectedGraph g = new WeightedDirectedGraph( 10, 0.2, 100 );232 System.out.println( "\nEin Zufallsgraph:\n\n" +233 g + "\n" + g.new ShortestPaths( 0 ) );234 }235236 } // class WeightedDirectedGraphEin Testlauf:Der Graph aus Beispiel 4.1.31:0 : null1 : (0, 300.0) -> null2 : (1, 800.0) -> (0, 1000.0) -> null3 : (2, 1200.0) -> null4 : (3, 1500.0) -> (5, 250.0) -> null5 : (3, 1000.0) -> (6, 900.0) -> (7, 1400.0) -> null6 : (7, 1000.0) -> null7 : (0, 1700.0) -> nullDistanzen zum Startknoten 4:dist(0) = 3350.0dist(1) = 3250.0dist(2) = 2450.0dist(3) = 1250.0dist(4) = 0.0dist(5) = 250.0dist(6) = 1150.0dist(7) = 1650.0


4.1. GERICHTETE GRAPHEN 171Kuerzeste Wege mit Startknoten 4:Ziel 0: [4, 5, 7, 0]Ziel 1: [4, 5, 3, 2, 1]Ziel 2: [4, 5, 3, 2]Ziel 3: [4, 5, 3]Ziel 4: [4]Ziel 5: [4, 5]Ziel 6: [4, 5, 6]Ziel 7: [4, 5, 7]Ein Zufallsgraph:0 : (2, 6.0) -> (3, 31.0) -> (4, 15.0) -> null1 : (0, 23.0) -> (5, 71.0) -> (7, 23.0) -> null2 : (8, 87.0) -> null3 : (7, 38.0) -> null4 : (3, 73.0) -> (4, 31.0) -> (8, 5.0) -> null5 : (2, 56.0) -> (7, 81.0) -> null6 : (1, 20.0) -> null7 : (9, 55.0) -> null8 : (9, 91.0) -> null9 : (3, 74.0) -> nullDistanzen zum Startknoten 0:dist(0) = 0.01 ist nicht erreichbardist(2) = 6.0dist(3) = 31.0dist(4) = 15.05 ist nicht erreichbar6 ist nicht erreichbardist(7) = 69.0dist(8) = 20.0dist(9) = 111.0Kuerzeste Wege mit Startknoten 0:Ziel 0: [0]Ziel 1: ist nicht erreichbarZiel 2: [0, 2]Ziel 3: [0, 3]Ziel 4: [0, 4]Ziel 5: ist nicht erreichbarZiel 6: ist nicht erreichbarZiel 7: [0, 3, 7]Ziel 8: [0, 4, 8]Ziel 9: [0, 4, 8, 9]


172 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN4.1.3 Starke KomponentenSchließlich wollen wir noch das Problem lösen, die starken Komponenten einesgerichteten Graphen zu bestimmen. Zur Erinnerung: Eine starke Komponenteeines gerichteten Graphen ist ein Teilgraph mit maximaler Knotenzahl, in demalle Knoten paarweise stark verb<strong>und</strong>en sind, d.h. sind u <strong>und</strong> v zwei beliebigeKnoten aus diesem Teilgraphen, dann gibt es einen Weg von u nach v <strong>und</strong> einenWeg von v nach u.Algorithmus 4.1.33. Ein Algorithmus zur Bestimmung der starken Komponenteneines gerichteten Graphen G:(1.) Führe Tiefensuche auf G durch. Dabei werden die Knoten in der Reihenfolgedurchnummeriert, in der die rekursiven Aufrufe von dfs beendetwerden. Werden nicht alle Knoten erreicht, so erfolgen weitere Aufrufe solange, bis jeder Knoten erreicht <strong>und</strong> damit nummeriert worden ist.(2.) Der Graph G −1 entstehe aus G, indem jede Kante (u, v) durch die Kante(v, u) ersetzt wird. Führe Tiefensuche auf G −1 durch. Dabei wird jeweilsmit dem Knoten begonnen, der unter allen noch nicht besuchten Knotendie höchste Nummer aus (1.) hat. Ist eine Erreichbarkeitskomponente inG −1 gef<strong>und</strong>en, dann werden ihre Knoten aus G −1 entfernt. Diese Knotenbilden gerade eine starke Komponente von G.Die Tiefensuche aus Algorithmus 4.1.19 ist also das entscheidende Hilfsmittel,um die starken Komponenten von G zu bestimmen. Ehe wir uns überlegen, dassobiger Algorithmus korrekt ist, schauen wir uns ein einfaches Beispiel an.Beispiel 4.1.34. Sei G der folgende Graph: A C E B D F G H I J K(1.) Tiefensuche auf G ergibt die folgenden Teilgraphen <strong>und</strong> die folgende Knotennummerierung:A : 5 B : 3 C : 4 E : 1 G : 2D : 11H : 10 F : 6 I : 9J : 8K : 7


4.1. GERICHTETE GRAPHEN 173(2.) Der inverse Graph G −1 : A C E B D F G H I J KTiefensuche auf G −1 :• Startknoten D :DFHd.h. die erste starke Komponente von G ist {D, F, H}.• Startknoten I : Id.h. die zweite starke Komponente von G ist {I}.• Startknoten J :JKd.h. die dritte starke Komponente von G ist {J, K}.• Startknoten A :AG B Cd.h. die vierte starke Komponente von G ist {A, B, C, G}.• Startknoten E : Ed.h. die fünfte starke Komponente von G ist {E}.Damit hat G die folgenden starken Komponenten: A C E B D F G H I J KLemma 4.1.35. Sei G = (V, E) ein gerichteter Graph mit |V | = n, <strong>und</strong> sei µ :V → {1, . . . , n} die in Algorithmus 4.1.33 (1.) erzeugte Knotennummerierung.Dann gilt: Zwei Knoten liegen in derselben starken Komponente von G genaudann, wenn sie in derselben Erreichbarkeitskomponente von G −1 liegen.


174 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENBeweis: Seien v, w ∈ V , v ≠ w.⇒“: Liegen v <strong>und</strong> w in derselben starken Komponente von G, so gibt es”Wege von v nach w <strong>und</strong> von w nach v in G, <strong>und</strong> damit auch in G −1 . Bei derTiefensuche auf G −1 werde der Knoten v (o.B.d.A.) vor dem Knoten w besucht.Da es in G −1 einen Weg von v nach w gibt, wird w beim Aufruf dfs(v) in G −1erreicht, d.h. v <strong>und</strong> w liegen in derselben Erreichbarkeitskomponente von G −1 .⇐“: Sei x der Knoten, der als Startknoten diente, als beim Durchlaufen von”G −1 die Komponente erzeugt wurde, die v <strong>und</strong> w enthält. Wir behaupten, dasses in G Wege von x nach v <strong>und</strong> von v nach x sowie Wege von x nach w <strong>und</strong> von wnach x gibt, woraus dann folgt, dass v <strong>und</strong> w in derselben starken Komponentevon G liegen. Natürlich reicht es, den Beweis für den Knoten v zu führen, wobeiwir v ≠ x annehmen.Weil v in der Erreichbarkeitskomponente von x in G −1 liegt, gibt es in G −1einen Weg von x nach v, somit gibt es in G einen Weg von v nach x. Weiterhingilt µ(x) > µ(v), denn v war beim Aufruf von dfs(x) in (2.) noch nicht besucht.Also terminiert dfs(v) in Schritt (1.) vor dfs(x). Für die zeitliche Abfolge derAufrufe <strong>und</strong> ihrer Beendigungen von dfs in (1.) gibt es damit drei Möglichkeiten:(a) dfs(v) ist beendet, ehe dfs(x) aufgerufen wird:v ¯v x ¯x↑ ↑Terminierung von dfs(v)Aufruf von dfs(v)Weil es in G einen Weg von v nach x gibt, wird beim Aufruf von dfs(v)in (1.) also dfs(x) nur dann nicht aufgerufen, wenn dieser Weg über einenZwischenknoten z führt, der vor v besucht wurde, d.h. wir haben diefolgende Situation: v z xDann gilt aber, dass dfs(z) sowohl dfs(v) als auch dfs(x) umfasst:z¯zv ¯v x ¯xDamit ist µ(z) > µ(x), <strong>und</strong> in (2.) wird dfs(z) vor dfs(x) aufgerufen. DieserAufruf würde aber bereits den Knoten v erreichen, so dass v nicht erst inder Erreichbarkeitskomponente von x erreicht würde. Damit scheidet derFall (a) aus.(b) dfs(x) wird unterhalb von dfs(v) aufgerufen:Dies widerspricht der Struktur der Tiefensuche.vx¯v¯x


4.1. GERICHTETE GRAPHEN 175(c) dfs(v) liegt innerhalb von dfs(x):xv¯v¯xDann gibt es in G einen Weg von x nach v.Für die folgende Implementierung erweitern wir Programm 4.1.10, das gerichteteGraphen durch ihre Adjazenzmatrix darstellt. Es werden zwei Variantender rekursiven Realisierung der Tiefensuche benutzt. Die erste durchläuft denGraphen G <strong>und</strong> notiert die neue Knotennummerierung, die zweite durchläuftden Graphen G −1 , wobei die Adjazenzmatrix von G entsprechend interpretiertwird, <strong>und</strong> ordnet jedem Knoten seine Komponentennummer zu. Als Aufwandfür diese Implementierung erhalten wir den Rechenzeitbedarf O(n 2 ).Programm 4.1.36. Eine Implementierung von Algorithmus 4.1.33:wie in Programm 4.1.1074 public class StrongComponents {7576 /** Nach der Konstruktion ist komponente[v] == k genau dann, wenn77 * Knoten v zur Komponente mit Nummer k gehoert.78 */79 private int[ ] komponente = new int[ nodeSize( ) ]; nodeSize8081 /** Die Nummer der aktuell zu bestimmenden Komponente, eine Zahl >= 1. */82 private int laufendeKomponente = 1;8384 /** Nach der Konstruktion ist nummer[v] == n genau dann, wenn85 * Knoten v die Nummer n hat. Waehrend der Depth-First-Nummerierung86 * ist nummer[v] == 0 genau dann, wenn v noch nicht besucht wurde.87 */88 private int[ ] nummer = new int[ nodeSize( ) ];8990 /** Die aktuell zu vergebende Knotennummer, eine Zahl >= 1. */91 private int laufendeNummer = 1;9293 /** Bestimmt die starken Komponenten des Graphen. */94 public StrongComponents( ) {95 // Depth-First-Nummerierung des Graphen:96 int nichtBesuchterKnoten;97 while( ( nichtBesuchterKnoten = nichtBesuchterKnoten( ) ) >= 0 )98 depthFirstNumbering( nichtBesuchterKnoten );99 // Depth-First-Traversierung des inversen Graphen:100 int maximalKnoten;101 while( ( maximalKnoten = maximalKnoten( ) ) >= 0 ) {102 inverseDepthFirstTraversal( maximalKnoten );103 laufendeKomponente++;104 }105 }


176 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN106107 /** Depth-First-Nummerierung der von Knoten v aus erreichbaren Knoten. */108 private void depthFirstNumbering( int v ) { depthFirstNumbering109 nummer[ v ] = 1; // als besucht markiert; die Nummer wird spaeter bestimmt110 for( int w = 0; w < nodeSize( ); w++ )111 if( isEdge( v, w ) && nummer[ w ] == 0 )112 depthFirstNumbering( w );113 nummer[v] = laufendeNummer; // die tatsaechliche Nummer wird bestimmt114 laufendeNummer++;115 }116117 /** Gibt den ersten waehrend der Depth-First-Nummerierung noch nicht118 * besuchten Knoten zurueck, oder -1, falls kein solcher Knoten existiert.119 */120 private int nichtBesuchterKnoten( ) { nichtBesuchterKnoten121 for( int v = 0; v < nodeSize( ); v++ )122 if( nummer[v] == 0 ) // v ist noch nicht besucht123 return v;124 return −1;125 }126127 /** Depth-First-Traversierung des inversen Graphen beginnend bei Knoten v.128 * Die erreichbaren Knoten bilden die Komponente mit Nummer129 * laufendeKomponente.130 */131 private void inverseDepthFirstTraversal( int v ) { inverseDepthFirstTraversal132 komponente[ v ] = laufendeKomponente;133 nummer[ v ] = 0; // erreichten Knoten als besucht markieren134 for( int w = 0; w < nodeSize( ); w++ )135 if( isEdge( w, v ) && nummer[ w ] != 0 )136 inverseDepthFirstTraversal( w );137 }138139 /** Gibt den Knoten mit maximalem Wert im Array nummer zurueck,140 * oder -1, falls kein Knoten dort einen Wert > 0 hat.141 */142 private int maximalKnoten( ) { maximalKnoten143 int maximalWert = 0, maximalKnoten = −1;144 for( int i = 0 ; i < nummer.length; i++ )145 if( maximalWert < nummer[i] ) {146 maximalWert = nummer[i];147 maximalKnoten = i;148 }149 return maximalKnoten;150 }151152 /** Gibt eine String-Darstellung der starken Komponenten zurueck. */153 public String toString( ) { toString154 StringBuffer out = new StringBuffer( );155 out.append( "Die starken Komponenten des Graphen:\n\n" );156 // Die Anzahl der Komponenten ist laufendeKomponente - 1.157 for( int k = 1; k < laufendeKomponente; k++ ) {158 out.append( " Komponente " + k + ": { " );159 boolean komma = false;


4.1. GERICHTETE GRAPHEN 177160 for( int v = 0; v < nodeSize( ); v++ )161 if( komponente[v] == k ) {162 out.append( ( komma ? ", " : "" ) + String.valueOf( v ) );163 komma = true;164 }165 out.append( " }\n" );166 }167 return out.toString( );168 }169170 } // class StrongComponentsdie Methoden conc, print <strong>und</strong> toString wie in Programm 4.1.10207 public static void main( String[ ] args ) { main208 DirectedGraph b = new DirectedGraph( 11 );209 b.insertEdge( 0, 1 ); b.insertEdge( 0, 2 ); b.insertEdge( 1, 6 );210 b.insertEdge( 1, 4 ); b.insertEdge( 2, 6 ); b.insertEdge( 3, 2 );211 b.insertEdge( 3, 7 ); b.insertEdge( 5, 3 ); b.insertEdge( 5, 4 );212 b.insertEdge( 6, 0 ); b.insertEdge( 7, 5 ); b.insertEdge( 7, 8 );213 b.insertEdge( 8, 9 ); b.insertEdge( 9, 10 ); b.insertEdge( 10, 9 );214 System.out.println( "Der Graph aus Beispiel 4.1.34:\n\n" +215 b + "\n" + b.new StrongComponents( ) );216 }217218 } // class DirectedGraphEin Testlauf:Der Graph aus Beispiel 4.1.34:| 0 1 2 3 4 5 6 7 8 9 10____|_________________________________0 | 0 1 1 0 0 0 0 0 0 0 01 | 0 0 0 0 1 0 1 0 0 0 02 | 0 0 0 0 0 0 1 0 0 0 03 | 0 0 1 0 0 0 0 1 0 0 04 | 0 0 0 0 0 0 0 0 0 0 05 | 0 0 0 1 1 0 0 0 0 0 06 | 1 0 0 0 0 0 0 0 0 0 07 | 0 0 0 0 0 1 0 0 1 0 08 | 0 0 0 0 0 0 0 0 0 1 09 | 0 0 0 0 0 0 0 0 0 0 110 | 0 0 0 0 0 0 0 0 0 1 0Die starken Komponenten des Graphen:Komponente 1: { 3, 5, 7 }Komponente 2: { 8 }Komponente 3: { 9, 10 }Komponente 4: { 0, 1, 2, 6 }Komponente 5: { 4 }


178 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN4.2 Ungerichtete GraphenDefinition 4.2.1. Ein ungerichteter Graph ist ein gerichteter Graph, bei demdie Kantenrelation E symmetrisch ist: (u, v) ∈ E genau dann, wenn (v, u) ∈ E.Zur Vereinfachung fassen wir die Kantenmenge {(u, v), (v, u)} als eine ungerichteteKante zwischen u <strong>und</strong> v auf, die wir graphisch wie folgt darstellen: u vDie Begriffe Weg <strong>und</strong> Teilgraph lassen sich unmittelbar auf ungerichtete Graphenübertragen. Ein Zykel ist ein geschlossener einfacher Weg, der mindestensdrei verschiedene Knoten durchläuft. Für einen Knoten v ist der Teilgraph,der von der Menge aller mit v verb<strong>und</strong>enen Knoten induziert wird, die(Zusammenhangs-)Komponente von v. Ein ungerichteter Graph heißt verb<strong>und</strong>en,wenn er aus nur einer Komponente besteht. Er heißt azyklisch, wenn erkeine Zykeln enthält.Ein Wald ist ein azyklischer, ungerichteter Graph, <strong>und</strong> ein freier Baum ist einverb<strong>und</strong>ener Wald. Indem man einen Knoten als Wurzel auszeichnet <strong>und</strong> alleKanten so orientiert, dass sie von der Wurzel weg zeigen, erhält man aus einemfreien Baum einen Baum gemäß Definition 2.1.1.Ein gewichteter ungerichteter Graph ist ein ungerichteter Graph G = (V, E) zusammenmit einer Gewichtungsfunktion c : E → R + , die symmetrisch ist, d.h.für alle u, v ∈ V ist c((u, v)) = c((v, u)). Die Kosten des Graphen G ergebensich als c(G) = ∑ e∈E c(e).Freie Bäume bzw. Wälder haben die folgenden wichtigen Eigenschaften.Lemma 4.2.2. Ein freier Baum mit n Knoten hat n − 1 Kanten; fügt maneine Kante hinzu, so entsteht genau ein Zykel. Allgemeiner gilt: Ein Wald mitn Knoten <strong>und</strong> k Komponenten hat n − k Kanten; fügt man innerhalb einerKomponente eine Kante hinzu, so entsteht genau ein Zykel.Zur Implementierung ungerichteter Graphen kann man wieder (dann symmetrische)Adjazenzmatrizen (Def. 4.1.8) oder Adjazenzlisten (Def. 4.1.11) verwenden.Auch Tiefensuche (Algorithmus 4.1.19) <strong>und</strong> Breitensuche (Algorithmus4.1.23) sind hier anwendbar. Diese <strong>Algorithmen</strong> liefern für ungerichteteGraphen auch die Zusammenhangskomponenten.4.2.1 Minimale aufspannende WälderWir beschließen dieses Kapitel mit der Untersuchung eines weiteren Graphen-Algorithmus.Definition 4.2.3. Sei G ein gewichteter ungerichteter Graph. Ein aufspannenderWald für G ist ein Wald, der Teilgraph von G ist <strong>und</strong> dieselbe Knotenmenge


4.2. UNGERICHTETE GRAPHEN 179sowie dieselbe Anzahl von Komponenten hat wie G. Ein minimaler aufspannenderWald für G ist ein aufspannender Wald für G mit minimalen Kosten. Ein(minimaler) aufspannender Baum ist ein verb<strong>und</strong>ener (minimaler) aufspannenderWald, existiert also nur, wenn G verb<strong>und</strong>en ist.Wir wollen im Folgenden die Aufgabe lösen, zu einem gewichteten ungerichtetenGraphen einen minimalen aufspannenden Wald zu bestimmen.Beispiel 4.2.4. Sei G der folgende ungerichtete Graph, wobei die Kosten einerKante als Markierung an die Kante geschrieben sind: 13A 184 9 7E 10 5 D B12 CDer folgende Baum ist ein aufspannender Baum für G: 13A9 E D B127 CSeine Kosten betragen 41. Ist er minimal?Lemma 4.2.5. Sei G = (V, E) ein ungerichteter Graph mit symmetrischerGewichtungsfunktion c. Sei H = (V, F ) ein Wald, der Teilgraph eines minimalenaufspannenden Waldes H ′ = (V, F ′ ) für G ist. Sei ferner (u, v) ∈ E eineKante, die verschiedene Komponenten von H verbindet <strong>und</strong> minimales Gewichtunter allen solchen Kanten hat. Dann ist auch (V, F ∪ {(u, v)}) Teilgraph einesminimalen aufspannenden Waldes für G.Beweis: Für (u, v) ∈ F ′ gilt die Aussage. Andernfalls enthält (V, F ′ ∪ {(u, v)})nach Lemma 4.2.2 einen Zykel, also gibt es in H ′ einen Weg von v nach u. Dav <strong>und</strong> u in verschiedenen Komponenten von H liegen, enthält dieser Weg aucheine Kante (ū, ¯v), die verschiedene Komponenten von H verbindet. Damit istder GraphH ′′ = (V, F ′ \ {(ū, ¯v)} ∪ {(u, v)})


180 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENein aufspannender Wald für G, <strong>und</strong> es giltc(H ′′ ) = c(H ′ ) − c((ū, ¯v)) + c((u, v)) ≤ c(H ′ )wegen c((u, v)) ≤ c((ū, ¯v)). Also ist auch H ′′ ein minimaler aufspannender Waldfür G, der nun (V, F ∪ {(u, v)}) als Teilgraphen enthält.Auf diesem Lemma basiert der folgende Algorithmus von Kruskal zur Bestimmungeines minimalen aufspannenden Waldes für G. Er arbeitet wie folgt: ZuBeginn wird der zu berechnende Wald als H = (V, ∅) gewählt. Nun werdendie Kanten von G der Reihe nach angeschaut, wobei die Kanten nach aufsteigendemGewicht sortiert sind. Hierfür eignet sich also eine Prioritätsschlange,wie sie etwa durch einen Heap realisiert wird. Verbindet die betrachtete Kantezwei Komponenten von H, so wird sie zu H hinzugenommen, andernfalls wirddie Kante ignoriert. Am Ende ist H dann der gesuchte minimale aufspannendeWald. Zur Verwaltung der Komponenten verwenden wir die <strong>Algorithmen</strong> ausAbschnitt 3.6.Algorithmus 4.2.6. Kruskals Algorithmus zur Bestimmung eines minimalenaufspannenden Waldes für G = (V, E). Ein Heap speichert Kanten (v, w) alsTripel (v, w, c(v, w)); minimales Gewicht hat maximale Priorität.1 WeightedUndirectedGraph minimalSpanningForest( ) { minimalSpanningForest2 Partition komponenten = new Partition( |V | );3 WeightedUndirectedGraph minimalSpanningForest =4 new WeightedUndirectedGraph( |V | );5 Heap heap = new Heap( |E| );6 for all( (v, w) ∈ E )7 heap.einfuegenInHeap( (v, w, c((v, w))) );8 while( !heap.istLeer( ) ) {9 Edge minimalEdge = heap.loeschenAusHeap( );10 int a = komponenten.find( minimalEdge.start );11 int b = komponenten.find( minimalEdge.destination );12 if( a != b ) { // minimalEdge verbindet zwei Komponenten13 komponenten.union( a, b );14 minimalSpanningForest.insertEdge( minimalEdge );15 }16 }17 return minimalSpanningForest;18 }Beispiel 4.2.7. (Forts. von Beispiel 4.2.4) Für den Graphen G liefert Algorithmus4.2.6 den folgenden minimalen aufspannenden Baum mit Kosten 28: A4 E B127 C5 D


4.2. UNGERICHTETE GRAPHEN 181Lemma 4.2.8. Kruskals Algorithmus hat einen Zeitbedarf von O(|E| log |E|).Beweis: Die Partition {{0}, . . . , {|V | − 1}} wird in Zeit O(|V |) initialisiert.Nach der Analyse von HEAPSORT (Beweis von Lemma 2.3.13) kann die Prioritätsschlangein Zeit O(|E|) initialisiert werden, wenn sie durch einen Heapimplementiert wird. Die while-Schleife wird |E|-mal durchlaufen. Insgesamt werdendabei |E| Elemente aus dem Heap entfernt, was Zeit O(|E| log |E|) kostet,<strong>und</strong> es werden 2|E| Finde- <strong>und</strong> maximal |V | − 1 Vereinigungs-Operationen ausgeführt.Nach Satz 3.6.11 reichen hierfür O(|E|α(2|E|, |V | − 1)) Schritte aus.Insgesamt haben wir also einen Rechenzeitbedarf von O(|E| log |E|).Programm 4.2.9. Die folgende Implementierung von Kruskals Algorithmusbenutzt als Heaps Instanzen der Klasse VollstaendigerBinaererBaum aus Programm2.3.14; im Heap werden Kanten als Tripel aus Startknoten, Zielknoten<strong>und</strong> Gewicht gespeichert (Typ Edge), die bezüglich ihres Gewichts geordnetsind. Die Klasse Partition aus Programm 3.6.12 dient zur Verwaltung derZusammenhangskomponenten.1 import java.util.LinkedList;2 import java.util.Random;34 /** Die Klasse WeightedUndirectedGraph implementiert ungerichtete Graphen5 * mit Gewichtungsfunktion ueber Adjazenzmatrizen. Knoten sind Zahlen >= 06 * vom Typ int, Gewichte sind Zahlen > 0 vom Typ double.7 * Mehrfachkanten sind nicht zugelassen.8 */9 public class WeightedUndirectedGraph {1011 /** Die Anzahl der Knoten des Graphen.12 * Knoten sind Zahlen v mit 0


182 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN3435 /** Konstruiert einen Zufallsgraphen mit n Knoten. Jede Kante ist mit36 * Wahrscheinlichkeit p vorhanden. Das Gewicht einer Kante ist als Zahl37 * vom Typ int gleichverteilt aus (0, maxWeight] gewaehlt.38 */39 public WeightedUndirectedGraph( int n, double p, double maxWeight ) { WeightedUndirectedGraph40 this( n );41 for( int v = 0; v < N; v++ )42 for( int w = 0; w < v; w++ )43 if( Math.random( ) < p )44 insertEdge( v, w, (int)( Math.random( ) * maxWeight ) + 1 );45 }4647 /** Gibt die Knotenzahl zurueck. */48 public int nodeSize( ) { nodeSize49 return N;50 }5152 /** Gibt die Kantenzahl zurueck. */53 public int edgeSize( ) { edgeSize54 int e = 0;55 for( int v = 0; v < N; v++ )56 for( int w = 0; w < v; w++ )57 e += isEdge( v, w ) ? 1 : 0;58 return e;59 }6061 /** Gibt die Summe aller Kantengewichte zurueck. */62 public double sumOfWeights( ) { sumOfWeights63 return sumOfWeights;64 }6566 /** Gibt true zurueck, wenn eine Kante zwischen Knoten v <strong>und</strong> w vorhanden ist,67 * sonst false. Falls nicht 0 = N | | w >= N )71 throw new IllegalArgumentException( "Knoten nicht vorhanden." );72 return matrix[v][w] > 0;73 }7475 /** Fuegt eine Kante mit Gewicht weight zwischen Knoten v <strong>und</strong> w ein,76 * falls zwischen v <strong>und</strong> w keine Kante vorhanden ist. Falls nicht77 * 0 0 gilt, wird eine Ausnahme ausgeloest.78 */79 public void insertEdge( int v, int w, double weight ) { insertEdge80 if( v < 0 | | w < 0 | | v >= N | | w >= N )81 throw new IllegalArgumentException( "Knoten nicht vorhanden." );82 if( weight


4.2. UNGERICHTETE GRAPHEN 18384 if( !isEdge( v, w ) ) {85 if( maximalWeight < weight )86 maximalWeight = weight;87 sumOfWeights += weight;88 matrix[v][w] = matrix[w][v] = weight; // die Matrix ist symmetrisch89 }90 }9192 /** Fuegt die Kante e ein, falls zwischen e.start <strong>und</strong> e.destination keine93 * Kante vorhanden ist. Falls nicht 0 0 gilt, wird eine Ausnahme ausgeloest.95 */96 public void insertEdge( Edge e ) { insertEdge97 insertEdge( e.start, e.destination, e.weight );98 }99100 /** Die Klasse Edge implementiert Kanten des ungerichteten Graphen.101 * Kanten sind Tripel aus Startknoten, Zielknoten <strong>und</strong> Gewicht.102 */103 public class Edge implements Comparable {104105 /** Der Startknoten der Kante. */106 private int start;107108 /** Der Zielknoten der Kante. */109 private int destination;110111 /** Das Gewicht der Kante. */112 private double weight;113114 /** Konstruiert eine Kante mit Startknoten start, Zielknoten destination115 * <strong>und</strong> Gewicht weight. Falls nicht 0 0116 * gilt, wird eine Ausnahme ausgeloest.117 */118 public Edge( int start, int destination, double weight ) { Edge119 if( start < 0 | | destination < 0 | |120 start >= nodeSize( ) | | destination >= nodeSize( ) )121 throw new IllegalArgumentException( "Knoten nicht vorhanden." );122 if( weight otherWeight ? +1 : 0;133 }134135 } // class Edge136


184 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMEN137 /** Kruskals Algorithmus bestimmt einen minimalen aufspannenden Wald. */138 public WeightedUndirectedGraph minimalSpanningForest( ) { minimalSpanningForest139 Partition komponenten = new Partition( N );140 WeightedUndirectedGraph minimalSpanningForest =141 new WeightedUndirectedGraph( N );142 // Ein Heap, der alle Kanten enthaelt;143 // minimales Gewicht hat maximale Prioritaet:144 VollstaendigerBinaererBaum heap =145 new VollstaendigerBinaererBaum( edgeSize( ) );146 for( int v = 0; v < N; v++ )147 for( int w = 0; w < v; w++ )148 if( isEdge( v, w ) )149 heap.einfuegenInHeap( new Edge( v, w, matrix[v][w] ) );150 while( !heap.istLeer( ) ) {151 Edge minimalEdge = (Edge)heap.loeschenAusHeap( );152 int a = komponenten.find( minimalEdge.start );153 int b = komponenten.find( minimalEdge.destination );154 if( a != b ) { // minimalEdge verbindet zwei Komponenten155 komponenten.union( a, b );156 minimalSpanningForest.insertEdge( minimalEdge );157 }158 }159 return minimalSpanningForest;160 }die Methoden conc <strong>und</strong> print wie in Programm 4.1.10175 /** Gibt eine String-Darstellung des Graphen zurueck.176 * Gewichte werden zu ganzen Zahlen abger<strong>und</strong>et.177 */178 public String toString( ) { toString179 StringBuffer out = new StringBuffer( );180 // Das Maximum der Laengen der Darstellungen von N <strong>und</strong> maximalWeight:181 int n = Math.max( Integer.toString( N ).length( ),182 Integer.toString( (int)maximalWeight ).length( ) );183 // Die Spaltenueberschrift:184 out.append( conc( " ", n+2 ) + "|" );185 for( int v = 0; v < N; v++ )186 out.append( print( v, n+1 ) );187 out.append( "\n" + conc( "_", n+2 ) + "|" + conc( "_", N*(n+1) ) + "\n" );188 // Die Matrix:189 for( int v = 0; v < N; v++ ) {190 out.append( print( v, n+1 ) + " |" );191 for( int w = 0; w < v; w++ )192 out.append( conc( " ", n+1 ) );193 for( int w = v; w < N; w++ )194 out.append( print( (int)matrix[v][w], n+1 ) );195 out.append( "\n" );196 }197 return out.toString( );198 }199


4.2. UNGERICHTETE GRAPHEN 185200 public static void main( String[ ] args ) { main201 WeightedUndirectedGraph usa = new WeightedUndirectedGraph( 8 );202 usa.insertEdge( 1, 0, 300 ); usa.insertEdge( 2, 1, 800 );203 usa.insertEdge( 2, 0, 1000 ); usa.insertEdge( 3, 2, 1200 );204 usa.insertEdge( 4, 3, 1500 ); usa.insertEdge( 4, 5, 250 );205 usa.insertEdge( 5, 3, 1000 ); usa.insertEdge( 5, 6, 900 );206 usa.insertEdge( 5, 7, 1400 ); usa.insertEdge( 6, 7, 1000 );207 usa.insertEdge( 7, 0, 1700 );208 System.out.println( "Der Graph aus Beispiel 4.1.31:\n\n" + usa );209 WeightedUndirectedGraph usaForest = usa.minimalSpanningForest( );210 System.out.println( "Ein minimaler aufspannender Wald mit Gewicht " +211 usaForest.sumOfWeights( ) + ":\n\n" + usaForest );212213 WeightedUndirectedGraph u = new WeightedUndirectedGraph( 5 );214 u.insertEdge( 0, 1, 13 ); u.insertEdge( 0, 3, 9 );215 u.insertEdge( 0, 4, 4 ); u.insertEdge( 1, 2, 12 );216 u.insertEdge( 1, 4, 18 ); u.insertEdge( 2, 3, 5 );217 u.insertEdge( 2, 4, 7 ); u.insertEdge( 3, 4, 10 );218 System.out.println( "\nDer Graph aus Beispiel 4.2.4:\n\n" + u );219 WeightedUndirectedGraph uForest = u.minimalSpanningForest( );220 System.out.println( "Ein minimaler aufspannender Wald mit Gewicht " +221 uForest.sumOfWeights( ) + ":\n\n" + uForest );222 }223224 } // class WeightedUndirectedGraphEin Testlauf:Der Graph aus Beispiel 4.1.31:| 0 1 2 3 4 5 6 7______|________________________________________0 | 0 300 1000 0 0 0 0 17001 | 0 800 0 0 0 0 02 | 0 1200 0 0 0 03 | 0 1500 1000 0 04 | 0 250 0 05 | 0 900 14006 | 0 10007 | 0Ein minimaler aufspannender Wald mit Gewicht 5450.0:| 0 1 2 3 4 5 6 7______|________________________________________0 | 0 300 0 0 0 0 0 01 | 0 800 0 0 0 0 02 | 0 1200 0 0 0 03 | 0 0 1000 0 04 | 0 250 0 05 | 0 900 06 | 0 10007 | 0


186 KAPITEL 4. GRAPHEN UND GRAPH-ALGORITHMENDer Graph aus Beispiel 4.2.4:| 0 1 2 3 4____|_______________0 | 0 13 0 9 41 | 0 12 0 182 | 0 5 73 | 0 104 | 0Ein minimaler aufspannender Wald mit Gewicht 28.0:| 0 1 2 3 4____|_______________0 | 0 0 0 0 41 | 0 12 0 02 | 0 5 73 | 0 04 | 0


Kapitel 5SortieralgorithmenIn diesem Kapitel kommen wir zum Problem des Sortierens zurück, das wir inAbschnitt 2.3 schon angesprochen hatten. Dort hatten wir zwei <strong>Algorithmen</strong> fürdas Sortieren kennengelernt, nämlich TREE SORT <strong>und</strong> HEAP SORT, die beidebinäre Bäume als unterliegende Datenstruktur für das Sortieren verwenden.Hier werden wir einige weitere Sortieralgorithmen kennenlernen, analysieren<strong>und</strong> miteinander vergleichen.5.1 Elementare SortieralgorithmenZunächst wollen wir die Problemstellung fixieren.Definition 5.1.1. Das Sortierproblem für die durch die Ordnung ≤ linear geordneteMenge Item ist das Problem, folgende Aufgabe zu lösen:Eingabe:Eine endliche Folge (a 0 , a 1 , . . . , a n−1 ) von Elementen aus Item.Aufgabe: Bestimme eine Permutation π : {0, . . . , n − 1} → {0, . . . , n − 1},so dass a π(0) ≤ a π(1) ≤ · · · ≤ a π(n−1) gilt, d.h. so dass die Folge(a π(0) , a π(1) , . . . , a π(n−1) ) aufsteigend sortiert ist.Ein Algorithmus, der diese Aufgabe löst, ist ein Sortieralgorithmus. Falls ausi < j <strong>und</strong> a i = a j stets π −1 (i) < π −1 (j) folgt (d.h. falls auch in der sortiertenFolge a i vor a j kommt), so heißt der Sortieralgorithmus stabil.Bemerkung 5.1.2. Man macht sich leicht klar, dass TREE SORT (aus Abschnitt2.3.1) so realisiert werden kann, dass es ein stabiles Verfahren ist,während HEAP SORT (aus Abschnitt 2.3.2) inhärent instabil ist.Hier betrachten wir zunächst zwei elementare Sortierverfahren. Immer nehmenwir an, dass die Eingabefolge (a 0 , . . . , a n−1 ) in einem Array a der Größe n überdem Typ Comparable steht. Am Ende der Berechnung enthält das Array diesortierte Ausgabefolge.187


188 KAPITEL 5. SORTIERALGORITHMENAlgorithmus 5.1.3. SELECTION SORT (Sortieren durch Auswählen):1 selectionSort( Comparable[ ] a ) { selectionSort2 for( int i = 0; i < n−1; i++ ) {3 // Erstes minimales Element in (a[i], . . . , a[n − 1]) finden . . .4 int min = i; // Position des aktuellen minimalen Elements5 for( int j = i+1; j < n; j++ )6 if( a[ j ].compareTo( a[ min ] ) < 0 )7 min = j;8 // . . . <strong>und</strong> mit a[i] vertauschen:9 swap( a, min, i );10 }11 }1213 /** Vertauscht die Array-Elemente a[i] <strong>und</strong> a[j] (0 ≤ i, j < n). */14 swap( Comparable[ ] a, int i, int j ) { swap15 Comparable tmp = a[ i ];16 a[ i ] = a[ j ];17 a[ j ] = tmp;18 }Satz 5.1.4. Algorithmus SELECTION SORT sortiert ein Array mit n Elementenin Zeit O(n 2 ). Dabei werden O(n 2 ) viele Schlüsselvergleiche <strong>und</strong> O(n)viele Vertauschungen durchgeführt.Beweis: Die äußere Schleife wird (n − 1)-mal, die innere jeweils (n − 1 − i)-maldurchlaufen. Dies ergibt einen Zeitbedarf von c · ∑n−2∑n−1i=0(n − 1 − i) = c ·i=1 i ∈O(n 2 ).Beim Sortieren durch Auswählen wird im i-ten Schritt das i-te Element dersortierten Folge bestimmt <strong>und</strong> an seinen Platz gebracht. Beim folgenden Verfahren,dem Sortieren durch Einfügen, wird die Anfangsfolge der Länge i + 1sortiert, indem das (i + 1)-te Element der unsortierten Folge in die bereits sortierteTeilfolge der ersten i Elemente an der richtigen Stelle eingefügt wird.Algorithmus 5.1.5. INSERTION SORT (Sortieren durch Einfügen):1 insertionSort( Comparable[ ] a ) { insertionSort2 for( int i = 1; i < n; i++ ) {3 Comparable tmp = a[ i ];4 int j = i;5 for( ; j > 0 && tmp.compareTo( a[ j−1 ] ) < 0; j−− )6 a[ j ] = a[ j−1 ];7 a[ j ] = tmp;8 }9 }In der inneren Schleife wird Platz für das Element a[i] gemacht, indem allegrößeren Elemente zwischen a[i − 1] <strong>und</strong> a[0] um jeweils einen Platz nach rechtsgeschoben werden. Die äußere Schleife wird (n − 1)-mal durchlaufen, die innereim schlechtesten Fall ∑ n−1i=1 i ∈ O(n2 ) mal. Also gilt:


5.2. SORTIERVERFAHREN MIT DIVIDE-AND-CONQUER 189Satz 5.1.6. Algorithmus INSERTION SORT sortiert ein Array mit n Elementenin Zeit O(n 2 ). Dabei werden O(n 2 ) Schlüsselvergleiche <strong>und</strong> Umspeicherungenvorgenommen.Statt das Anfangsstück von a[i−1] bis a[0] linear zu durchlaufen, um die richtigePosition für das Element a[i] zu bestimmen, kann man auf diesem Breich auchBinärsuche für diese Aufgabe einsetzen. Dann wird die Position für a[i] in ZeitO(log i) gef<strong>und</strong>en. Allerdings brauchen wir im schlechtesten Fall noch immer iUmspeicherungen, um a[i] an der gef<strong>und</strong>enen Position unterzubringen, d.h. dieseVariante von INSERTION SORT kommt mit O(n log n) Schlüsselvergleichenaus, braucht aber dennoch Rechenzeit O(n 2 ).Algorithmus 5.1.7. INSERTION SORT mit Binärsuche :1 insertionSortBinarySearch( Comparable[ ] a ) { insertionSortBinarySearch2 for( int i = 1; i < n; i++ ) {3 Comparable tmp = a[ i ];4 // Position fuer a[i] im Bereich a[0] bis a[i − 1] binaer suchen:5 int left = 0;6 int right = i−1;7 while( left 0 )12 left = mid + 1;13 else {14 left = mid;15 break;16 }17 }18 // Dann a[i] an Position left bringen:19 for( int j = i; j > left; j−− )20 a[ j ] = a[ j−1 ];21 a[ left ] = tmp;22 }23 }5.2 Sortierverfahren mit Divide-and-ConquerDie nächsten beiden Sortierverfahren beruhen auf der Strategie ”Divide andConquer“ (Teile <strong>und</strong> Herrsche): Die Folge (a 0 , . . . , a n−1 ) wird in zwei Teilfolgenaufgeteilt, etwa (a 0 , . . . , a m ) <strong>und</strong> (a m+1 , . . . , a n−1 ), die dann getrennt voneinandernach demselben Verfahren sortiert <strong>und</strong> anschließend zu einer sortiertenGesamtfolge zusammengefügt werden. Beim ersten Verfahren ist das Aufteilentrivial, <strong>und</strong> die gesamte Arbeit wird beim Zusammenfügen der sortierten Teilfolgengeleistet. Beim zweiten Verfahren erfordert das Aufteilen die hauptsächlicheArbeit, <strong>und</strong> das Zusammenfügen ist trivial.


190 KAPITEL 5. SORTIERALGORITHMENAls wichtigen Bestandteil des ersten Verfahrens brauchen wir also einen Algorithmus,der zwei sortierte Teilfolgen zu einer sortierten Folge zusammenmischt.Algorithmus 5.2.1. MERGE: Mischen zweier sortierter Teil-Arrays a[left] bisa[mid] <strong>und</strong> a[mid + 1] bis a[right] zu einem sortierten Array a[left] bis a[right].Im Array b wird das Ergebnis zwischengespeichert.1 merge( Comparable[ ] a, Comparable[ ] b, int left, int mid, int right ) { merge2 int i1 = left; // laeuft hoch bis mid3 int i2 = mid + 1; // laeuft hoch bis right4 int bPos = left; // kopiere nach b ab bPos5 while( i1


5.2. SORTIERVERFAHREN MIT DIVIDE-AND-CONQUER 191Zum Aufwand: Die Anzahl der Schlüsselvergleiche ist C MERGE SORT (1) = 0 <strong>und</strong>C MERGE SORT (n) = 2 · C MERGE SORT (n/2) + (n − 1) für n > 1, also gilt:Anzahl der Schlüsselvergleiche: C MERGE SORT (n) ∈ O(n log n)Zeitbedarf: T MERGE SORT (n) ∈ O(n log n)Platzbedarf:S MERGE SORT (n) = 2n + cHieraus <strong>und</strong> aus der Art <strong>und</strong> Weise, wie in MERGE die beiden Teilfolgen zusammengemischtwerden, ergibt sich die folgende Aussage.Satz 5.2.3. Algorithmus MERGE SORT ist ein stabiles Sortierverfahren, dasein Array mit n Elementen in Zeit O(n log n) sortiert.MERGE SORT arbeitet ausgehend von dem Gesamtbereich gewissermaßen Top-Down. Für die Abarbeitung der rekursiven Aufrufe wird also ein Keller gebraucht.Durch ein entsprechendes Bottom-Up-Verfahren lässt sich dieser Kellereinsparen.Algorithmus 5.2.4. MERGE SORT BOTTOM UP:1 mergeSortBottomUp( Comparable[ ] a ) { mergeSortBottomUp2 Comparable[ ] b = new Comparable[ n ];3 for( int step = 1; step < n; step *= 2 ) {4 // Je zwei Bereiche der Groesse step werden zusammengemischt:5 for( int left = 0; left < n; left += 2*step ) {6 if( left + step < n ) { // sind noch zwei Bereich uebrig?7 int mid = left + step − 1;8 // Der letzte Bereich muss an Position n − 1 enden.9 if( mid + step >= n )10 merge( a, b, left, mid, n − 1 );11 else12 merge( a, b, left, mid, mid + step );13 }14 }15 }16 }Bei MERGE SORT wird die zu sortierende Folge in der Mitte geteilt, die entstehendenTeilfolgen werden unabhängig voneinander sortiert, <strong>und</strong> schließlichwerden die sortierten Teilfolgen zu einer sortierten Folge zusammengemischt.Es wäre natürlich vorteilhaft, wenn man die zu sortierende Folge stets so teilenkönnte, dass die Teilfolgen nach dem Sortieren nicht mehr zusammengemischtwerden müssten. Dies kann man erreichen, wenn man die Folge vor dem Teilenso umordnet, dass für eine Stelle m (0 ≤ m < n) folgendes gilt:0 ≤ i < m < j < n impliziert a i ≤ a m ≤ a j .


192 KAPITEL 5. SORTIERALGORITHMENDieses Umordnen wird als Partitionieren bezeichnet, wobei das Element a mder Schnittpunkt (oder: das Pivotelement) ist. Alle Elemente, die in der umgeordnetenFolge vor dem Schnittpunkt auftreten, sind also ≤ a m , die dahinter≥ a m . Die noch zu sortierenden Teilfolgen sind dann (a 0 , . . . , a m−1 ) <strong>und</strong>(a m+1 , . . . , a n−1 ). Für die Wahl des Schnittpunkts gibt es mehrere Strategien:• Der Schnittpunkt ist das erste Element a 0 der Eingabefolge.• Der Schnittpunkt ist das mittlere Element a ⌊(n−1)/2⌋ der Eingabefolge.• Der Schnittpunkt ist ein zufällig gewähltes Element der Eingabefolge.• Betrachte das erste, das mittlere <strong>und</strong> das letzte Element der Eingabefolge,d.h. die Menge {a 0 , a ⌊(n−1)/2⌋ , a n−1 }. Der Schnittpunkt ist das zweitgrößteElement dieser Menge ( ”Median-von-drei“).Bei zufällig verteilten Eingaben ist jede dieser Strategien akzeptabel, bei vorsortiertenoder umgekehrt sortierten Eingabefolgen schneidet aber beispielsweisedie erste Strategie sehr schlecht ab, da die Partitionierung sehr ungleich großeTeilfolgen erzeugt.Algorithmus 5.2.5. PARTITION: Aufteilen des Bereichs a[left] bis a[right]mit der Strategie ”Median-von-drei“ zur Wahl des Schnittpunkts:1 int partition( Comparable[ ] a, int left, int right ) { partition2 // “Median von drei”:3 int mid = ( left + right) / 2;4 if( a[ mid ].compareTo( a[ left ] ) < 0 )5 swap( a, mid, left );6 if( a[ right ].compareTo( a[ left ] ) < 0 )7 swap( a, right, left );8 if( a[ right ].compareTo( a[ mid ] ) < 0 )9 swap( a, right, mid );10 Comparable cut = a[ mid ]; // der Schnittpunkt der Partitionierung11 swap( a, mid, right−1 ); // der Schnittpunkt kommt an Position right-11213 // Das eigentliche Partitionieren:14 int i1 = left; // laeuft hinauf15 int i2 = right−1; // laeuft hinunter16 while( i1 < i2 ) {17 do i1++;18 while( a[ i1 ].compareTo( cut ) < 0 ); // nun ist a[i1] ≥ cut19 do i2−−;20 while( a[ i2 ].compareTo( cut ) > 0 ); // nun ist a[i2] ≤ cut21 if( i1 < i2 )22 swap( a, i1, i2 );23 }24 swap( a, i1, right − 1 ); // den Schnittpunkt wieder an seinen Platz bringen25 return i1;26 }


5.2. SORTIERVERFAHREN MIT DIVIDE-AND-CONQUER 193Beispiel 5.2.6. Die unsortierte Folge (93, 82, 30, 75, 13, 28, 33, 50, 11, 39) stehtbei (i). Bei (ii) wird die Situation nach Programmzeile 10 gezeigt. Der Schnittpunktist 39 als der Median der Menge {93, 13, 39}. Nach dem Vertauschen inZeile 11 entsteht das Array (iii); die initialen Zeigerpositionen (Zeile 14 <strong>und</strong> 15)sind markiert. Das Vertauschen in Zeile 22 findet zweimal statt, bei (vi) <strong>und</strong>bei (v); am Ende ist i1 = 5 <strong>und</strong> i2 = 4. Bei (vi) muss noch das Vertauschen inZeile 24 stattfinden. Das Resultat steht bei (vii).a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9](i) 93 82 30 75 13 28 33 50 11 39(ii) 13 82 30 75 39 28 33 50 11 93(iii) 13 82 30 75 11 28 33 50 39 93↑↑(iv) 13 82 30 75 11 28 33 50 39 93↑↑(v) 13 33 30 75 11 28 82 50 39 93↑ ↑(vi) 13 33 30 28 11 75 82 50 39 93(vii) 13 33 30 28 11 39 82 50 75 93Zum Aufwand (sei n = right − left + 1):Anzahl der Schlüsselvergleiche: C PARTITION (n) ≤ n + 2Zeitbedarf:T PARTITION (n) ∈ O(n)Algorithmus 5.2.7. QUICK SORT (Sortieren durch Partitionieren): Zur Optimierungwerden Arrays der Größe CUTOFF oder kleiner hierbei mit INSER-TION SORT sortiert. CUTOFF muss dabei größer als 1 sein; typische Wertesind zwischen 5 <strong>und</strong> 20.1 void quickSort( Comparable[ ] a ) { quickSort2 quickSort( a, 0, n − 1 );3 }45 void quickSort( Comparable[ ] a, int left, int right ) { quickSort6 if( left + CUTOFF > right ) // fuer kleine Bereiche7 insertionSort( a, left, right );8 else { // fuer grosse Bereiche9 int m = partition( a, left, right );10 quickSort( a, left, m − 1 ); // kleine Elemente sortieren11 quickSort( a, m + 1, right ); // grosse Elemente sortieren12 }13 }


194 KAPITEL 5. SORTIERALGORITHMENSatz 5.2.8. Algorithmus QUICK SORT sortiert ein Array mit n Elementenim schlechtesten Fall in Zeit O(n 2 ) mit O(n 2 ) Schlüsselvergleichen.Beweis: Wir haben gesehen, dass beim Partitionieren höchstens n+2 Schlüsselvergleiche<strong>und</strong> Zeit O(n) gebraucht werden. Im schlechtesten Fall wird beimrekursiven Partitionieren der Schnittpunkt an den Rand gelegt, d.h. die Folgewird nicht wirklich geteilt. Dann sind n geschachtelte Aufrufe möglich, wobeider i-te Aufruf eine Teilfolge der Größe n − i + 1 bearbeitet. Damit erhalten wir∑ ni=1 (n − i + 3) ∈ O(n2 ) Schlüsselvergleiche <strong>und</strong> Rechenzeit O(n 2 ).Damit ist QUICK SORT im schlechtesten Fall sehr ineffizient. Für die mittlereRechenzeit sieht es aber wesentlich besser aus.Satz 5.2.9. Für das Verhalten im Mittel gilt für QUICK SORT folgendes: Sindalle Schlüssel verschieden <strong>und</strong> wird der Schnittpunkt zufällig gewählt, dann istdie durchschnittliche Anzahl der Schlüsselvergleiche C(n) ∈ O(n log n).Beweis: Sei (a 0 , . . . , a n−1 ) eine Folge, in der alle Schlüssel verschieden sind. DieWahl des Schnittpunktes beim Partitionieren soll nun zufällig geschehen 1 . Wirnehmen an, dass beim Aufruf von partition(a, l, r) jedes Element der Teilfolge(a l , . . . , a r ) mit Wahrscheinlichkeit 1/(r − l + 1) als Schnittpunkt gewählt wird.Für jedes m (l ≤ m ≤ r) entstehen also mit Wahrscheinlichkeit 1/(r − l + 1)die beiden Teilfolgen (a l , . . . , a m−1 ) <strong>und</strong> (a m+1 , . . . , a r ). Damit erhalten wir<strong>und</strong> C(0) = C(1) = 0. Also gilt<strong>und</strong> insbesondere für n > 1C(n) = (n + 1) + 1 n−1n ·∑ ( )C(k) + C(n − k − 1)k=0n−1∑n · C(n) = n(n + 1) + 2 · C(k)k=0n−2∑(n − 1) · C(n − 1) = (n − 1)n + 2 · C(k).Subtrahiert man die letzte von der vorletzten Gleichung, so ergibt sichalso istk=0n · C(n) − (n − 1) · C(n − 1) = 2n + 2 · C(n − 1),C(n)n + 1 = 2n + 1C(n − 1)+ ,n1 Man überzeuge sich, dass dies mit n + 1 Schlüsselvergleichen implementiert werden kann.


5.2. SORTIERVERFAHREN MIT DIVIDE-AND-CONQUER 195worausC(n)n + 1 = 2n + 1 + 2 n + 2n − 1 + · · · + 2 3 + C(1) n+1∑ 1= 2 ·2kk=3folgt. Nun ist aber H n = ∑ nk=11/k die n-te Harmonische Zahl, für dieln n ≤ H n ≤ 1 + ln ngilt. Damit erhalten wir die Abschätzungn+1C(n)n + 1 = 2 · ∑ 1k = 2(H n+1 − 3/2) < 2 ln(n + 1),k=3die wiederum C(n) < 2(n + 1) ln(n + 1) ∈ O(n log n) liefert.Bemerkung 5.2.10. An zusätzlichem Speicherplatz braucht QUICK SORTeinen Keller zum Abarbeiten der Rekursion, der im schlechtesten Fall PlatzO(n) braucht. Ändert man QUICK SORT aber so ab, dass nach dem Teileneiner Folge stets zuerst die kleinere Teilfolge weiterbearbeitet wird, so brauchtdieser Keller nur Platz O(log n). Diese Verbesserung ist im folgenden Algorithmusrealisiert.1 void quickSortTuned( Comparable[ ] a ) { quickSortTuned2 quickSortTuned( a, 0, n − 1 );3 }45 void quickSortTuned( Comparable[ ] a, int left, int right ) { quickSortTuned6 if( left + CUTOFF > right ) // fuer kleine Bereiche7 insertionSort( a, left, right );8 else { // fuer grosse Bereiche9 int m = partition( a, left, right );10 // Rekursiven Aufruf zuerst fuer den kleineren Bereich:11 if( m − left < right − m ) {12 quickSortTuned( a, left, m − 1 ); // die kleinen Elemente sortieren13 quickSortTuned( a, m + 1, right ); // die grossen Elemente sortieren14 }15 else {16 quickSortTuned( a, m + 1, right ); // die grossen Elemente sortieren17 quickSortTuned( a, left, m − 1 ); // die kleinen Elemente sortieren18 }19 }20 }Bemerkung 5.2.11. Das Sortierverfahren QUICK SORT ist nicht stabil.


196 KAPITEL 5. SORTIERALGORITHMEN5.3 Sortieren durch FachverteilenDie bisher betrachteten Sortierverfahren benutzen von den verwendeten Schlüsselwertennur die Eigenschaft, dass sie aus einer linear geordneten Menge stammen.In diesen Verfahren werden Entscheidungen in Abhängigkeit davon getroffen,wie gewisse Schlüsselvergleiche ausgehen. Für das Sortieren von n Elementenbraucht man bei dieser Vorgehensweise im schlechtesten Fall Θ(n log n)Vergleiche. Hier wollen wir nun noch ein Verfahren vorstellen, das Informationenüber den Aufbau der Schlüsselelemente ausnutzt.Sei dazu Item ⊆ {0, 1, . . . , m k − 1} für ein m ≥ 2 <strong>und</strong> ein k ≥ 1. Dann lässt sichjedes Element p ∈ Item eindeutig darstellen als∑k−1p = d i · m ii=0mit 0 ≤ d i < m. Damit ist d k−1 d k−2 . . . d 1 d 0 die Darstellung der Zahl p zurBasis m. Wir nehmen hier also an, dass der Schlüsselbereich Item endlich ist.Das Sortierverfahren RADIX SORT sortiert eine Folge nun dadurch, dass dieeinzelnen Ziffern der Darstellung der Schlüssel zur Basis m von rechts nach linksdurchgegangen werden. Für jede Stelle wird die Folge einmal durchlaufen <strong>und</strong>anhand der Werte dieser Stelle umgeordnet. Nach k Durchläufen ist die Folgeschließlich sortiert, der Aufwand beträgt also nur O(k · n).Algorithmus 5.3.1. RADIX SORT sortiert hier der Einfachheit halber einArray über dem Typ Integer. Wir nehmen Item ⊆ {0, . . . , m k − 1} an für festeWerte m ≥ 2 <strong>und</strong> k ≥ 1.1 void radixSort( Integer[ ] a ) { radixSort2 Erzeuge m leere Listen list[0] bis list[m − 1].3 Erzeuge eine Hilfsliste tmpList, die zu Beginn die Elemente von a enthaelt.4 for( int i = 0; i < k; i++ ) { // Stelle i betrachten5 while( !tmpList.isEmpty( ) ) {6 int p = tmpList.removeFirst( );7 Sei p = d n−1 , . . . , d 1 d 0 als Darstellung zur Basis m.8 list[ d i ].addLast( p ); // p hinten in list[d i ] einfuegen9 }10 for( int j = 0; j < m; j++ ) {11 list[j] an tmpList anhaengen <strong>und</strong> list[j] leer machen.12 }13 }14 // tmpList enthaelt jetzt alle Elemente von a in sortierter Folge.15 tmpList.toArray( a ); // Schreibe diese Liste zurueck ins Array a16 }Beispiel 5.3.2. Seien k = 2 <strong>und</strong> m = 10, also Item ⊆ {0, . . . , 99}. Wir sortierendie Liste (3, 18, 14, 6, 47, 7, 56, 92, 98, 60, 75, 12). Für i = 0 werden dieElemente wie folgt auf die Listen list[0] bis list[9] verteilt:


5.4. SORTIERVERFAHREN IM VERGLEICH 197list[0] list[1] list[2] list[3] list[4] list[5] list[6] list[7] list[8] list[9]60 92 3 14 75 6 47 1812 56 7 98Daraus entsteht die Liste (60, 92, 12, 3, 14, 75, 6, 56, 47, 7, 18, 98). Für i = 1 liefertdie Verteilung der neuen Liste folgendes:list[0] list[1] list[2] list[3] list[4] list[5] list[6] list[7] list[8] list[9]3 12 47 56 60 75 926 14 987 18Wir erhalten also die sortierte Liste (3, 6, 7, 12, 14, 18, 47, 56, 60, 75, 92, 98).Man kann sich leicht klarmachen, dass dieses Verfahren auch auf Schlüsselmengenübertragen werden kann, die aus verschiedenartigen Komponenten bestehen,wie beispielsweise das Datum. Dann wird erst nach dem Tag, dann nachdem Monat <strong>und</strong> schließlich nach dem Jahr sortiert, wobei diese letzte R<strong>und</strong>eselbst wieder aus mehreren R<strong>und</strong>en bestehen kann.5.4 Sortierverfahren im VergleichDie folgende Tabelle fasst die wichtigsten Eigenschaften der betrachteten Sortierverfahrenzusammen.Verfahren Vergleiche Zeitbedarf Platzbedarf stabilTREE SORT O(n log n) O(n log n) 2n + c +HEAP SORT O(n log n) O(n log n) n + c −SELECTION SORT O(n 2 ) O(n 2 ) n + c −INSERTION SORT O(n 2 ) O(n 2 ) n + c +mit Binärsuche O(n log n) O(n 2 ) n + c −MERGE SORT O(n log n) O(n log n) 2n + c +QUICK SORT O(n 2 ) O(n 2 ) n + c log n −im Mittel O(n log n) O(n log n)RADIX SORT O(k · n) O(k · n) 2n + c +Die folgenden beiden Tabellen geben die Ergebnisse einiger Experimente zumVergleich des Laufzeitverhaltens von Sortierverfahren wieder. Immer wurdenArrays der Größe n zufällig mit ganzzahligen Werten aus dem Intervall [0, n)belegt. Die erste Tabelle gibt die benötigte Rechenzeit pro Array der jeweiligenGröße in Millisek<strong>und</strong>en an, die zweite Tabelle in Sek<strong>und</strong>en. Die Zahlen ergebensich als Durchschnittswerte für große Stichproben.Laufzeitmessungen dieser Art sind in der Regel zurückhalten zu interpretieren,da sie von vielen Faktoren (Plattform, Compiler, etc.) abhängen. Es zeigt sich


198 KAPITEL 5. SORTIERALGORITHMENimmerhin deutlich, dass QUICK SORT unter allen betrachteten Verfahren dasmit Abstand schnellste ist. Und dies gilt, obwohl QUICK SORT das schlechtesteVerhalten im schlechtesten Fall unter den sechs schnellsten Sortierverfahren ist.n 10 50 100 200 500 1000SELECTION SORT 0,009 0,21 0,8 3,3 21,1 94,9INSERTION SORT 0,006 0,11 0,4 1,6 10,5 45,5INSERTION S. BINARY SEARCH 0,009 0,08 0,2 0,6 2,3 7,6MERGE SORT 0,017 0,11 0,3 0,6 1,6 3,5MERGE S. BOTTOM UP 0,015 0,10 0,2 0,5 1,5 3,5HEAP SORT 0,028 0,23 0,5 1,3 3,8 8,5QUICK SORT 0,007 0,05 0,1 0,3 0,9 2,0QUICK SORT TUNED 0,006 0,05 0,1 0,3 0,9 2,0RADIX SORT 0,030 0,16 0,3 0,8 2,1 4,4n 2000 10000 50000 100000 200000SELECTION SORT 0,411 10,64 270,33 — —INSERTION SORT 0,192 5,16 147,26 — —INSERTION S. BINARY SEARCH 0,026 0,57 14,50 58,42 —MERGE SORT 0,008 0,05 0,29 0,65 1,4MERGE S. BOTTOM UP 0,008 0,05 0,35 0,73 1,6HEAP SORT 0,019 0,12 0,79 1,82 3,9QUICK SORT 0,005 0,03 0,20 0,44 1,0QUICK SORT TUNED 0,005 0,03 0,21 0,43 1,0RADIX SORT 0,012 0,10 1,63 3,08 12,8Programm 5.4.1. Zuletzt geben wir hier noch eine Implementierung aller diskutiertenSortierverfahren an, die auch den obigen Tests zugr<strong>und</strong>e lag:1 import java.util.Arrays;2 import java.util.List;3 import java.util.LinkedList;45 /** Die Klasse Sort implementiert verschiedene Sortieralgorithmen.6 * Alle <strong>Algorithmen</strong> sortieren ein Array ueber dem Typ Comparable7 * bezueglich der durch compareTo definierten Ordnung.8 */9 public class Sort {1011 /** Implementiert den Algorithmus SELECTION SORT. */12 public static void selectionSort( Comparable[ ] a ) { selectionSort13 for( int i = 0; i < a.length−1; i++ ) {14 // Erstes minimales Element in (a[i],. . .,a[n-1]) finden . . .15 int min = i; // Position des aktuellen minimalen Elements16 for( int j = i+1; j < a.length; j++ )17 if( a[ j ].compareTo( a[ min ] ) < 0 )18 min = j;19 // . . . <strong>und</strong> mit a[i] vertauschen:20 swap( a, min, i );21 }22 }23


5.4. SORTIERVERFAHREN IM VERGLEICH 19924 /** Hilfsmethode zur Vertauschung der Array-Elemente a[i] <strong>und</strong> a[j].25 * Wir setzen 0 0 && tmp.compareTo( a[ j−1 ] ) < 0; j−− )39 a[ j ] = a[ j−1 ];40 a[ j ] = tmp;41 }42 }4344 /** Implementiert den Algorithmus INSERTION SORT als Hilfmethode fuer45 * QUICK SORT auf dem Teil-Array a[left] bis a[right].46 */47 public static void insertionSort( Comparable[ ] a, int left, int right ) { insertionSort48 for( int i = left+1; i left && tmp.compareTo( a[ j−1 ] ) < 0; j−− )52 a[ j ] = a[ j−1 ];53 a[ j ] = tmp;54 }55 }5657 /** Implementiert den Algorithmus INSERTION SORT mit Binaersuche. */58 public static void insertionSortBinarySearch( Comparable[ ] a ) { insertionSortBinarySearch59 for( int i = 1; i < a.length; i++ ) {60 Comparable tmp = a[ i ];61 // Position fuer a[i] im Bereich a[0] bis a[i-1] binaer suchen:62 int left = 0;63 int right = i−1;64 while( left 0 )69 left = mid + 1;70 else {71 left = mid;72 break;73 }74 }


200 KAPITEL 5. SORTIERALGORITHMEN75 // Dann a[i] an Position left bringen:76 for( int j = i; j > left; j−− )77 a[ j ] = a[ j−1 ];78 a[ left ] = tmp;79 }80 }8182 /** Implementiert den Algorithmus MERGE SORT. */83 public static void mergeSort( Comparable[ ] a ) { mergeSort84 mergeSort( a, new Comparable[ a.length ], 0, a.length−1 );85 }8687 /** Eine rekursive Hilfsmethode fuer die Methode mergeSort88 * sortiert den Bereich a[left] bis a[right].89 */90 private static void mergeSort( Comparable[ ] a, Comparable[ ] b, mergeSort91 int left, int right ) {92 if( left < right ) {93 int mid = ( left + right ) / 2;94 mergeSort( a, b, left, mid );95 mergeSort( a, b, mid+1, right );96 merge( a, b, left, mid, right );97 }98 }99100 /** Implementiert den Algorithmus MERGE SORT ohne Rekursion. */101 public static void mergeSortBottomUp( Comparable[ ] a ) { mergeSortBottomUp102 Comparable[ ] b = new Comparable[ a.length ];103 for( int step = 1; step < a.length; step *= 2 ) {104 // Je zwei Bereiche der Groesse step werden zusammengemischt:105 for( int left = 0; left < a.length; left += 2*step ) {106 if( left + step < a.length ) { // sind noch zwei Bereich uebrig?107 int mid = left + step − 1;108 // Der letzte Bereich muss an Position a.length-1 enden.109 if( mid + step >= a.length )110 merge( a, b, left, mid, a.length − 1 );111 else112 merge( a, b, left, mid, mid + step );113 }114 }115 }116 }117118 /** Eine Hilfsmethode fuer die Methode mergeSort.119 * Die sortierten Bereiche a[left] bis a[mid] <strong>und</strong> a[mid+1] bis a[right]120 * werden zu einem sortierten Bereich a[left] bis a[right] gemischt.121 * Im Array b wird das Ergebnis zwischengespeichert.122 */123 private static void merge( Comparable[ ] a, Comparable[ ] b, merge124 int left, int mid, int right ) {125 int i1 = left; // laeuft hoch bis mid126 int i2 = mid + 1; // laeuft hoch bis right127 int bPos = left; // kopiere nach b ab bPos


5.4. SORTIERVERFAHREN IM VERGLEICH 201128 while( i1


202 KAPITEL 5. SORTIERALGORITHMEN180 else {181 quickSortTuned( a, m + 1, right ); // die grossen Elemente sortieren182 quickSortTuned( a, left, m − 1 ); // die kleinen Elemente sortieren183 }184 }185 }186187 /** Eine Hilfsmethode fuer die Methode quickSort.188 * Gibt die Position des Schnittpunkts zurueck.189 */190 private static int partition( Comparable[ ] a, int left, int right ) { partition191 // “Median von drei”:192 int mid = ( left + right) / 2;193 if( a[ mid ].compareTo( a[ left ] ) < 0 )194 swap( a, mid, left );195 if( a[ right ].compareTo( a[ left ] ) < 0 )196 swap( a, right, left );197 if( a[ right ].compareTo( a[ mid ] ) < 0 )198 swap( a, right, mid );199 Comparable cut = a[ mid ]; // der Schnittpunkt der Partitionierung200 swap( a, mid, right−1 ); // der Schnittpunkt kommt an Position right-1201202 // Das eigentliche Partitionieren:203 int i1 = left; // laeuft hinauf204 int i2 = right−1; // laeuft hinunter205 while( i1 < i2 ) {206 do i1++;207 while( a[ i1 ].compareTo( cut ) < 0 ); // nun ist a[i1] groesser gleich cut208 do i2−−;209 while( a[ i2 ].compareTo( cut ) > 0 ); // nun ist a[i2] kleiner gleich cut210 if( i1 < i2 )211 swap( a, i1, i2 );212 }213 swap( a, i1, right − 1 ); // den Schnittpunkt wieder an seinen Platz bringen214 return i1;215 }216217 /** Implementiert den Algorithmus RADIX SORT.218 * Jede Zahl im Array a muss < 10^k sein.219 */220 public static void radixSort( Integer[ ] a, int k ) { radixSort221 LinkedList[ ] list = new LinkedList[ 10 ]; // die Listen list[0] bis list[9]222 for( int i = 0; i < 10; i++ )223 list[ i ] = new LinkedList( );224 LinkedList tmpList = new LinkedList( ); // die Hilfsliste225 for( int i = 0; i < a.length; i++ )226 tmpList.addLast( a[ i ] );227228 for( int i = 0, mult = 1; i < k; i++ ) {229 while( !tmpList.isEmpty( ) ) {230 Integer p = (Integer)tmpList.removeFirst( );231 list[ ( p.intValue( ) / mult ) % 10 ].addLast( p );232 }


5.4. SORTIERVERFAHREN IM VERGLEICH 203233 for( int j = 0; j < 10; j++ ) {234 tmpList.addAll( list[ j ] ); // list[j] an tmpList anhaengen235 list[ j ].clear( ); // list[j] leer machen236 }237 mult *= 10;238 }239 // tmpList enthaelt jetzt alle Elemente von a in sortierter Folge240 tmpList.toArray( a );241 }242243 /** Testet, ob zwei Arrays identisch sind, d.h. gleich lang sind <strong>und</strong> an244 * gleichen Positionen Elemente enthalten, die true beim Vergleich mit == liefern.245 */246 public static boolean identical( Object[ ] a, Object[ ] b ) { identical247 if( a.length != b.length )248 return false;249 for( int i = 0; i < a.length; i++ )250 if( a[ i ] != b[ i ] )251 return false;252 return true;253 }254255 public static void main( String[ ] args ) { main256257 final int n = 20;258 final int max = 100;259 // Erzeuge ein Array der Laenge n, das zufaellige ganzzahlige Werte260 // gleichverteilt aus dem Bereich [0, max) enthaelt:261 Integer[ ] a = new Integer[ n ];262 for( int i = 0; i < n; i++ )263 a[ i ] = new Integer( (int)( Math.random( ) * max ) );264 System.out.println( "Das unsortierte Array:\n" + Arrays.asList( a ) );265 System.out.println( );266267 // Teste die verschiedenen Sortieralgorithmen:268 Integer[ ] a1 = (Integer[ ])a.clone( );269 Integer[ ] a2 = (Integer[ ])a.clone( );270 Integer[ ] a3 = (Integer[ ])a.clone( );271 Integer[ ] a4 = (Integer[ ])a.clone( );272 Integer[ ] a5 = (Integer[ ])a.clone( );273 Integer[ ] a6 = (Integer[ ])a.clone( );274 Integer[ ] a7 = (Integer[ ])a.clone( );275 Integer[ ] a8 = (Integer[ ])a.clone( );276 Integer[ ] a9 = (Integer[ ])a.clone( );277278 long time0 = System.currentTimeMillis( );279 selectionSort( a1 );280 long time1 = System.currentTimeMillis( );281 insertionSort( a2 );282 long time2 = System.currentTimeMillis( );283 insertionSortBinarySearch( a3 );284 long time3 = System.currentTimeMillis( );285 mergeSort( a4 );


204 KAPITEL 5. SORTIERALGORITHMEN286 long time4 = System.currentTimeMillis( );287 mergeSortBottomUp( a5 );288 long time5 = System.currentTimeMillis( );289 a6 = (Integer[ ]) new VollstaendigerBinaererBaum( a ).heapSort( );290 long time6 = System.currentTimeMillis( );291 quickSort( a7 );292 long time7 = System.currentTimeMillis( );293 quickSortTuned( a8 );294 long time8 = System.currentTimeMillis( );295 radixSort( a9, Integer.toString( max − 1 ).length( ) );296 long time9 = System.currentTimeMillis( );297298 System.out.println( "Das mit selectionSort sortierte Array:\n" +299 Arrays.asList( a1 ) );300 System.out.println( "Das mit insertionSort sortierte Array:\n" +301 Arrays.asList( a2 ) );302 System.out.println( "Das mit insertionSortBinarySearch sortierte Array:\n" +303 Arrays.asList( a3 ) );304 System.out.println( "Das mit mergeSort sortierte Array:\n" +305 Arrays.asList( a4 ) );306 System.out.println( "Das mit mergeSortBottomUp sortierte Array:\n" +307 Arrays.asList( a5 ) );308 System.out.println( "Das mit heapSort sortierte Array:\n" +309 Arrays.asList( a6 ) );310 System.out.println( "Das mit quickSort sortierte Array:\n" +311 Arrays.asList( a7 ) );312 System.out.println( "Das mit quickSortTuned sortierte Array:\n" +313 Arrays.asList( a8 ) );314 System.out.println( "Das mit radixSort sortierte Array:\n" +315 Arrays.asList( a9 ) );316 System.out.println( );317318 // Laufzeiten:319320 System.out.println( "Laufzeiten in Millisek<strong>und</strong>en:" );321 System.out.println( "selectionSort:\t\t\t" +322 (time1 − time0) + " ms" );323 System.out.println( "insertionSort:\t\t\t" +324 (time2 − time1) + " ms" );325 System.out.println( "insertionSortBinarySearch:\t" +326 (time3 − time2) + " ms" );327 System.out.println( "mergeSort:\t\t\t" +328 (time4 − time3) + " ms" );329 System.out.println( "mergeSortBottomUp:\t\t" +330 (time5 − time4) + " ms" );331 System.out.println( "heapSort:\t\t\t" +332 (time6 − time5) + " ms" );333 System.out.println( "quickSort:\t\t\t" +334 (time7 − time6) + " ms" );335 System.out.println( "quickSortTuned:\t\t\t" +336 (time8 − time7) + " ms" );337 System.out.println( "radixSort:\t\t\t" +338 (time9 − time8) + " ms" );339 System.out.println( );340


5.4. SORTIERVERFAHREN IM VERGLEICH 205341 // Test, ob alle Sortierverfahren identische Resultate geliefert haben:342 System.out.println( "Alle <strong>Algorithmen</strong> liefern sortierte Folgen: " +343 ( Arrays.equals( a1, a2 ) && Arrays.equals( a1, a3 ) &&344 Arrays.equals( a1, a4 ) && Arrays.equals( a1, a5 ) &&345 Arrays.equals( a1, a6 ) && Arrays.equals( a1, a7 ) &&346 Arrays.equals( a1, a8 ) && Arrays.equals( a1, a9 ) ) );347 System.out.println( );348349 // Test, ob Stabilitaet verletzt ist:350 System.out.println( "Test auf Stabilitaet am Beispiel:" );351 // Wir wissen, dass insertionSort stabil ist, also dient a2 als Referenzarray:352 System.out.println( "selectionSort:\t\t\t" +353 ( identical( a2, a1 ) ? "+" : "-" ) );354 System.out.println( "insertionSort:\t\t\t" + "+ (immer +)" );355 System.out.println( "insertionSortBinarySearch:\t" +356 ( identical( a2, a3 ) ? "+" : "-" ) );357 System.out.println( "mergeSort:\t\t\t" +358 ( identical( a2, a4 ) ? "+" : "-" ) + " (immer +)" );359 System.out.println( "mergeSortBottomUp:\t\t" +360 ( identical( a2, a5 ) ? "+" : "-" ) + " (immer +)" );361 System.out.println( "heapSort:\t\t\t" +362 ( identical( a2, a6 ) ? "+" : "-" ) );363 System.out.println( "quickSort:\t\t\t" +364 ( identical( a2, a7 ) ? "+" : "-" ) );365 System.out.println( "quickSortTuned:\t\t\t" +366 ( identical( a2, a8 ) ? "+" : "-" ) );367 System.out.println( "radixSort:\t\t\t" +368 ( identical( a2, a9 ) ? "+" : "-" ) + " (immer +)" );369 }370371 } // class Sort


206 KAPITEL 5. SORTIERALGORITHMEN


LiteraturA. Aho, J. Hopcroft, J. Ullman, The Design and Analysis of ComputerAlgorithms. Addison-Wesley, 1974K. Arnold, J. Gosling, D. Holmes, Die Programmiersprache Java.Addison-Wesley, 2001R.H. Güting, <strong>Datenstrukturen</strong> <strong>und</strong> <strong>Algorithmen</strong>. Teubner, 1992E. Horowitz, S. Sahni, F<strong>und</strong>amentals of Data Structures in PASCAL.3 rd Edition, Computer Science Press, 1990K. Mehlhorn, Data Structures and Algorithms. Band 1 <strong>und</strong> 2, Springer, 1984<strong>Datenstrukturen</strong> <strong>und</strong> <strong>Algorithmen</strong>. Band 1 <strong>und</strong> 2, Teubner, 1986T. Ottmann, P. Widmayer, <strong>Algorithmen</strong> <strong>und</strong> <strong>Datenstrukturen</strong>.BI-Wissenschaftsverlag, 1990U. Schöning, <strong>Algorithmen</strong> – kurz gefaßt.Spektrum Akademischer Verlag, 1997R. Sedgewick, <strong>Algorithmen</strong>. 2. Auflage, Addison-Wesley, 2002R. Sedgewick, <strong>Algorithmen</strong> in C++, Teil 1-4.3. Auflage, Addison-Wesley, 2002M.A. Weiss, Data Structures & Algorithm Analysis in Java.Addison-Wesley, 1999M.A. Weiss, Data Structures & Problem Solving using Java.2 nd Edition, Addison-Wesley, 2002207

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!