Algorithmen und Datenstrukturen 2. Datenstrukturen und Algorithmen in C++ und Java Datenstrukturen und Algorithmen sind eng miteinander verbunden. Die Wahl der richtigen Datenstruktur entscheidet über effiziente Laufzeiten. Beide erfüllen nur alleine ihren Zweck. Leider ist die Wahl der richtigen Datenstruktur nicht so einfach. Eine Rehe von schwierigen Problemen in der Informatik wurden deshalb noch nicht gelöst, da eine passende Datenorganisation bis heute noch nicht gefunden wurde. Wichtige Datenstrukturen (Behälterklassen, Collection, Container) werden in C++ und Java bereitgestellt. Auch Algorithmen befinden sich in diesen „Sammlungen“. Sammlungen (Kollektionen) sind geeignete Datenstrukturen (Behälter) zum Aufbewahren von Daten. Durch ihre vielfältigen Ausprägungen können sie zur Lösung unterschiedlicher Aufgaben herangezogen werden. Ein Lösungsweg umfaßt dann das Erzeugen eines solchen Behälters, das Einfügen, Modifizieren und Löschen der Datenelemente. Natürlich steht dabei im Mittelpunkt der Zugriff auf die Datenelemente, das Lesen der Dateninformationen und die aus diesen Informationen resultierenden Schlußfolgerungen. Verallgemeinert bedeutet dies: Das Suchen nach bestimmten Datenwerten, die in den Datenelementen der Datensammlung (Kollektion) gespeichert sind. Suchmethoden bestehen aus einer Reihe bestimmter Operationen. Im wesentlichen sind dies - Das Initialisieren der Kollektion ( die Bildung einer Instanz mit der Datenstruktur, auf der die Suche erfolgen soll - das Suchen eines Datenelements (z.B. eines Datensatzes oder mehrerer Datensätze)in der Datensammlung mit einem gegebenen Kriterium (z.B. einem identifizierenden Schlüssel) - das Einfügen eines neuen Datenelements. Bevor eingefügt werden kann, muß festgestellt werden, ob das einzufügende Element in der Kollektion schon vorliegt. - das Löschen eines Datenelements. Ein Datenelement kann nur dann gelöscht werden, falls das Element in der Kollektion vorliegt. Häufig werden Suchvorgänge in bestimmten Kollektionen (Tabellen) benötigt, die Daten über identifizierende Kriterien (Schlüssel) verwalten. Solche Tabellen können als Wörterbücher (dictionary) oder Symboltabellen implementiert sein. In einem Wörterbuch sind bspw. die „Schlüssel“ Wörter der deutschen Sprache und die Datensätze, die zu den Wörtern gehörenden Erläuterungen über Definition, Aussprache usw. Eine Symboltabelle beschreibt die in einem Programm verwendeten Wörter (symbolische Namen). Die Datensätze sind die Deklarationen bzw. Anweisungen des Programms. Für solche Anwendungen sind nur zwei weitere zusätzliche Operationen interessant: - Verbinden (Zusammenfügen) von Kollektionen, z.B. von zwei Wörterbüchern zu einem großen Wörterbuch - Sortieren von Sammlungen, z.B. des Wörterbuchs nach dem Schlüssel Die Wahl einer geeigneten Datenstruktur (Behälterklasse) ist der erste Schritt. Im zweiten Schritt müssen die Algorithmen implementiert werden. Die StandardBibliotheken der Programmiersprachen C++ und Java bieten Standardalgorithmen an. 1 Algorithmen und Datenstrukturen 2.1 Datenstrukturen und Algorithmen in C++ 2 Algorithmen und Datenstrukturen 2.2 Datenstrukturen und Algorithmen in Java 2.2.1 Durchwandern von Daten mit Iteratoren Bei Datenstrukturen gibt es eine Möglichkeit, gespeicherte Daten unabhängig von der Implementierung immer mit der gleichen Technik abzufragen. Bei Datenstrukturen handelt es sich meistens um Daten in Listen, Bäumen oder ähnlichem und oft wird nur die Frage nach der Zugehörigkeit eines Worts zum Datenbestand gestellt (z.B. „Gehört das Wort dazu?“). Auch die Möglichkeit Daten in irgendeiner Form aufzuzählen, ist eine häufig gestellte Aufgabe. Hierfür bieten sich Iteratoren an. In Java umfaßt das Interface Enumeration die beiden Funktionen hasMoreElements() und nextElement(), mit denen durch eine Datenstruktur iteriert werden kann. public interface Enumeration { public boolean hasMoreElements(); // Test, ob noch ein weiteres Element aufgezählt werden kann public Object nextElement() throws NoSuchElementException; /* setzt den internen Zeiger auf das nächste Element, d. h. liefert das das nächste Element der Enumertion zurück. Diese Funktion kann eine NoSuchException auslösen, wenn nextElement() aufgerufen wird, obwohl hasMoreElements() unwahr ist */ } Die Aufzählung erfolgt meistens über for (Enumeration e = ds.elements(); e.hasMoreElements(); ) System.out.println(e.nextElements()); Die Datenstruktur ds besitzt eine Methode elements(), die ein Enumeration-Objekt zurückgibt, das die Aufzählung erlaubt. 2.2.2 Die Klasse Vector class java.util.Vector extends AbstractList implements List, Cloneable, Serializable Die Klasse Vector beschreibt ein Array mit variabler Länge. Objekte der Klasse Vector sind Repräsentationen einer linearen Liste. Die Liste kann Elemente beliebigen Typs enthalten, ihre Länge ist zur Laufzeit veränderbar (Array mit variabler Länge). Vector erlaubt das Einfügen von Elementen an beliebiger Stelle, bietet sequentiellen und wahlfreien Zugriff auf die Elemente. Das JDK realisiert Vector als Array von Elementen des Typs Object. Der Zugriff auf Elemente erfolgt über Indizes. Es wird dazu aber kein Operator [], sondern es werden Methoden benutzt, die einen Index als Parameter annehmen. Anlegen eines neuen Vektors (Konstruktor): public Vector() public Vector(int initialCapacity,int capacityIncrement) // Ein Vector vergrößert sich automatisch, falls mehr Elemente aufgenommen werden, als // ursprünglich vorgesehen (Resizing). Dabei sollen initialCapacity und capacityIncrement // passend gewählt werden. 3 Algorithmen und Datenstrukturen Einfügen von Elementen: public void addElement(Object obj) // Anhängen an des Ende der bisher vorliegenden Liste von Elementen Eigenschaften: public final boolean isEmpty() // Prüfen, ob der Vektor leer ist public final int size() // bestimmt die Anzahl der Elemente public final int capacity() // bestimmt die interne Größe des Arrays. Sie kann mit ensureCapacity() geändert // werden Einfügen an beliebiger Stelle innerhalb der Liste: public void insertElementAt(Object obj, int index) throws ArrayIndexOutOfBoundsException // fügt obj an die Position index in den „Vector“ ein. Zugriff auf Elemente: Für den sequentiellen Zugriff steht ein Iterator zur Verfügung. Wahlfreier Zugriff erfolgt über: public Object firstElement() throws ArrayIndexOutOfBoundException; public Object lastElement() throws ArrayIndexOutOfBoundException; public Object elementAt(int index) throws ArrayIndexOutOfBoundException; firstElement() liefert das erste, lastElement() das letzte Element- Mit elementAt() wird auf das Element an der Position index zugegriffen. Alle 3 Methoden verursachen eine Ausnahme, wenn das gewünschte Element nicht vorhanden ist. Arbeitsweise des internen Arrays. Der Vector vergrößert sich automatisch, falls mehr Elemente aufgenommen werden. Die Operation heißt Resizing. Die Größe des Felds. Mit capacity() erhält man die interne Größe des Arrays. Sie kann mit ensureCapacity() geändert werden. ensureCapacity(int minimumCapacity) bewirkt bei einem Vector, daß er mindestens minCapacity Elemente aufnehmen soll. Der Vektor verkleinert nicht die aktuelle Kapazität, falls sie schon höher als minCapacity ist. Zur Veränderung dieser Größe, dient die Methode trimToSize(). Sie reduziert die Kapazität des Vectors auf die Anzahl der Elemente, die gerade im Vector sind. Die Anzahl der Elemente kann über die Methode size() erfragt werden. Sie kann über setSize(int newSize) geändert werden. Ist die neue Größe kleiner als die alte, so werden die Elemente am Ende des Vectors abgeschnitten. Ist newSize größer als die alte Größe, werden die neu angelegten Elemente mit null initialisiert. Bereitstellen des Interface Enumerartion. In der Klasse Vector liefert die Methode public Enumeration elements() einen Enumerator (Iterator) für alle Elemente, die sich in Vector befinden. 4 Algorithmen und Datenstrukturen Vector << Konstruktoren >> public Vector() // Ein Vector in der Anfangsgröße von 10 Elementen wird angelegt public Vector(int startKapazitaet) // Ein Vector enthält Platz für startKapazitaet Elemente public Vector(int startKapazitaet, int kapazitaetsSchrittweite) << Methoden >> public final synchronized Object elementAt(int index) // Das an der Stelle index befindliche Objekt wird zurückgegeben public final int size() public final synchronized Object firstElement() public final synchronized Object lastElement(); public final synchronized void insertElementAt(Object obj, int index) // fügt Object obj an index ein und verschiebt die anderen Elemente public final synchronized void setElementAt(Object obj, int index) public final synchronized copyInto(Object einArray[]) // kopiert die Elemente des Vektors in das Array einArray // Falls das bereitgestellte Objektfeld nicht so groß ist wie der Vektor, // dann tritt eine ArrayIndexOutOfBounds Exception public final boolean contains(Object obj) // sucht das Element, liefert true zurück wenn o im Vector vorkommt public final int indexOf(Object obj) // sucht im Vector nach dem Objekt obj. Falls obj nicht in der Liste ist, wird // -1 übergeben public final int lastIndexOf(Object obj) public final synchronized boolean removeElement(Object obj) // entfernt obj aus der Liste. Konnte es entfernt werden, wird true // zurückgeliefert public final synchronized void removeElementAt(int index) // entfernt das Element an Stelle index public final synchronized void removeAllElements() // löscht alle Elemente public final int capacity() // gibt an, wieviel Elemente im Vektor Patz haben // (, ohne daßautomatische Größenanpassung erfolgt) public synchronized Object clone() // Implementierung der clone()-methode von Object, d.h. eine Referenz // das kopierte Feld wird zurückgegeben. Die Kopie ist flach. public final synchronized String toString() Abb.: Die Klasse Vector 5 Algorithmen und Datenstrukturen 2.2.3 Die Klasse Stack class java.util.Stack extends Vector Ein Stack ist eine nach dem LIFO-Prinzip arbeitende Datenstruktur. Elemente werden vorn (am vorderen Ende der Liste) eingefügt und von dort auch wieder entnommen. In Java ist ein Stack eine Ableitung von Vector mit neuen Zugriffsfunktionen für die Implementierung des typischen Verhaltens von einem Stack. Konstruktor: public Stack(); Hinzufügen neuer Elemente: public Object push(Object item); Zugriff auf das oberste Element: public Object pop(); // Zugriff und Entfernen des obersten Element public Object peek() // Zugriff auf das oberste Element Suche im Stack: public int search(Object o) // Suche nach beliebigem Element, // Rueckgabewert: Distanz zwischen gefundenem und // obersten Stack-Element bzw. –1, // falls das Element nicht da ist. Test: public boolean empty() // bestimmt, ob der Stack leer ist Vector Stack public Stack() public Object push(Object obj) public synchronized Object pop() public synchronized Object peek() public synchronized int search(Object obj) public boolean empty() Abb.: Die Klasse Stack Anwendungen: 1. Umrechnen von Dezimalzahlen in andere Basisdarstellungen Aufgabenstellung: Defaultmäßig werden Zahlen dezimal ausgegeben. Ein Stapel, der Ganzzahlen aufnimmt, kann dazu verwendet werden, Zahlen bezogen auf eine andere Basis als 10 darzustellen. Die Funktionsweise der Umrechnung von Dezimalzahlen in eine Basis eines anderen Zahlensystem zeigen die folgenden Beispiele: 6 Algorithmen und Datenstrukturen 2810 3 8 4 34 8 72 10 1 64 0 16 2 4 0 1020 4 5310 1 32 1 16 0 8 1 4 0 2 1 1101012 Mit einem Stapel läßt sich die Umrechnung folgendermaßen unterstützen: 6 leerer Stapel n 355310 7 7 4 4 4 1 1 1 1 n%8=1 n/8=444 n%8=4 n/8=55 n 444 10 n%8=7 n/8=6 n 5510 n 610 n%8=6 n/6=0 n 010 Abb.: Umrechnung von 355310 in 67418 mit Hilfe eines Stapel Algorithmus zur Lösung der Aufgabe: 1) Die am weitesten rechts stehende Ziffer von n ist n%b. Sie ist auf dem Stapel abzulegen. 2) Die restlichen Ziffern von n sind bestimmt durch n/b. Die Zahl n wird ersetzt durch n/b. 3) Wiederhole die Arbeitsschritte 1) und 2) bis keine signifikanten Ziffern mehr übrig bleiben. 4) Die Darstellung der Zahl in der neuen Basis ist aus dem Stapel abzulesen. Der Stapel ist zu diesem Zweck zu entleeren. Implementierung: Das folgende kleine Testprogramm1 realisiert den Algorithmus und benutzt dazu eine Instanz von Stack. import java.util.*; public class PR61210 { public static void main(String[] args) { int zahl = 3553; // Dezimalzahl int b = 8; // Basis Stack s = new Stack(); // Stapel do { s.push(new Integer(zahl % b)); zahl /= b; } while (zahl != 0); while (!s.empty()) { System.out.print(s.pop()); } System.out.println(); } } 1 pr61210 7 Algorithmen und Datenstrukturen Ein Stack ist ein Vector. Die Vector-Klasse wird von der Klasse Stack erweitert. Das ist sicherlich nicht immer besonders sinnvoll. Funktionen, die im Gegensatz zur Leistungsfähigkeit eines Stapels stehen sind add(), addAll(), addElement(), capacity(), clear(), clone(), contains(), copyInto(), elementsAt(), .... . 2.2.4 Die Klasse Bitset für Bitmengen class java.util.BitSet implements Cloneable, Serializable Die Klasse Bitset bietet komfortable Möglichkeiten zur bitweisen Manipulation von Daten. Bitset anlegen und füllen. Mit zwei Methoden lassen sich die Bits des Bitsets leicht ändern: set(bitNummer) und clear(bitNummer). Mengenorintierte Operationen. Das Bitset erlaubt mengenorientierte Operationen mit einer weiteren Menge. BitSet public void and(BitSet bs) public void or(BitSet bs) public void xor(BitSet bs) public void andNot(Bitset set) // löscht alle Bits im Bitset, dessen Bit in set gesetzt sind public void clear(int index) // Löscht ein Bit. Ist der Index negativ, kommt es // zur Auslösung von IndexOutOfBoundsException public void set(int index) // Setzt ein Bit. Ist der Index negativ, kommt es // zur Auslösung von IndexOutOfBoundsException public boolean get(int index) // liefert den Wert des Felds am übergebenen Index, // kann IndexOutOfBoundsException auslösen. public int size() public boolean equals(Object o) // Vergleicht sich mit einem anderen Bitset-Objekt o. Abb.: Die Klasse BitSet 8 Algorithmen und Datenstrukturen 2.2.5 Die Klasse Hashtable und assoziative Speicher Eine Hashtabelle (Hashtable) ist ein assoziativer Speicher, der Schlüssel (keys) mit Werten verknüpft. Die Datenstruktur ist mit einem Wörterbuch vergleichbar. Die Hashtabelle arbeitet mit Schlüssel/Werte Paaren. Aus dem Schlüssel wird nach einer Funktion – der sog. Hashfunktion – ein Hashcode berechnet. Dieser dient als Index für ein internes Array. Dieses Array hat zu Anfang ein feste Grösse. Leider hat dieses Technik einen entscheidenden Nachteil. Besitzen zwei Wörter denselben Hashcode, dann kommt es zu einer Kollision. Auf ihn muß die Datenstruktur vorbereitet sein. Hier gibt es verschiedene Lösungsansätze. Die unter Java implementierte Variante benutzt eine verkettete Liste (separate Chaining). Falls eine Kollision auftritt, so wird der Hashcode beibehalten und der Schlüssel bzw. Wert in einem Listenelement an den vorhandenen Eintrag angehängt. Wenn allerdings irgendwann einmal eine Liste durchsucht werden muß, dann wird die Datenstruktur langsam. Ein Maß für den Füllgrad ist der Füllfaktor (Load Factor). Dieser liegt zwischen 0 und 100 %. 0 bedeutet: kein Listenelement wird verwendet. 100 % bedeutet: Es ist kein Platz mehr im Array und es werden nur noch Listen für alle zukommenden Werte erweitert. Der Füllfaktor sollte für effiziente Anwendungen nicht höher als 75% sein. Ist ein Füllfaktor nicht explizit angegeben, dann wird die Hashtabelle „rehashed“, wenn mehr als 75% aller Plätze besetzt sind. class java.util.Hashtable extends Dictionary implements Map, Cloneable, Serializable Erzeugen von einem Objekt der Klasse HashTable: public Hashtable() /* Die Hashtabelle enthält eine Kapazität von 11 Einträgen und einen Füllfaktor von 75 % */ public HashTable(int initialCapacity) /* erzeugt eine Hashtabelle mit einer vorgegebenen Kapazität und dem Füllfaktor 0.75 */ public HashTable(int initialCapacity, float loadFactor) /* erzeugt eine Hashtabelle mit einer vorgebenen Kapazität und dem angegebenen Füllfaktor */ Daten einfügen: public Object put(Object key, Object value) /* speichert den Schlüssel und den Wert in der Hashtabelle. Falls sich zu dem Schlüssel schon ein Eintrag in der Hashtabelle befand, so wird dieser zurückgegeben. Anderenfalls ist der Rückgabewert null. Die Methode ist vorgegeben vom Interface Map. Es überschreibt die Methode von der Superklasse Dictionary. */ Daten holen: public Object get(Object key) Schlüssel entfernen. public Object remove(Object key) Löschen der Werte. public void clear() Test. public boolean containsKey(Object key) // Test auf einen bestimmten Schlüssel public boolean containsValue(Object value) // Test auf einen bestimmten Wert Aufzählen der Elemente. Mit keys() und elements() bietet die Hashtabelle zwei Methoden an, die eine Aufzählung zurückgeben: public Enumeration keys() // liefert eine Aufzählung aller Schlüssel, überschreibt keys() in Dictionary. public Enumeration elements() // liefert eine Aufzählung der Werte, überschreibt elements() in Dictionary 9 Algorithmen und Datenstrukturen Wie üblich liefern beide Iteratoren ein Objekt, welches das Interface Enumeration implementiert. Der Zugriff erfolgt daher mit Hilfe der Methoden hasMoreElements() und nextElement(). Dictionary {abstract} public abstract Object put (Object key, Object value) publicc abstract Object get(Object key) public abstract Enumeration elements() public abstract Enumeration keys() public abstract int size() public abstract boolean isEmpty() public abstract Object remove(Object key) Hashtable Map << Konstruktor >> public Hashtable(int initialKapazitaet) public Hashtable(int initialKapazitaet, float Ladefaktor) public Hashtable() << Methoden >> public synchronized boolean contains(Object wert) public synchronized boolean containsKey(Object key) public synchronized void clear() public synchronized Object clone() protected void rehash() public synchronized String toString() Properties << Konstruktor >> public Properties() // legt einen leeren Container an public Properties(Properties defaults) // füllt eine Property-Liste mit den angegebenen Default-Werten << Methoden >> public String getProperty(String key) public String getProperty(String key, String defaultKey) public synchronized void load(InputStream in) throws IOException // Hier muß ein InputStream übergeben werden, der die daten der // Property-Liste zur Verfügung stellt. public synchronizred void save(OutputStream out, String header) public void list(PrintStream out) public void list(PrintWriter out) public Enumeration propertyNames() // beschafft ein Enumerations-Objekt mit denen Eigenschaften // der Property-Liste aufgezählt werden können Abb.: Die Klassen Hashtable und Properties 10 Algorithmen und Datenstrukturen Die Klasse Hashtable ist eine Konkretisierung der abstrakten Klasse Dictionary. Diese Klasse beschreibt einen assoziativen Speicher, der Schlüssel auf Werte abbildet und über den Schlüsselbegriff einen effizienten Zugriff auf den Wert ermöglicht. Einfügen und der Zugriff auf Schlüssel erfolgt nicht auf der Basis des Operators „==“, sondern mit Hilfe der Methode „equals“. Schlüssel müssen daher lediglich inhaltlich gleich sein, um als identisch angesehen zu werden. Bsp.2: Hashtabelle zum Test der von Zufallszahlen der Klasse Math.random. import java.util.*; class Zaehler { int i = 1; public String toString() { return Integer.toString(i); } } public class Statistik { public static void main(String args[]) { Hashtable h = new Hashtable(); for (int i = 0; i < 10000; i++) { // Erzeuge eine Zahl zwischen 0 und 20 Integer r = new Integer((int)(Math.random() * 20)); if (h.containsKey(r)) ((Zaehler) h.get(r)).i++; else h.put(r,new Zaehler()); } System.out.println(h); } } Die Klasse Hashtable benutzt das Verfahren der Schlüsseltransformation (HashFunktion) zur Abbildung von Schlüsseln auf Indexpostionen eines Arrays. Die Kapazität der Hash-Tabelle gibt die Anzahl der Elemente an, die insgesamt untergebracht werden können. Der Ladefaktor zeigt an, bei welchem Füllungsgrad die Hash-Tabelle vergrößert werden muß. Das Vergrößern erfolgt automatisch, falls die Anzahl der Elemente innerhalb der Tabelle größer ist als das Produkt aus Kapazität und Ladefaktor. Seit dem JDK 1.2 darf der Ladefaktor auch größer als 1 sein. In diesem Fall wird die Hash-Tabelle erst dann vergrößert, wenn der Füllungsgrad größer als 100% ist und bereits ein Teil der Elemente in den Überlaufbereichen untergebracht wurde. Die Klasse Hashtable ist eine besondere Klasse für Wörterbücher. Ein Wörterbuch ist eine Datenstruktur, die Elemente miteinander assoziiert. Das Wörterbuchproblem ist das Problem, wie aus dem Schlüssel möglichst schnell der zugehörige Wert konstruiert wird. Die Lösung des Problems ist: Der Schlüssel wird als Zahl kodiert (Hashcode) und dient in einem Array als Index. An einem Index hängen dann noch die Werte mit gleichem Hashcode als Liste an. 2 vgl. pr13215 11 Algorithmen und Datenstrukturen 2.2.6 Die abstrakte Klasse Dictionary Die Klasse Dictionary ist eine abstrakte Klasse, die Methoden anbietet, wie Objekte (also Schlüssel und Wert) miteinander assoziiert werden: public abstract Object put(Object key,Object value) // fügt den Schlüssel key mit dem verbundenen Wert value in das Wörterbuch // ein public abstract Object get(Object key) // // // // liefert das zu key gehörende Objekt zurück. Falls kein Wert mit dem Schlüssel verbunden ist, so liefert get() eine null. Eine null als Schlüssel oder Wert kann nicht eingesetz werden. In put() würde das zu einer NullPointerException führen. public abstract Object remove(Object key) // entfernt ein Schlüssel/Wertepaar aus dem Wörterbuch. Zurückgegeben wird // der assoziierte Wert. public abstract boolean isEmpty() // true, falls keine Werte im Wörterbuch public int size() gibt zurück, wie viele Elemente aktuell im Wörterbuch sind. public abstract Enumeration keys() // liefert eine Enumeration für alle Schlüssel public abstract Enumeration elements() // liefert eine Enumeration über alle Werte. 2.2.7 Die Klasse Properties Die Properties Klasse ist eine Erweiterung von Hashtable. Ein Properties Objekt erweitert die Hashtable um die Möglichkeit, sich unter einem wohldefinierten Format über einen Strom zu laden und zu speichern. Erzeugen. public Properties() // erzeugt ein leeres Propertes Objekt ohne Worte. public Properties(Properties p) // erzeugt ein leeres Properties Objekt mit Standard-werten aus den // übergebenen Properties Die Methode getProperty(). public String getProperty(String s) // sucht in den Properties nach der Zeichenkette public String getProperty(String key, String default) // sucht in den Properties nach der Zeichenkette key. Ist dieser nicht // vorhanden, wird der String default zurückgegeben Eigenschaften ausgeben. Die Methode list() wandert durch die Daten und gibt sie auf einem PrintWriter aus: public void list(PrintWriter pw) // listet die Properties auf dem PrintWriter aus. Bsp.: Systemeigenschaften der Javaumgebung 12 Algorithmen und Datenstrukturen 2.2.8 Collection API Die Java 2 Plattform hat Java erweitert um das Collection API. Anstatt Collection kann man auch Container (Behälter) sagen. Ein Container ist ein Objekt, das wiederum Objekte aufnimmt und die Verantwortung für die Elemente übernimmt. Im „util“-Paket befinden sich sechs Schnittstellen, die grundlegende Eigenschaften der Containerklassen definieren. Das in Java 1.2 enthaltene Collections Framework beinhaltet im Wesentlichen drei Grundformen: Set, List und Map. Jede dieser Grundformen ist als Interface implementiert. Die Interfaces List und Set sind direkt aus Collection abgeleitet. Es gibt auch noch eine abstrakte Implementierung des Interface, mit dessen Hilfe das Erstellen eigener Collections erleichtert wird. Bei allen Collections, die das Interface Collection implementieren, kann ein Iterator zum Durchlaufen der Elemente mit der Methode „iterator()“ beschafft werden. Zusätzlich fordert die JDK 1.2-Spezifikation für jede Collection-Klasse zwei Konstruktoren: - Einen parameterlosen Konstruktor zum Anlegen einer neuen Collection. - Ein mit einem einzigen Collection-Argument ausgestatteter Konstruktor, der eine neue Collection anlegt und mit den Elementen der als Argument übergebenen Collection auffüllt. 2.2.8.1 Die Schnittstellen Collection, Iterator, Comparator Das Interface Collection bildet die Basis der Collection-Klasse und –Interfaces des JDK 1.2. Alle Behälterklassen implementieren das Collection Interface und geben den Klassen damit einen äußeren Rahmen. 13 Algorithmen und Datenstrukturen Das Interface Collection << interface >> Collection public void clear(); // Optional: Löscht alle Elemente in dem Container. Wird dies vom Container nicht unterstützt, // kommt es zur UnSupportedOperationException public boolean add(Object o); // Optional: Fügt ein Objekt dem Container hinzu und gibt true zurück, falls sich das Element // einfügen läßt. Gibt false zurück, falls schon ein Objektwert vorhanden ist und doppelte Werte // nicht erlaubt sind. public boolean addAll(Collection c); // fügt alle Elemente der Collection c dem Container hinzu public boolean remove(Object o); // Entfernen einer einzelnen Instanz. Rückgabewert ist true, wenn das Element gefunden und // entfernt werden konnte public boolean removeAll(Collection c); // Oprtional: Entfernt alle Objekte der Collection c aus dem Container public boolean contains(Object o); // liefert true, falls der Container das Element enthält // Rückgabewert ist true, falls das vorgegebene Element gefunden werden konnte public boolean containsAll(Collection c); // liefert true, falls der Container alle Elemente der Collection c enthält. public boolean equals(Object o); // vergleicht das angegebene Objekt mit dem Container, ob die gleichen Elemente vorkommen. public boolean isEmpty(); // liefert true, falls der Container keine Elemente enthält public int size(); // gibt die Größe des Containers zurück public boolean retainAll(Collection c); public Iterator iterator(); public Object [] toArray(); // gibt ein Array mit Elementen des Containers zurück public Object [] toArray(Object [] a); public int hashCode(); // liefert den Hashwert des Containers public String toString() // Rückgabewert ist die Zeichenketten-Repräsentation der Kollektion. Abb.: Das Interface Collection Die abstrakte Basisklasse AbstractCollection implementiert die Methoden des Interface Collection (ohne iterator() und size()). AbstractCollection ist die Basisklasse von AbstractList und AbstractSet. 14 Algorithmen und Datenstrukturen Das Interface Iterator << interface >> Iterator public boolean hasNext(); // gibt true zurück, wenn der Iterator mindestens ein weiteres Element enthält. public Object next(); // liefert das nächste Element bzw. löst eine Ausnahme des Typs NoSuchElementException // aus, wenn es keine weiteren Elemente gibt public void remove(); // entfernt das Element, das der Iterator bei next() geliefert hat. Abb.: Bei allen Collections, die das Interface Collection implementieren, kann ein Iterator zum Durchlaufen der Elemente mit der Methode „iterator()“ beschafft werden. Das Interface Comparator Vergleiche zwischen Objekten werden mit speziellen Objekten vorgenommen, den Comparatoren. Ein konkreter Comparator implementiert die folgende Schnittstelle. << interface >> java.util.Comparator public int compare(Object o1, Object o2) // vergleicht 2 Argumente auf ihre Ordnung public boolean equals(Object arg) // testet, ob zwei Objekte bzgl. des Comparator-Objekts gleich sind Abb. Bsp.: Konstruktion einer Klasse EvenComparator 15 Algorithmen und Datenstrukturen 2.2.8.2 Die Behälterklassen und Schnittstellen des Typs List Behälterklassen des Typs List fassen eine Menge von Elementen zusammen, auf die sequentiell oder über Index (-positionen) zugegriffen werden kann. Wie Vektoren der Klasse Vector3 hat das erste Element den Index 0 und das letzte den Index „size() – 1“. Es ist möglich an einer beliebigen Stelle ein Element einzufügen oder zu löschen. Die weiter hinten stehenden Elemente werden dann entsprechend weiter nach rechts bzw. nach links verschoben. Das Interface List4 << interface >> List public void add(int index, Object element); // Einfügen eines Elements an der durch Index spezifizierten Position public boolean add(Object o); // Anhängen eines Elements ans Ende der Liste // Rückgabewert ist true, falls die Liste durch den Aufruf von add verändert wurde. Er ist false, // wenn die Liste nicht verändert wurde. Das kann bspw. der Fall sein, wenn die Liste keine // Duplikate erlaubt und ein bereits vorhandenes Element noch einmal eingefügt werden soll. // Konnte das Element aus einem anderen Grund nicht eingefügt werden, wird eine Ausnahme // des Typs UnsupportedOperationException, CallsCastException oder IllegalArgumentException // ausgelöst public boolean addAll(Collection c); // Einfügen einer vollständigen Collection in die Liste. Der Rückgabewert ist true, falls die Liste // durch den Aufruf von add verändert wurde public boolean addAll(int index, Collection c) public void clear(); public boolean equals(Object object); public boolean contains(Object element); public boolean containsAll(Collection collection); public Object remove(int index) public boolean remove(Object element); public boolean removeAll(Collection c); // Alle Elemente werden gelöscht, die auch in der als Argument angegebenen // Collection enthalten sind. public boolean retainAll(Collection c); // löscht alle Elemente außer den in der Argument-Collection enthaltenen public Object get(); public int hashCode(); public Iterator iterator(); public ListIterator listIterator(): public ListIterator listIterator(int startIndex); public Object set(int index, Obeject element); public List subList(int fromIndex, int toIndex); public Object [] toArray(); public Object [] toArray(Object [] a); Abb.: Auf die Elemente einer Liste läßt sich mit einem Index zugreifen und nach Elementen läßt sich mit linearem Aufwand suchen. Doppelte Elemente sind erlaubt. 3 seit Java 1.2 implementiert die Klasse Vector die Schnittstelle List Da ebenfalls das AWT-Paket eine Klasse mit gleichen namen verwendet, muß der voll qualifizierte Name in Anwendungen benutzt werden. 4 16 Algorithmen und Datenstrukturen Die Schnittstelle List, die in ArrayList und LinkedList eine Implementierung findet, erlaubt sequentiellen Zugriff auf die gespeicherten gespeicherten Elemente. Das Interface List wird im JDK von verschiedenen Klassen implementiert: AbstractList ist eine abstrakte Basisklasse (für eigene List-Implementierungen), bei der alle Methoden die Ausnahme UnsupportedException auslösen und diverse Methoden abstract deklariert sind. Die direkten Subklassen sind AbstractSequentialList, ArrayList und Vector.java.util. AbstractList implementiert bereits viele Methoden für die beiden Listen-Klassen5: abstract class AbstractList extends AbstractCollection implements List << Methoden >> public void add(int index, Object element) // Optional: Fügt ein Objekt an der spezifizierten stelle ein public boolean add(Object o) // Optional: Fügt das Element am Ende an public boolean addAll(int index, Collection c) // Optional: Fügt alle Elemente der Collection ein public void clear() // Optional: Löscht alle Elemente public boolean equals(Object o) // vergleicht die Liste mit dem Objekt public abstract Object get(int index) // liefert das Element an dieser Stelle int hashCode() // liefert HashCode der Liste int indexOf(Object o) // liefert Position des ersten Vorkommens für o oder –1, // wenn das Element nicht existiert. Iterator iterator() // liefert den Iterator. Überschreibt die Methode AbstractCollection, // obwohl es auch listIterator() für die spezielle Liste gibt. Die Methode // ruft aber listIterator() auf und gibt ein ListIterator-Objekt zurück Object remove(int index) // löscht ein Element an Position index. protected void removeRange(int fromIndex, int toIndex) // löscht Teil der Liste von fromIndex bis toIndex. fromIndex wird mitgelöscht, // toIndex nicht. public Object set(int index, Object element) // Optional. Ersetzt das Element an der Stelle index mit element. public List subList(int fromIndex, int toIndex) // liefert Teil einer Liste fromIndex (einschließlich) bis toIndex (nicht mehr dabei) Abb.: AbstractSequentialList bereitet die Klasse LinkedList darauf vor, die Elemente in einer Liste zu verwalten und nicht wie ArrayList in einem internen Array. LinkedList realisiert die doppelt verkettete, lineare Liste und implementiert List. ArrayList implementiert die Liste als Feld von Elementen und implementiert List. Da Arraylist ein Feld ist, ist der Zugriff auf ein spezielles Element sehr schnell. Eine LinkedList 5 Beim aufruf einer optionalen Methode, die von der Subklasse nicht implementiert wird, führt zur UnsupportedOperationException. 17 Algorithmen und Datenstrukturen muß aufwendiger durchsucht werden. Die verkettete Liste ist aber deutlich im Vorteil, wenn Elemente gelöscht oder eingefügt werden. 18 Algorithmen und Datenstrukturen << interface >> Collection << interface >> List LinkedList ArrayList << Konstruktor >> public LinkedList(); public LinkedList(Collection collection); << Konstruktor public ArrayList(); public ArrayList(Collection collection); public ArrayList(int anfangsKapazitaet); << Methoden >> public void addFirst(Object object); public void addLast(Object object); public Object getFirst(); public Object getLast(); public Object removeFirst(); public Object removeLast(); << Methoden >> protected void removeRange (int fromIndex, int toIndex) // löscht Teil der Liste von // fromIndex bis toIndex. fromIndex wird // mitgelöscht, toIndex nicht. Vector Abb. Das Interface ListIterator << interface >> ListIterator public boolean hasPrevious(); // bestimmt, ob es vor der aktuellen Position ein weiteres Element gibt, der Zugriff ist mit // previous möglich public boolean hasNext(); public Object next(); public Object previous(); public int nextIndex(); puplic int previousIndex(); public void add(Object o); // Einfügen eines neuen Elements an der Stelle der Liste, die unmittelbar vor dem nächsten // Element des Iterators liegt public void set(Object o); // erlaubt, das durch den letzten Aufruf von next() bzw. previous() beschaffene Element zu // ersetzen public void remove(); Abb.: ListIterator ist eine Erweiterung von Iterator. Die Schnittstelle fügt noch Methoden hinzu, damit an aktueller Stelle auch Elemente eingefügt werden können. 19 Algorithmen und Datenstrukturen Mit einem ListIterator läßt sich rückwärts laufen und auf das vorgehende Element zugreifen. Bsp.: 1. Simulation einer Schlange6 Eine verkettete Liste hat neben den normalen Funktionen aus AbstractList noch weitere Hilfsmethoden zur Implementierung von einem Stack oder einer Schlange. Es handelt sich dabei um die Methoden addFirst(), addLast(), getFirst(), getLast() und removeFirst(). import java.util.*; public class ListBsp { public static void main(String [] args) { LinkedList schlange = new LinkedList(); schlange.add("Thomas"); schlange.add("Andreas"); schlange.add("Josef"); System.out.println(schlange); schlange.removeFirst(); schlange.removeFirst(); System.out.println(schlange); } } 2. Entfernen von Duplikaten7 import java.util.*; public class EntfDupl { public static void main(String args[]) { int [] a = { 1, 7, 7, 1, 5, 1, 2, 7, 2, 1, 6, 6, 3, 6, 7 }; LinkedList l = new LinkedList(); for (int i = 0; i < a.length; i++) { l.add(new Integer(a[i])); } System.out.println(l); // int n = l.size(); int i = 0; do { int aktWert = ((Integer) l.get(i)).intValue(); i++; if (i == l.size()) break; int j = i; do { // if (j >= l.size()) break; int wert = ((Integer) l.get(j)).intValue(); if (wert == aktWert) l.remove(j); j++; } while (j < l.size()); System.out.println(l); } while (i < l.size()); } 6 7 Vgl. pr22220 vgl. pr22220 20 Algorithmen und Datenstrukturen } 3. Das Josephus-Problem 21 Algorithmen und Datenstrukturen 2.2.8.3 Behälterklassen des Typs Set Ein Set ist eine Menge, in der keine doppelten Einträge vorkommen können. Set hat die gleichen Methoden wie Collection. Standard-Implementierung für Set sind das unsortierte HashSet (Array mit veränderlicher Größe) und das sortierte TreeSet (Binärbaum). Bsp.8: import java.util.*; public class SetBeispiel { public static void main(String args []) { Set set = new HashSet(); set.add("Gerhard"); set.add("Thomas"); set.add("Michael"); set.add("Peter"); set.add("Christian"); set.add("Valentina"); System.out.println(set); Set sortedSet = new TreeSet(set); System.out.println(sortedSet); } } 8 Vgl. pr13230 22 Algorithmen und Datenstrukturen << interface >> Collection << interface >> Set public boolean add(Object element); public boolean addAll(Collection collection); public void clear(); public boolean equals(Object object); public boolean contains(Object element); public boolean containsAll(Collection collection); public int hashCode(); public Iterator iterator(); public boolean remove(Object element); public boolean removeAll(Collection collection); public boolean retainAll(Collection collection); public int size(); public Object[] toArray(); public Object[] toArray(Object[] a); HashSet public HashSet(); public HashSet(Collection collection); public HashSet(int anfangskapazitaet); public HashSet(int anfangskapazitaet, int ladeFaktor); << interface >> SortedSet public Object first(); public Object last(); public SortedSet headSet(Object toElement); public SortedSet subSet(Object fromElement, Object toElement); public SortedSet tailSet(Object fromElement); public Comparator comparator(); public Object fisrt(); public Object last(); TreeSet9 public TreeSet() public TreeSet(Collection collection); public TreeSet(Comparator vergleich); public TreeSet(SortedSet collection); Die Schnittstelle Set Ist eine im mathematischen Sinne definierte Menge von Objekten. Die Reihenfolge wird durch das Einfügen festgelegt. Wie von mathematischen Mengen bekannt, darf ein Set keine doppelten Elemente enthalten. Besondere Beachtung muß Objekten 9 implementiert die sortierte Menge mit Hilfe der Klasse TreeMap, verwendet einen Red-Black-Tree als Datenstruktur 23 Algorithmen und Datenstrukturen geschenkt werden, die ihren Wert nachträglich ändern. Die kann ein Set nicht kontrollieren. Eine Menge kann sich nicht selbst als Element enthalten. Zwei Klassen ergeben sich aus Set: die abstrakte Klasse AbstractSet und die konkrete Klasse HashSet. Die Schnittstelle SortedSet erweitert Set so, daß Elemente sortiert ausgelesen werden können. Das Sortierkriterium wird durch die Hilfsklasse Comparator gesetzt. 2.2.8.4 Behälterklassen des Typs Map Ein Map ist eine Menge von Elementen, auf die über Schlüssel zugegriffen wird. Jedem Schlüssel (key) ist genau ein Wert (value) zugeordnet. StandardImplementierungen sind HashMap, HashTable und TreeMap. 24 Algorithmen und Datenstrukturen Interface Map, SortedMap und implemetierende Klassen << interface >> Collection << interface >> Map public void clear(); public boolean containskey(Object key); public boolean containsValue(Object value); public Set entrySet(); public Object get(Object key); public boolean isEmpty(); public Set keySet(); public Object remove(Object key); public int size(); public Collection values(); HashMap public HashMap(); public Hashmap(Collection collection); public HashMap(int anfangskapazitaet); public HashMap(int anfangskapazitaet, int ladeFaktor); << interface >> SortedMap public Comparator comparator(); public Object firstKey(); public Object lastKey(); public SortedMap headmap(Object toKey); public SortedMap subMap(Object fromKey, Object toKey); public SortedMap tailMap(Object fromKey); Hashtable TreeMap public TreeMap(); public TreeMap(Map collection); public TreeMap(Comparator vergleich); public TreeMap(SortedMap collection); Abb.: Die Schnittstelle Map Eine Klasse, die Map implementiert, behandelt einen assoziativen Speicher. Dieser verbindet einen Schlüssel mit einem Wert. Die Klasse Hashtable erbt von Map. Map ist für die implementierenden Klassen AbstractMap, HashMap, Hashtable, RenderingHints, WeakHashMap und Attributes das, was die abstrakte Klasse Dictionary für die Klasse Hashtable ist. Die Schnittstelle SortedMap Eine Map kann mit Hilfe eines Kriteriums sortiert werden und nennt sich dann SortedMap. SortedMap erweitert direkt Map. Das Sortierkriterium wird mit einem speziellen Objekt, das sich Comparator nennt, gesetzt. Damit besitzt auch der 25 Algorithmen und Datenstrukturen assoziative Speicher über einen Iterator eine Reihenfolge. Nur die konkrete Klasse TreeMap implementiert bisher eine SortedMap. Die abstrakte Klasse AbstractMap implementiert die Schnittstelle Map. Die konkrete Klasse HashMap implementiert einen assoziativen Speicher, erweitert die Klasse AbstractMap und implementiert die Schnittstelle Map. Die konkrete Klasse TreeMap Erweitert AbstractMap und implementiert SortedMap. Ein Objekt von TreeMap hält Elemente in einem Baum sortiert. Bsp.10: Aufbau und Anwendung einer Hash-Tabelle import java.io.*; import java.util.*; public class HashTabTest { public static void main(String [ ] args) { Map map = new HashMap(); String eingabeZeile = null; BufferedReader eingabe = null; try { eingabe = new BufferedReader( new FileReader("eing.txt")); } catch (FileNotFoundException io) { System.out.println("Fehler beim Einlesen!"); } try { while ( (eingabeZeile = eingabe.readLine() ) != null) { StringTokenizer str = new StringTokenizer(eingabeZeile); if (eingabeZeile.equals("")) break; String key = str.nextToken(); String daten = str.nextToken(); System.out.println(key); map.put(key,daten); } } catch (IOException ioe) { System.out.println("Eingefangen in main()"); } try { eingabe.close(); } catch(IOException e) { System.out.println(e); } System.out.println("Uebersicht zur Hash-Tabelle"); System.out.println(map); //h.printHashTabelle(); System.out.println("Abfragen bzw. Modifikationen"); // Wiederauffinden String eingabeKey = null; BufferedReader ein = new BufferedReader( 10 vgl. pr23300 26 Algorithmen und Datenstrukturen new InputStreamReader(System.in)); System.out.println("Wiederauffinden von Elementen"); while (true) { try { System.out.print("Bitte Schluessel eingeben, ! bedeutet Ende: "); eingabeKey = ein.readLine(); // System.out.println(eingabeKey); if (eingabeKey.equals("!")) break; String eintr = (String) map.get(eingabeKey); if (eintr == null) System.out.println("Kein Eintrag!"); else { System.out.println(eintr); System.out.println("Soll dieser Eintrag geloescht werden? "); String antwort = ein.readLine(); // System.out.println(antwort); if ((antwort.equals("j")) || (antwort.equals("J"))) { // System.out.println("Eintrag wird entfernt!"); map.remove(eingabeKey); } } } catch(IOException ioe) { System.out.println(eingabeKey + " konnte nicht korrekt eingelesen werden!"); } } System.out.println(map); System.out.println("Sortierte Tabelle"); Map sortedMap = new TreeMap(map); System.out.println(sortedMap); } } 2.2.8.5 Implementierung von Graphen-Algorithmen mit Behälterklassen 1. Kürzeste Pfade in gerichteten, ungewichteten Graphen. Lösungsbeschreibung. Die ungewichteten Graphen G: folgende k1 k3 Abbildung k2 k4 k6 zeigt k5 k7 Abb.: 27 einen gerichteten, Algorithmen und Datenstrukturen Ausgangspunkt ist ein Startknoten s (Eingabeparameter). Von diesem Knoten aus soll der kürzeste Pfad zu allen anderen Knoten gefunden werden. Es interessiert nur die Anzahl der Kanten, die in dem Pfad enthalten sind. Falls für s der Knoten k3 gewählt wurde, kann zunächst am Knoten k3 der Wert 0 eingetragen werden. Die „0“ wird am Knoten k3 vermerkt. k1 k2 k3 k4 k5 0 k6 k7 Abb.: Der Graph nach Markierung des Startknoten als erreichbar Danach werden alle Knoten aufgesucht, die „eine Einheit“ von s entfernt sind. Im vorliegenden Fall sind das k1 und k6. Dann werden die Knoten aufgesucht, die von s zwei Einheiten entfernt sind. Das geschieht über alle Nachfolger von k 1 und k6. Im vorliegenden Fall sind es die Knoten k2 und k4. Aus den benachbarten Knoten von k2 und k4 erkennt man, daß k5 und k7 die kürzesten Pfadlängen von drei Knoten besitzen. Da alle Knoten nun bewertet sind ergibt sich folgenden Bild: k1 k2 1 k3 2 k4 0 k5 2 1 k6 k7 Abb.: Graph nach Ermitteln aller Knoten mit der kürzesten Pfadlänge 2 Die hier verwendete Strategie ist unter dem Namen „breadth-first search“ bekannt. Die „Breitensuche zuerst“ berücksichtigt zunächst alle Knoten vom Startknoten aus, die am weitesten entfernt liegenden Knoten werden zuerst ausgerechnet. Übertragen der Lösungsbeschreibung in Quellcode. Zu Beginn sollte eine Tabelle mit folgenden Einträgen vorliegen: k k1 k2 k3 k4 k5 k6 k7 bekannt false false false false false false false dk 0 pk 0 0 0 0 0 0 0 Die Tabelle überwacht den Fortschritt beim Ablauf des Algorithmus und führt Buch über gewonnene Pfade. Für jeden Knoten werden 3 Angaben in der Tabelle verwaltet: 28 Algorithmen und Datenstrukturen - die Distanz dk des jeweiligen Knoten zu dem Startknoten s. Zu Beginn sind alle Knoten von s aus unerreichbar ( ). Ausgenommen ist natürlich s, dessen Pfadlänge ist 0 (k 3). - Der Eintrag pk ist eine Variable für die Buchführung (und gibt den Vorgänger im Pfad an). - Der Eintrag unter „bekannt“ wird auf „true“ gesetzt, nachdem der zugehörige Knoten erreicht wurde. Zu Beginn wurden noch keine Knoten erreicht. Das führt zu der folgenden Knotenbeschreibung: class Vertex { public String public LinkedList public boolean public int public Vertex ..... } name; adj; bekannt; dist; path; // Name des Knoten // Benachbarte Knoten // Kosten // Vorheriger Knoten auf dem kuerzesten Pfad Die Grundlage des Algorithmus kann folgendermaßen beschrieben werden: /* /* /* /* 1 2 3 4 */ */ */ */ /* 5 */ /* 6 */ /* 7 */ /* 8 */ /* 9 */ void ungewichtet(Vertex s) { Vertex v, w; s.dist = 0; for (int aktDist = 0; aktDist < ANZAHL_KNOTEN; aktDist++) for each v if (!v.bekannt && v.dist == aktDist) { v.bekannt = true; for each w benachbart_zu v if (w.dist == INFINITY) { w.dist = aktDist + 1; w.path = v; } } } Der Algorithmus deklariert schrittweise je nach Distanz (d = 0, d = 1, d= 2 ) die Knoten als bekannt und setzt alle benachbarten Knoten von d w auf die Distanz d w d 1. 2 Die Laufzeit des Algorithmus liegt bei O ( V ) .11 Die Ineffizienz kann beseitigt werden: Es gibt nur zwei unbekannte Knotentypen mit d v . Einigen Knoten wurde dv = aktDist zugeordnet, der Rest zeigt dv = aktDist + 1. Man braucht daher nicht die ganze Tabelle, wie es in Zeile 3 und Zeile 4 beschrieben ist, nach geeigneten Knoten zu durchsuchen. Am einfachsten ist es, die Knoten in zwei Schachtel einzuordnen. In die erste Schachtel kommen Knoten, für die gilt: dv = aktDist. In die zweite Schachtel kommen Knoten, für die gilt: dv = aktDist + 1. In Zeile 3 und Zeile 4 kann nun irgendein Knoten aus der ersten Schachtel herausgegriffen werden. In Zeile 9 kann w der zweiten Schachtel hinzugefügt werden. Wenn die äußere for-Schleife terminiert ist die erste Schachtel leer, und die zweite Schachtel kann nach der ersten Schachtel für den nächsten Durchgang übertragen werden. Durch Anwendung einer Schlange (Queue) kann das Verfahren verbessert werden. Am Anfang enthält diese Schlange nur Knoten mit Distanz aktDist. Benachbarte Knoten haben die Distanz aktDist + 1 und werden „hinten“ an die Schlange 11 wegen der beiden verschachtelten for-Schleifen 29 Algorithmen und Datenstrukturen angefügt. Damit wird garantiert, daß zuerst alle Knoten mit Distanz aktDist bearbeitet werden. Der verbesserte Algorithmus kann in Pseudocode so formuliert werden: /* 1 */ /* 2 */ /* 3 */ /* /* /* /* 4 5 6 7 */ */ */ */ /* 8 */ /* 9 */ /*10 */ void ungewichtet(Vertex s) { Queue q; Vertex v, w; q = new Queue(); q.enqueue(s); s.dist = 0; while (!q.isEmpty()) { v = q.dequeue(); v.bekannt = treu; // Wird eigentlich nicht mehr benoetigt for each w benachbart_zu v if (w.dist == INFINITY) { w.dist = v.dist + 1; w.path = v; q.enqueue(w); } } } Die folgende Tabelle zeigt, wie sich die Daten der Tabelle während der Ausführung des Algorithmus ändern: Anfangszustand k bekannt k1 false k2 false k3 false k4 false k5 false k6 false k7 false Q: k3 dk 0 k2 aus der Schlange k bekannt dk k1 true 1 k2 true 2 k3 true 0 k4 false 2 k5 false 3 k6 true 1 k7 false Q: k4, k5 pk 0 0 0 0 0 0 0 k3 aus der Schlange bekannt dk pk false 1 k3 false 0 true 0 0 false 0 false 0 false 1 k3 false 0 Q: k1, k6 k1 aus der Schlange bekannt dk pk true 1 k3 false 2 k1 true 0 0 false 2 k1 false 0 false 1 k3 false 0 Q: k6, k2, k4 k6 aus der Schlange bekannt dk pk true 1 k3 false 2 k1 true 0 0 false 2 k1 false 0 true 1 k3 false 0 Q: k2, k4 pk k3 k1 0 k1 k2 k3 0 k4 aus der Schlange bekannt dk pk true 1 k3 true 2 k1 true 0 0 true 2 k1 false 3 k2 true 1 k3 false 3 k4 Q: k5, k7 k5 aus der Schlange bekannt dk pk true 1 k3 true 2 k1 true 0 0 true 2 k1 true 3 k2 true 1 k3 false 3 k4 Q: k7 K7 aus der Schlange bekannt dk pk true 1 k3 true 2 k1 true 0 0 true 2 k1 true 3 k2 true 1 k3 true 3 k4 Q: leer Abb.: Veränderung der Daten während der Ausführung des Algorithmus zum kürzesten Pfad Implementierung12. class Vertex { String name; LinkedList adj; int dist; Vertex path; // Konstruktor 12 // // // // Name des Knoten Benachbarte Knoten Kosten Vorheriger Knoten auf dem kuerzesten Pfad vgl.: pr22850 30 Algorithmen und Datenstrukturen public Vertex( String nm ) { name = nm; adj = new LinkedList( ); reset( ); } // Methode public void reset( ) { dist = Graph.INFINITY; path = null; } } Über die Instanzvariable adj wird die Liste der benachbarten Knoten geführt, dist enthält die Kosten, path den Vorgängerknoten vom kürzesten Pfad. Identifiziert wird der Knoten durch einen Namen (Typ: String). Die Klasse Graph implementiert die Methode ungewichtet(). Die Schlange in dieser Liste wird über eine LinkedList mit den Methoden removeFirst() und addLast() simuliert. Zum Aufbau des Graphen dient die Methode addEdge(). Die Kanten werden aus einer Textdatei, die je Zeile ein Knotenpaar (source, destination) umfaßt. Über die Hashmap vertexMap werden die Referenzen zu den Knoten hergestellt. public class Graph { public static final int INFINITY = Integer.MAX_VALUE; private HashMap vertexMap = new HashMap( ); // Abbildung der Knoten // Methode Hinzufuegen Kante public void addEdge(String sourceName,String destName ) { Vertex v = getVertex(sourceName); Vertex w = getVertex(destName); v.adj.add( w ); } // Ausgabe des Pfads public void printPath(String destName) throws NoSuchElementException { Vertex w = (Vertex) vertexMap.get(destName); if( w == null ) throw new NoSuchElementException( "Destination vertex not found" ); else if( w.dist == INFINITY ) System.out.println( destName + " is unreachable" ); else { printPath( w ); System.out.println( ); } } // Falls vertexName nicht da ist, fuege den Knoten // mit diesem Namen in die vertexMap. // In jedem Fall: Rueckgabe des Knoten. private Vertex getVertex(String vertexName) { Vertex v = (Vertex) vertexMap.get(vertexName); if( v == null ) { v = new Vertex(vertexName); vertexMap.put(vertexName, v); } return v; } private void printPath(Vertex dest) { if( dest.path != null ) { printPath( dest.path ); System.out.print( " to " ); } System.out.print( dest.name ); } 31 Algorithmen und Datenstrukturen private void clearAll( ) { for( Iterator itr = vertexMap.values( ).iterator( ); itr.hasNext( ); ) ( (Vertex)itr.next( ) ).reset( ); } public void ungewichtet( String startName ) throws NoSuchElementException { clearAll( ); Vertex start = (Vertex) vertexMap.get(startName); if( start == null ) throw new NoSuchElementException( "Startknoten wurde nicht gefunden" ); LinkedList q = new LinkedList( ); // Schlange fuer breadth search first q.addLast(start); start.dist = 0; while( !q.isEmpty( ) ) { Vertex v = (Vertex) q.removeFirst( ); for( Iterator itr = v.adj.iterator( ); itr.hasNext( ); ) { Vertex w = (Vertex) itr.next( ); if( w.dist == INFINITY ) { w.dist = v.dist + 1; w.path = v; q.addLast( w ); } } } } /* * Verarbeitung einer Anforderung; * Rueckgabe false, falls Dateiende. */ public static boolean processRequest(BufferedReader in, Graph g) { String startName = null; String destName = null; try { System.out.println( "Starknoten:" ); if( (startName = in.readLine( ) ) == null ) return false; System.out.println( "Zielknoten:" ); if( ( destName = in.readLine( ) ) == null ) return false; g.ungewichtet(startName); g.printPath(destName); } catch( Exception e ) { System.err.println( e ); } return true; } /* * Eine main()-Routine, die * 1. Eine Datei liest, die Kanten enthaelt * (Der Dateiname wird als Parameter ueber die * Kommandozeile eingegeben); * 2. den Graphen aufbaut; * 3. wiederholt 2 Knoten anfordert und * den Algorithmus zur Berechnung der kuerzesten Pfade * in Gang setzt. * Die Datei besteht aus Zeilen mit dem Format * Quelle (source) Ziel (destination). */ public static void main( String [ ] args ) { Graph g = new Graph( ); 32 Algorithmen und Datenstrukturen try { FileReader din = new FileReader(args[0]); BufferedReader graphFile = new BufferedReader(din); // Lies die Kanten und fuege ein String zeile; while( ( zeile = graphFile.readLine( ) ) != null ) { StringTokenizer st = new StringTokenizer(zeile); try { if( st.countTokens( ) != 2 ) throw new Exception( ); String source = st.nextToken( ); String dest = st.nextToken( ); g.addEdge(source, dest); } catch( Exception e ) { System.err.println( e + " " + zeile ); } } } catch( Exception e ) { System.err.println( e ); } System.out.println( "File read" ); BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) ); while( processRequest( in, g ) ) ; } } 2. Berechnung der kürzesten Pfadlängen in gewichteten Graphen (Algorithmus von Dijkstra) Lösungsbeschreibung. Die Lösung stützt sich auf die Berechnung der kürzesten Pfadlängen in ungewichteten Graphen13 ab. Im Algorithmus von Dijkstra werden auch die Daten über „bekannt“, dv (kürzeste Pfadlänge) und pv (letzter Knoten, der eine Veränderung von dv verursacht hat) verwaltet. Der Algorithmus von Dijkstra wählt einen Knoten v aus, der das kleinste d v unter allen unbekannten Knoten hat und deklariert, daß der kürzeste Pfad von s nach v bekannt ist. Danach werden die Werte zu dw berechnet. Im ungewichteten Graphen ist dw = dv + 1, falls d w ist. „dw“ wird erniedrigt, falls der Knoten v einen kürzeren Pfad anbietet. Es gilt: d w d v cv , w , falls das eine Verbesserung bewirkt. Der folgende Graph: 13 vgl. 1. 33 Algorithmen und Datenstrukturen 2 k1 k2 4 1 3 10 2 2 k3 k4 5 8 k5 4 k6 6 k7 1 Abb.: Graph nach Ermitteln aller Knoten mit der kürzesten Pfadlänge 2 mit der Knotenbeschreibung class Vertex { .... public LinkedList public boolean public DistType14 public Vertex ... } adj; // Benachbarte Knoten bekannt; // dist; // Kosten path; // Vorheriger Knoten auf dem kuerzesten Pfad führt zu der folgende Initialisierung: k k1 k2 k3 k4 k5 k6 k7 bekannt false false false false false false false dk 0 pk null null null null null null null Abb.: Anfangszustand der Tabelle mit den Daten für den Algorithmus von Dijkstra Der erste Knoten (Start( ist der Knoten k1 mit Pfadlänge 0. Nachdem k1 bekannt ist, ergibt sich folgendes Bild: k k1 k2 k3 k4 k5 k6 k7 bekannt true false false false false false false dk 0 2 1 pk null k1 null k1 null null null Abb.: Zustand der Tabelle nach „k1 ist bekannt“ „k1“ besitzt die Nachbarknoten: k2 und k4. „k4“ wird gewählt und als bekannt markiert. Die Knoten k3, k5, k6 und k7 sind jetzt die benachbarten Knoten. 14 DistType ist wahrscheinlich int 34 Algorithmen und Datenstrukturen k k1 k2 k3 k4 k5 k6 k7 bekannt true false false true false false false dk 0 2 3 1 3 9 5 pk null k1 k4 k1 k4 k4 k4 Abb.: Zustand der Tabelle nach „k4 ist bekannt“ „k2“ wird gewählt. „k4“ ist benachbart, aber schon bekannt. „k5“ ist ebenfalls benachbart, wir aber nicht ausgerichtet, da die Kosten von „k 2“ aus 2 +10 = 12 sind und ein Pfad der Länge 3 schon bekannt ist k k1 k2 k3 k4 k5 k6 k7 bekannt true true false true false false false dk 0 2 3 1 3 9 5 pk null k1 k4 k1 k4 k4 k4 Abb.: Zustand der Tabelle nach „k2 ist bekannt“ Der nächste ausgewählte Knoten ist „k5“ (ohne Ausrichtungen), danach wird k3 gewählt. Die Wahl von „k3“ bewirkt die Ausrichtung von „k6“ k k1 k2 k3 k4 k5 k6 k7 bekannt true true true true true false false dk 0 2 3 1 3 8 5 pk null k1 k4 k1 k4 k3 k4 Abb.: Zustand der Tabelle „k5 ist bekannt“ und (anschließend) „k 3 ist bekannt“. „k7“ wird gewählt. Daraus resultiert folgende Tabelle: k k1 k2 k3 k4 k5 k6 k7 bekannt true true true true true false true dk 0 2 3 1 3 6 5 pk null k1 k4 k1 k4 k7 k4 Abb.: Zustand der Tabelle „k7 ist bekannt“. Schließlich bleibt nur noch k6 übrig. Das ergibt dann die folgende Abschlußtabelle: 35 Algorithmen und Datenstrukturen k k1 k2 k3 k4 k5 k6 k7 bekannt true true true true true true true dk 0 2 3 1 3 6 5 pk null k1 k4 k1 k4 k7 k4 Abb.: Zustand der Tabelle nach „k6 ist bekannt“. Der Algorithmus, der diese Tabellen folgendermaßen beschrieben werden: berechnet, kann (in Pseudocode) void dijkstra(Vertex s) { Vertex v, w; /* 1 */ s.dist = 0; /* 2 */ for(; ;) { /* 3 */ v = kleinster_unbekannter_Distanzknoten; /* 4 */ if (v == null) /* 5 */ break; /* 6 */ v.bekannt = true; /* 7 */ for each w benachbart_zu v /* 8 */ if (!w.bekannt) /* 9 */ if (v.dist + cvw < w.dist) { /* 10 */ w.dist = v.dist + cvw; /* 11 */ w.pfad = v; } } } Die Laufzeit des Algorithmus resultiert aus dem Aufsuchen aller Knoten (in den beiden for-Schleifen) und im Aufsuchen der Kanten (cvw) (in der inneren forSchleife): O(|E| + |V|2) = O(|V|2) Ein Problem des vorstehenden Algorithmus ist das Durchsuchen der Knotenmenge nach der kleinsten Distanz15. Man kann das wiederholte Bestimmen der kleinsten Distanz einer prioritätsgesteuerten Warteschlange übertragen. Der Leistungsaufwand beträgt dann O(|E| log(|V)+|V| log(|V|)). Der Algorithmus (in Pseudocode) könnte so aussehen: void dijkstra(Vertex s) { Vertex v, w; PriorityQueue pq = new PriorityQueue(); s.dist = 0; for each k V (G ) { pq.einfuegen(...,k.dist); } while (!pq.isEmpty) { v = extractMinimal(pq); v.bekannt = true; for each w benachbart_zu v { if (!w.bekannt) 15 v = kleinster_unbekannter_Distanzknoten 36 Algorithmen und Datenstrukturen { if (v.dist + cvw < w.dist) { w.dist = v.dist + cvw; adjustiere pq an dem neuen Wert w.dist w.pfad = v; } } } } } Negativ bewertete Kanten. Falls der Graph negativ bewertete Kanten hat, funktioniert der Algorithmus von Dijkstra nicht. Implementiert man den DijkstraAlgorithmus mit einer Schlange, dann dürfen Kantenbewertungen auch negativ sein: void negativGewichtet(Vertex s) { Queue q; Vertex v, w; q = new Queue(); q = enqueue(s); while(!q.isEmpty()) { v = dequeue(); for each w benachbart_zu w if (v.dist + cvw < w.dist) { w.dist = v.dist + cvw; w.path = v; if (w ist_nicht_in q) q.enqueue(w); } } } Implementierung.16 import java.util.*; import java.io.*; class Vertex { String name; // Name des Knoten LinkedList adj; // Benachbarte Knoten boolean bekannt; int dist; // Kosten Vertex path; // Vorheriger Knoten auf dem kuerzesten Pfad // Konstruktor public Vertex(String nm) { name = nm; adj = new LinkedList( ); reset( ); } // Methode public void reset( ) { dist = Graph.INFINITY; path = null; bekannt = false; } } public class Graph { public static final int INFINITY = Integer.MAX_VALUE; private HashMap vertexMap = new HashMap(); // Abbildung der Knoten private HashMap edgeMap = new HashMap(); // Abbildung der Kanten // Methode Hinzufuegen Kante public void addEdge(String sourceName,String destName,String distanz) 16 vgl. pr22851 37 Algorithmen und Datenstrukturen { Vertex v = getVertex(sourceName); Vertex w = getVertex(destName); String key = sourceName + destName; v.adj.add(w); edgeMap.put(key,distanz); } // Ausgabe des Pfads public void printPath(String destName) throws NoSuchElementException { Vertex w = (Vertex) vertexMap.get(destName); if( w == null ) throw new NoSuchElementException( "Destination vertex not found" ); else if( w.dist == INFINITY ) System.out.println( destName + " is unreachable" ); else { printPath( w ); System.out.println( ); } } // Falls vertexName nicht da ist, fuege den Knoten // mit diesem Namen in die vertexMap. // In jedem Fall: Rueckgabe des Knoten. private Vertex getVertex(String vertexName) { Vertex v = (Vertex) vertexMap.get(vertexName); if( v == null ) { v = new Vertex(vertexName); vertexMap.put(vertexName, v); } return v; } private void printPath(Vertex dest) { if( dest.path != null ) { printPath( dest.path ); System.out.print( " to " ); } System.out.print( dest.name ); } private void clearAll( ) { for( Iterator itr = vertexMap.values( ).iterator( ); itr.hasNext( ); ) ( (Vertex)itr.next( ) ).reset( ); } public void dijkstra(String startName) throws NoSuchElementException { clearAll( ); Vertex v, w; Vertex start = (Vertex) vertexMap.get(startName); // System.out.println(vertexMap); // System.out.println(edgeMap); if( start == null ) throw new NoSuchElementException( "Startknoten wurde nicht gefunden" ); // LinkedList q = new LinkedList( ); // Schlange fuer breadth search first // q.addLast(start); start.dist = 0; v = start; w = start; for (; ;) { if (v == null) break; 38 Algorithmen und Datenstrukturen v.bekannt = true; // w = null; int kleinstW = INFINITY; // v = null; for( Iterator itr = v.adj.iterator( ); itr.hasNext( ); ) { w = (Vertex) itr.next( ); if (w == null) break; if (!w.bekannt) { String key = v.name + w.name; int cvw = Integer.parseInt((String) edgeMap.get(key)); if (v.dist + cvw < w.dist) { w.dist = v.dist + cvw; w.path = v; System.out.println(w.name); if (kleinstW > w.dist) { kleinstW = w.dist; v = w; } // v = w; } } } } } /* * Verarbeitung einer Anforderung; * Rueckgabe false, falls Dateiende. */ public static boolean processRequest(BufferedReader in, Graph g) { String startName = null; String destName = null; try { System.out.println( "Starknoten:" ); if( (startName = in.readLine( ) ) == null ) return false; System.out.println( "Zielknoten:" ); if( (destName = in.readLine( ) ) == null ) return false; // System.out.println(startName); g.dijkstra(startName); System.out.println("Kuerzeste Wege wurden berechnnet"); g.printPath(destName); } catch( Exception e ) { System.err.println( e ); } return true; } /* * * * * * * * * * * Eine main()-Routine, die 1. Eine Datei liest, die Kanten enthaelt (Der Dateiname wird als Parameter ueber die Kommandozeile eingegeben); 2. den Graphen aufbaut; 3. wiederholt 2 Knoten anfordert und den Algorithmus zur Berechnung der kuerzesten Pfade in Gang setzt. Die Datei besteht aus Zeilen mit dem Format Quelle (source) Ziel (destination) Gewicht. 39 Algorithmen und Datenstrukturen */ public static void main(String [] args) { Graph g = new Graph( ); try { FileReader din = new FileReader(args[0]); BufferedReader graphFile = new BufferedReader(din); // Lies die Kanten und fuege ein String zeile; while( ( zeile = graphFile.readLine() ) != null ) { StringTokenizer st = new StringTokenizer(zeile); try { if( st.countTokens( ) != 3 ) throw new Exception( ); String source = st.nextToken( ); String dest = st.nextToken( ); String distanz = st.nextToken(); g.addEdge(source, dest, distanz); } catch( Exception e ) { System.err.println( e + " " + zeile ); } } } catch( Exception e ) { System.err.println( e ); } System.out.println( "File read" ); BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) ); while( processRequest( in, g ) ) ; } } 40 Algorithmen und Datenstrukturen 3. Berechnung eines minimal spannenden Baums in einem zusammenhängenden, ungerichteten, ungewichteten Graphen (Algorithmus von Prim) Definition: Ein minimal spannender Baum eines Graphen G ist ein spannender Baum von G von minimaler Gesamtlänge unter allen spannenden Bäumen von G. Lösungsbeschreibung. Der folgende Graph 2 k1 k2 4 1 3 10 2 7 k3 k4 5 k5 8 4 k6 6 k7 1 besitzt folgenden minimale Spannbaum: 2 k1 k2 1 2 7 k3 k4 k5 4 k6 6 k7 1 Abb.: Die Anzahl der Kanten in einem minimal spannenden Baum ist |V| - 1 (Anzahl der Knoten – 1). Der minimal spannende Baum ist - ein Baum, der keine Zyklen besitzt. - spannend, da er jeden Knoten abdeckt. - ein Minimum. Der Algorithmus von Prim arbeitet stufenweise. Auf jeder Stufe wird ein Knoten ausgewählt. Die Kanten auf seine nachfolgenden Knoten werden dann untersucht. Die Untersuchung folgt nach den Vorschriften des Dijkstra-Algorithmus. Es gibt nur eine Ausnahme hinsichtlich der Ermittlung der Distanz: d w min( d v , cvw ) Die Ausgangssituation zeigt folgende Tabelle: 41 Algorithmen und Datenstrukturen k k1 k2 k3 k4 k5 k6 k7 bekannt false false false false false false false dv 0 pv null null null null null null null Abb.: Ausgangssituation „k1“ wird ausgewählt, „k2, k3, k4 sind zu k1 benachbart“. Das führt zur folgenden Tabelle: k k1 k2 k3 k4 k5 k6 k7 bekannt true false false false false false false dv 0 2 4 1 pv null k1 k1 k1 null null null Abb.: Die Tabelle im Zustand „k1 ist bekannt“ Der nächste Knoten, der ausgewählt wird ist k4. Jeder Knoten ist zu k4 benachbart. Ausgenommen ist k1, da dieser Knoten „bekannt“ ist. k2 bleibt unverändert, denn die „Kosten“ von k4 nach k2 sind 3, bei k2 ist 2 eingetragen. Der Rest wird, wie die folgende Tabelle zeigt, verändert: k k1 k2 k3 k4 k5 k6 k7 bekannt true false false true false false false dv 0 2 2 1 7 8 4 pv null k1 k4 k1 k4 k4 k4 Abb.: Die Tabelle im Zustand „k4 ist bekannt“ Der nächste Knoten, der ausgewählt wird, ist k2. Das zeigt keine Auswirkungen. Dann wird k3 gewählt. Das bewirkt eine Veränderung der Distanz zu k6. k k1 k2 k3 k4 k5 k6 k7 bekannt true true true true false false false dv 0 2 2 1 7 5 4 pv null k1 k4 k1 k4 k3 k4 Abb.: Tabelle mit Zustand „k2 ist bekannt“ und (anschließend) mit dem Zustand „k 3 ist bekannt“ Es folgt die Wahl des Knoten k7, was die Ausrichtung von k6 und k5 bewirkt: k Bekannt dv pv 42 Algorithmen und Datenstrukturen k1 k2 k3 k4 k5 k6 k7 true true true true false false true 0 2 2 1 6 1 4 null k1 k4 k1 k7 k7 k4 Abb.: Tabelle mit Zustand „k7 ist bekannt“ Jetzt werden noch k6 und dann k5 bestimmt. Die Tabelle nimmt danach folgende Gestalt an: k k1 k2 k3 k4 k5 k6 k7 Bekannt true true true true true true true dv 0 2 2 1 6 1 4 pv null k1 k4 k1 k7 k7 k4 Abb.: Tabelle mit Zustand „k6 ist bekannt“ und (anschließend) „k5 ist bekannt“ Die Tabelle zeigt, daß folgende Kanten den minimal spannenden Baum bilden: (k2,k3),k3,k4)(k4,k1),(k5,k7),(k6,k7),(k7,k4) Der Algorithmus von Prim zeigt weitgehende Übereinstimmung mit dem Algorithmus von Dijkstra. Er besitzt aber auch Gültigkeit für ungerichtete Graphen. Jede Kante ist daher in zwei Adjazenzlisten zu führen. Ohne Heaps ist die Laufzeit O(|V| 2). 43 Algorithmen und Datenstrukturen 2.2.9 Algorithmen Die Wahl einer geeigneten Datenstruktur ist der erste Schritt. Im zweiten Schritt müssen die Algorithmen implementiert werden. Die Java Bibliothek hilft mit einigen Standardalgorithmen weiter. Dazu zählen Funktionen zum Sortieren und Suchen in Containern und das Füllen von Containern. Zum flexiblen Einsatz dieser Funktionen haben die Java-Entwickler die Klasse Collections bereitgestellt. Collections bietet Algorithmen statischer Funktionen an, die als Parameter ein Collection Objekt erwarten. Leider sind viele Algorithmen nur auf List Objekte definiert17, z.B. public static void shuffle(List list) // würfelt die Werte einer Liste durcheinander Bsp.18: import java.util.*; public class VectorShuffle { public static void main(String args[]) { Vector v = new Vector(); for (int i = 0; i < 10; i++) v.add(new Integer(i)); Collections.shuffle(v); System.out.println(v); } } public static void shuffle(List list, Random rnd) // würfelt die werte der Liste durcheinander und benutzt dabei den Random Generator rnd. Nur die Methoden min() und max() arbeiten auf allgemeinen Collection-Objekten. 2.2.9.1 Datenmanipulation Daten umdrehen. Die Methode reverse() dreht die Werte einer Liste um. Die Laufzeit ist linear zu der Anzahl der Elemente. public static void reverse(List l) // dreht die Elemente in der Liste um Listen füllen. Mit der fill()-Methode läßt sich eine Liste in linearer Zeit belegen. Nützlich ist dies, wenn eine Liste mit Werten initialisiert werden muß. public static void fill (List l, Object o) // füllt eine Liste mit dem Element o Daten zwischen Listen kopieren. Die Methode copy(List quelle, List ziel) kopiert alle Elemente von quelle in die Liste ziel und überschreibt dabei Elemente, die evtl. an dieser Stelle liegen. public static void copy(List quelle, List ziel) // kopiert Elemente von quelle nach ziel. Ist ziel zu klein, gibt es eine IndexOutOfBoundsException 17 18 Nutzt die Collection Klasse keine List Objekte, arbeitet sie mit Iterator Objekten, um allgemein zu bleibem vgl. pr22122 44 Algorithmen und Datenstrukturen 2.2.9.2 Größter und kleinster Wert einer Collection Die Methoden min() und max() suchen das größte und kleinste Element einer Collection. Die Laufzeit ist linear zur Größe der Collection. Die Methoden machen keinen Unterschied, ob die Liste schon sortiert ist oder nicht. public static Object min(Collection c) // public static Object max(Collection c) /* Falls min() bzw. max() auf ein Collection-Objekt angewendet wird, erfolgt die Bestimmung des Minimums bzw. Maximums nach der Methode compareTo der Comparable Schnittstelle. Byte, Character, Double, File, Float, Long, Short, String, Integer, BigInteger, ObjectStreamField, Date und Calendar haben diese Schnittstelle implementiert. Lassen sich die Daten nicht vergleichen, dann gibt es eine ClassCastException */ public static Object min(Collection c, Comparator vergl) // public static Object max(Collection c, Comparator vergl) 2.2.9.3 Sortieren Die Collection Klasse bietet zwei sort() Methoden an, die die Elemente einer Liste stabil19 sortieren. Die Methode sort() sortiert die Elemente in ihrer natürlichen Ordnung, z.B.: - Zahlen nach der Größe (13 < 40) - Zeichenketten alphanumerisch (Juergen < Robert < Ulli) Eine zweite überladene Form von sort() arbeitet mit einem speziellen Comparator Objekt, das zwei Objekte mit der Methode compare() vergleicht. public static void sort(List liste) // sortiert die Liste public static void sort(List liste,Comparator c) // sortiert die Liste mit dem Comparator c Die Sortierfunktion arbeitet nur mit List-Objekten. „sort()“ gibt es aber auch in der Klasse Arrays. Bsp.: Das folgende Programm sortiert eine Reihe von Zeichenketten in aufsteigender Folge. Es nutzt die Methode Arrays.asList() zur Konstruktion einer Liste aus einem Array20. import java.util.*; public class CollectionsSortDemo { public static void main(String args[]) { String feld[] = 19 Stabile Sortieralgorithmen beachten die Reihenfolge von gleichen Elementen, z.B. beim Sortieren von Nachrichten in einem Email-Programm, zuerst nach dem Datum und anschließend nach dem Sender, soll die Liste innerhalb des Datum sortiert bleiben. 20 Leider gibt es keinen Konstruktor für ArrayList, der einen Array mit Zeichenketten zuläßt. 45 Algorithmen und Datenstrukturen { "Regina","Angela","Michaela","Maria","Josepha", "Amalia","Vera","Valentina","Daniela","Saida", "Linda","Elisa" }; List l = Arrays.asList(feld); Collections.sort(l); System.out.println(l); } } Die Java Bibliothek bietet nicht viel zur Umwandlung von Feldern („Array“) in dynamische Datenstrukturen. Eine Ausnahme bildet die Hilfsklasse Arrays, die die Methode asList() anbietet. Die Behälterklassen ArrayList und LinkedList werden über asList() nicht unterstützt, d.h. Über asList() wird zwar eine interne Klasse ArrayList benutzt, die eine Erweiterung von AbstractList ist, aber nur das notwendigste implementiert. Sortieralgorithmus. Es handelt sich um einen optimierten „Merge-Sort“. Seine Laufzeit beträgt N log( N ) . Die sort() Methode arbeitet mit der toArray() Funktion der Klasse List. Damit werden die Elemente der Liste in einem Feld (Array) abgelegt. Schließlich wird die sort() Methode der Klasse Arrays genutzt und mit einem ListIterator wieder in die Liste eingefügt. Daten in umgekehrter Reihenfolge sortieren. Das wird über ein spezielles Comparator-Objekt geregelt, das von Collections über die Methode reverseOrder() angefordert werden kann. Bsp.21: import java.util.*; public class CollectionsReverseSortDemo { public static void main(String args[]) { Vector v = new Vector(); for (int i = 0; i < 10; i++) { v.add(new Double(Math.random())); } Comparator comparator = Collections.reverseOrder(); Collections.sort(v,comparator); System.out.println(v); } } Eine andere Möglichkeit für umgekehrt sortierte Listen besteht darin, erst die Liste mit sort() zu sortieren und anschließend mit reverse() umzudrehen. 21 Vgl. pr22122 46 Algorithmen und Datenstrukturen 2.2.9.4 Suchen von Elementen Die Behälterklassen enthalten die Methode contains(), mit der sich Elemente suchen lassen. Für sortierte Listen gibt es eine wesentlich schnellere Suchmethode: binarySearch(): public static int binarySearch(List liste, Object key) // sucht ein Element in der Liste. Gibt die Position zurück oder ein Wert kleiner 0, // falls key nicht in der Liste ist. public static int binarySearch(List liste, Object key, Comparator c) // Sucht ein Element mit Hilfe des Comparator Objekts in der Liste. Gibt die Position zurück oder // einen Wert kleiner als 0, falls der key nicht in der Liste ist. Bsp.22: Das folgende Programm sortiert (zufällig ermittelte) Daten und bestimmt Daten in Listen mit Hilfe der binären Suche. import java.util.*; public class ListSort { public static void main(String [] args) { final int GR = 20; // Verwenden einer natuerliche Ordnung List a = new ArrayList(); for (int i = 0; i < GR; i++) a.add(new VglClass((int)(Math.random() * 100))); Collections.sort(a); Object finde = a.get(GR / 2); int ort = Collections.binarySearch(a,finde); System.out.println("Ort von " + finde + " = " + ort); // Verwenden eines Comparator List b = new ArrayList(); // Bestimmt zufaellig Zeichenketten der Laenge 4 for (int i = 0; i < GR; i++) b.add(Felder.randString(4)); // Instanz fuer den Comparator AlphaVgl av = new AlphaVgl(); // Sortieren Collections.sort(b,av); // Binaere Suche finde = b.get(GR / 2); ort = Collections.binarySearch(b,finde,av); System.out.println(b); System.out.println("Ort von " + finde + " = " + ort); } } 2.2.9.5 Typsichere Datenstrukturen Die Datenstrukturen des Pakets java.util haben einen großen Nachteil: Die eingesetzten Typen sind immer vom Typ Object. Typsicherheit über Templates, wie C++ es bietet, ist bisher nicht vorgesehen. Es spricht einiges dafür, daß in den nächsten Javagenerationen generische Typen Berücksichtigung finden. 22 Vgl. pr22122 47