8 Datenstrukturen 8- 1 Datenstrukturen Inhalt Vordefinierte Datenstrukturen Eigene Datenstrukturen Übungsaufgaben Vordefinierte Datenstrukturen Wir behandeln an dieser Stelle nur zwei Beispiele. Vector Die Klasse Vector funktioniert wie ein Array, das bei Bedarf wächst. Sie ist in java.util erklärt und muß erst importiert werden. import java.util.*; Danach kann man einen Vektor wie folgt deklarieren Vector v = new Vector(); Der Konstruktor akzeptiert auch eine Minimalgröße. Was kann man nun auf dem Vektor speichern? Java unterstützt einen speziellen Datentyp Object, der entweder ein Array oder eine Instanz einer Klasse sein kann (eigentlich eine Referenz auf eines von beiden). Um solch ein Object in ein Array oder ein Klassen-Objekt (die Syntax ist hier etwas unglücklich, da Objekte eigentlich nur Instanzen von Klassen bezeichnen) umzuwandeln, verwendet man einen type cast. Normalerweise verwendet man Vektoren zur Speicherung homogener Daten. Dies ist aber nicht notwendigerweise so, wie das folgende Beispiel zeigt, das Daten verschiedener Typen auf einem Vektor speichert. import java.util.*; public class Test { static public void main (String args[]) { Vector v=new Vector(); // Speichern: v.addElement("Test"); // Speichere String v.addElement(new Integer(2)); // Speichere Integer-Objekt v.addElement(new double[100]); // Speichere Array 8 Datenstrukturen 8- 2 // Auslesen: System.out.println((String)v.elementAt(0)); // druckt: Test System.out.println((Integer)v.elementAt(1)); // druckt: 2 System.out.println(((double[])v.elementAt(2)).length); // druckt: 100 } } Die Integer-Klasse ist eine sogenannte Wrapper-Klasse für int-Werte. Sie ist genau zu dem Zweck gedacht, int-Werte in Strukturen wie Vektoren zu speichern. Sie besitzt eine sinnvolle toString()-Methode und kann daher direkt ausgedruckt werden. Analog existiert Double, Boolean etc. Der type cast für ein Array hat also die Form (datentyp []). Man beachte das zusätzliche Klammerpaar vor .length. Dieses Paar ist nötig, um den type cast auf das Objekt v.elementAt(2) wirken zu lassen, bevor dessen Länge ausgegeben wird. Macht man beim Unwandeln des Objektes einen Fehler, so gibt es eine IllegalTypeCastException. Hashtable Hashtables sind ein Beispiel für ein fortgeschrittenes und nützliches Konzept. Sie dienen dazu, Daten anhand eines Schlüssels abzuspeichern, unter dem Sie später wiedergefunden werden können. Der Schlüssel kann ein beliebiges Objekt sein, das eine hashCode() Methode hat. Meist jedoch wird einfach ein String verwendet. Hashtables speichern die Objekte in einem Array unten einem Index, der sich aus dem Hashcode des Schüssels ergibt. Dabei müssen natürlich eventuelle Kollisionen aufgelöst werden, wofür es verschiedene Strategien gibt. Folgendes Beispiel illustriert das Abpeichern und wiederfinden von Objekten. import java.util.*; public class Test { public static void main (String args[]) { Hashtable H=new Hashtable(); H.put("eins","one"); H.put("zwei","two"); H.put("drei","three"); System.out.println(H.get("zwei")); // druckt: two System.out.println(H.get("vier")); // druckt: null } } Falls also ein Schlüssel nicht gefunden wird, liefert get() den Wert null. Hashtables eignen sich also für Wörterbücher oder Eigenschaftlisten (Properties). Sie sind sowohl beim Zugriff als auch beim Abspeichern sehr effektiv. 8 Datenstrukturen 8- 3 Eigene Datenstrukturen Listen Als Beispiel erzeugen wir eine einfach verkettete Liste. Jedes Listenelement ist ein Objekt auf dem Heap, das ein Zeiger auf das nächste Listenelement enthält. Das letzte Listenelement zeigt auf null. Dies ist sehr einfach mit folgender Klasse zu realisieren. class ListElement { private ListElement Next; public ListElement () // Listelement ohne Nachfolger { Next=null; } public ListElement (ListElement next) // mit Nachfolger next { Next=next; } public void setNext (ListElement next) // setze Nachfolger { Next=next; } public ListElement getNext () // lies Nachfoger { return Next; } } Mit Hilfe dieser Klasse kann man einfach 10 Elemente erzeugen. Mit folgendem Code werden einfach 10 Listenelemente erzeugt, wobei die neueste jeweils vorne an die Liste gehängt wird. public class Test { public static void main (String args[]) { ListElement e=null; for (int i=0; i<10; i++) { e=new ListElement(e); } } } Will man irgendeinen Inhalt in die Listenelemente speichern, so erzeugt man einfach einen Erben, der das kann. Z.B. class StringElement extends ListElement { private String S; public StringElement (String s) { S=s; } public StringElement (String s, ListElement next) { super(next); S=s; } public void setString (String s) { S=s; } public String getString () { return S; } } Man beachte den Aufruf des Konstruktors von ListElement mit Hilfe von super(next). Bei 8 Datenstrukturen 8- 4 dem Default-Konstruktor (super()) ist dieser Aufruf nicht notwendig, sondern wird automatisch durchgeführt. public class Test { public static void main (String args[]) { ListElement e=null; for (int i=0; i<10; i++) { e=new StringElement("Number "+i,e); } while (e!=null) { System.out.println(((StringElement)e).getString()); e=e.getNext(); } } } Dieses Programm gibt die Zahlen in umgekehrter Reihenfolge aus, da das neueste ListenElement jeweils vorne angehängt wird, aber die Liste von vorne nach hinten gedruckt wird. Alternativ könnte man auch ein Object-Datenfeld in ListElement aufnehmen und die Strings darauf ablegen. Dies erspart die Deklaration eines Kindes für jedes aufzunehmende Objekt. Die entsprechende Deklaration sieht so aus. class ListElement { private ListElement Next; Object Content; public ListElement (Object content) // Listelement ohne Nachfolger { this(content,null); } public ListElement (Object content, ListElement next) // mit Nachfolger next { Next=next; Content=content; } public void setNext (ListElement next) // setze Nachfolger { Next=next; } public ListElement getNext () // lies Nachfoger { return Next; } public void setContent (Object content) { Content=content; } public Object getContent () { return Content; } } public class Test { public static void main (String args[]) { ListElement e=null; for (int i=0; i<10; i++) { e=new ListElement("Number "+i,e); } while (e!=null) { System.out.println((String)(e.getContent())); e=e.getNext(); } 8 Datenstrukturen 8- 5 } } Beachten Sie den Aufruf des zweiten Konstruktors mittels this(). Im Hauptprogramm muß hier getContent() erst nach String umgewandelt werden, bevor man es verwenden kann. Bäume Wir geben hier zum Studium nur das Listing von sortierten Binärbäumen an. Ein Baum besteht aus Knoten, mit je zwei Kindern, die wieder Bäume sind (oder null). Der Knoten hat als Inhalt einen String. Alle Knoten im linken Kindbaum haben einen alphabetisch kleineren String als Inhalt und alle Knoten im rechten Kindbaum einen alphabetisch größeren. Wir erzeugen einen solchen Baum, und geben die Inhalte sortiert wieder aus. class StringNode { private String S; protected StringNode Left,Right; public StringNode (String s) { Left=Right=null; S=s; } public void insert (String s) // Fuege s korrekt ein. { if (s.compareTo(S)>0) // dann rechts { if (Right==null) Right=new StringNode(s); else Right.insert(s); } else // sonst links { if (Left==null) Left=new StringNode(s); else Left.insert(s); } } public String getString () { return S; } public StringNode getLeft () { return Left; } public StringNode getRight () { return Right; } } public class Test { public static void main (String args[]) { StringNode tree=null; for (int i=0; i<20; i++) // 20 Zusfallsstrings speichern { String s="Zufallszahl "+Math.random(); if (tree==null) tree=new StringNode(s); else tree.insert(s); } print(tree); // Sortiert wieder ausdrucken } public static void print (StringNode tree) // Rekursive Druckfunktion { if (tree==null) return; print(tree.getLeft()); System.out.println(tree.getString()); 8 Datenstrukturen 8- 6 print(tree.getRight()); } } Übungsaufgaben 1. Ändern Sie das Hauptprogramm für die verkettete Liste (mit StringElement) so um, daß die neuen Listenelemente jeweils an das Ende der Liste angehängt werden. 2. Schreiben Sie eine Klasse StringList, die eine Liste von Strings verwaltet. Sie soll eine Zeiger auf das erste und das letzte Element enthalten, und Strings vor und hinter die Liste hängen können. 3. Schreiben Sie ein rekursives Unterprogramm, das eine Liste von Strings von hinten nach vorne ausgibt. 4. Schreiben Sie ein rekursives Unterprogramm, das alle Vorkommen eines Strings aus der Liste streicht, etwa definiert als static StringElement remove(StringElement e, String s); 5. Schreiben Sie void remove (String s); als Methode der Klasse StringList aus 2. Hier muß am Schluß noch Last aktualisiert werden. Lösung. Aufgaben ohne Lösung 1. Schreiben Sie eine Klasse StringList, die eine vorwärts und rückwärts verkettete Liste verwaltet. Dazu muß natürlich ListElement einen PreviousVerweis bekommen, der dann auch verwaltet werden muß. 2. Schreiben Sie eine iterative Version von remove(String s), die den String s aus eine StringList entfernt.