Algorithmen und Datenstrukturen

Algorithmen und Datenstrukturen Java-Tutorium, Sprachliche Informationsverarbeitung, Sommersemester 2007, Fabian Steeg 01. Sitzung, 05.04.2007: Wieder...
Author: Theresa Acker
28 downloads 1 Views 115KB Size
Algorithmen und Datenstrukturen Java-Tutorium, Sprachliche Informationsverarbeitung, Sommersemester 2007, Fabian Steeg 01. Sitzung, 05.04.2007: Wiederholung Ferienaufgabe (Übung zu Schleifen und OOP) 02. Sitzung, 19.04.2007: Datenstrukturen, Listen, Stack 03. Sitzung, 26.04.2007: Laufzeit, Stack-Implementierung 04. Sitzung, 03.05.2007: Collections: Set, List, Map 05. Sitzung, 10.05.2007: Iterable und Iterator 06. Sitzung, 16.05.2007: Binäre Suche und Comparable 07. Sitzung, 24.05.2007: Rekursion: Baumkonstruktion und Traversierung 08. Sitzung, 14.06.2007: Sortierverfahren 09. Sitzung, 21.06.2007: Sprachspezifisches Sortieren von Strings und Comparable 10. Sitzung, 28.06.2007: Nicht-binäre Bäume, Crawling, sprachspezifischer Selection-Sort 11. Sitzung, 12.07.2007: Tokenisierung und Reguläre Ausdrücke 01. Sitzung, 05.04.2007: Wiederholung Ferienaufgabe (Übung zu Schleifen und OOP) Thema der Sitzung war eine Wiederholung von einigen Themen rund um IO sowie ein Besprechung der Ferienaufgabe (Game of Life). Nachfolgend die nötigen Schritte zur Lösung der Aufgabe (ein ähnliches Vorgehen, allerdings komplexer, wird zur Lösung der Java-Abschlussarbeit nötig sein). Einkommentieren der vorgegebenen Zeile in der Application.java für die eigene Animal-Implementierung (siehe Kommentare dort): animals[x][y] = new MyAnimal(); //animals[x][y] = null;

Implementieren von MyAnimal (implements Animal), einer einfachen Klasse mit zwei Attributen und Gettern und Settern, sowie einer Methode zum Anlegen einer Kopie (wird von bestehenden Programmteilen verwendet, wenn die Matrix kopiert wird): public class MyAnimal implements Animal { private Integer age = 0; private Boolean living;

public Animal createCopy() { MyAnimal newAnimal = new MyAnimal(); newAnimal.setAge(this.age); newAnimal.setLiving(this.living); return newAnimal; }

public Integer getAge() { return age; } public Boolean isAlive() { return living; } public void setAge(Integer age) { this.age=age; } public void setLiving(Boolean living) { this.living = living; } }

Einkommentieren der Zeile für die eigene Runtime-Implementierung in der Application.java (siehe Kommentare dort):

AbstractLifeRuntime runtime = new MyRuntimeImpl(animals); //AbstractLifeRuntime runtime = null;

Implementieren von MyRuntimeImpl (extends AbstractLifeRuntime). Diese Klasse enthält die zentrale Programmlogik: Die Berechnung der Folgegeneration, etwa in folgender Form (wie wir es im Tutorium gemacht haben; dass es am Ende nicht lief lag am fehlenden icons-Ordner, so dass man nichts sah): @Override public Animal[][] run() { Animal[][] kopie = super.copyMatrix(); for (int i = 0; i < super.animals.length; i++) { for (int j = 0; j < super.animals[i].length; j++) { if (!animals[i][j].isAlive() && count(i, j) == 3) { kopie[i][j].setLiving(true); } else if (animals[i][j].isAlive() && (count(i, j) < 2 || count(i, j) > 3)) { kopie[i][j].setLiving(false); } } } animals = kopie; return kopie; }

Die Methode, die die Anzahl der Nachbarn prüft ist, wie in der Aufgabenstellung beschrieben, in eine Methode ausgelagert. Hier laufen wir in einer geschachtelten for-Schleife über die acht Nachbarn (und das Lebewesen selbst, was wir am Ende wieder abziehen, falls es lebt und daher gezählt wurde): private int count(int i, int j) { int c = 0; for (int a = -1; a < 2; a++) { for (int b = -1; b < 2; b++) { try { if (super.animals[i + a][j + b].isAlive()) c++; } catch (ArrayIndexOutOfBoundsException x) { // hier soll einfach gar nichts passieren } } } // das lebewesen selbst nicht zaehlen: if (animals[i][j].isAlive()) c--; return c; }

02. Sitzung, 19.04.2007: Datenstrukturen, Listen, Stack Thema: verkettete Listen und andere Datenstrukturen, sowie Einstieg in das Thema Komplexität. Datenstrukturen sind Zusammenfassungen elementarer Datentypen, Listen sind Datenstrukturen, in denen die Elemente linear geordnet sind Stacks und Queues sind einfache Listen, die Einfuegen und Entfernen in konstanter Laufzeit, d.h. O(1), ermoeglichen Ein Stack wird z.B. von einem Kellerautomaten verwendet, der kontextfreie Sprachen verarbeiten kann, z.B. XML oder HTML Ein Stack kann als objektorientierte, rekursive Datenstruktur implementiert werden 03. Sitzung, 26.04.2007: Laufzeit, Stack-Implementierung Thema: Wir haben den letzte Woche besprochenen simplen Stack implementiert:

** * Eine erste Datenstruktur: ein simpler Keller/Stack (eine LIFO-Liste) */ public class SimpleStack { StackElement top; /**

/**

}

* Einfuegen mit Laufzeitk. O(1), d.h. konstant */ public void push(String key) { if (top == null) { top = new StackElement(key); } else { // am anfang einfuegen: StackElement newStart = new StackElement(key); newStart.next = top; top = newStart; } }

* Entfernen mit Laufzeitk. O(1), d.h. konstant */ private String pop() { if (top == null) { throw new IllegalStateException("Empty Stack!"); } // erstes element holen: StackElement toReturn = top; this.top = top.next; return toReturn.key; }

Ein Element eines Stacks: /** * Eine simple rekursive Datenstruktur: Ein Element einer Liste. */ public class StackElement { /** * Jedes Element speichert einen Wert. */ String key;

public StackElement(String key) { this.key = key; } /**

}

* Jeder Element hat einen Nachfolger (einfach verkettete Liste). */ StackElement next;

Die Benutzung erfolgt dann etwa so: public static void main(String[] args) { SimpleStack list = new SimpleStack(); list.push("Hallo"); list.push("Welt"); list.push("!"); System.out.println(list.pop()); System.out.println(list.pop()); System.out.println(list.pop()); }

Mögliche Verbesserungen: Object statt String im Stack; Generics statt Object. 04. Sitzung, 03.05.2007: Collections: Set, List, Map Thema: Collections, Iterable, foreach-Schleifen Iterable und die foreach-Schleife

Die foreach-Schleife ist eine schoene, einfache Konstruktion zum Durchlaufen von allem, was das Interface 'Iterable' implementiert, das sind etwa alle Collection-Klassen (diese implementieren das Interface 'Collection') sowie Arrays, aber auch eigene Klassen koennen Iterable implementieren, z.B. die Waschstrasse mit Autos: for ( Auto a : waschstrasse ) { a. wasche(); } Wenn man jedoch den Index braucht, wird es schnell unschoen und fehleranfaelliger als eine klassische for-Schleife, die ja den Zaehler sauber im Kopf der Schleife kapselt (denkt an die Schwierigkeiten mit Zaehler-Deklaration, wann hochzaehlen und sowas) Collections, Interfaces und Implementationen Die wichtigsten Interfaces der Java Collection Classes sind List (Reihenfolge relevant, doppelte Eintraege erlaubt), Set (Reihenfolge nicht relevant, keine doppelten Eintraege, dt. 'Menge') und Map (Direktzugriffsstruktur, bei der der Schluessel nicht eine Zahl ist, wie bei Arrays oder Listen, sondern etwas beliebiges, z.B. ein String) Verschiedene Datenstrukturen sind fuer vers. Aufgaben geeignet, aber verschiedene Implementierung einer Datenstruktur sind je nach Einsatz der Struktur zu bevorzugen, z.B. LinkedList fuer haeufiges Einfuegen in der Mitte, ArrayList fuer haufigen Zugriff in der Mitte Um eine Austauchbarkeit zu gewaehrleisten sollte daher, wie auch schon im ersten Semester gemacht, immer auf ein Interface programmiert werden, also z.B. List liste = new ArrayList() statt ArrayList list = new ArrayList() 05. Sitzung, 10.05.2007: Iterable und Iterator Wir haben Iterable und Iterator implementiert, um eigene Klassen genau wie die Java Collection Klassen in der for-each-Schleife zu verwenden. Beide Interfaces haben wir in separaten Klassen implementiert. Man kann beide auch in einer einzigen Klasse implementieren (s.u.). /** * Auch das geht: eine einzige Klasse implementiert sowohl Iterable und * Iterator, hier wie oefters mal angedeutet fuer die Waschstrasse. */ public class Waschstrasse implements Iterable, Iterator {

public static void main(String[] args) { Waschstrasse waschstrasse = new Waschstrasse(); waschstrasse.anstellen(new Auto()); waschstrasse.anstellen(new Auto()); // Das ist was man im Endeffekt davon hat: for (Auto a : waschstrasse) { a.waschen(); } } List autos = new ArrayList();

Integer index = 0;

private void anstellen(Auto auto) { autos.add(auto); } /**

/**

* Die hier ist aus Iterable, weil this Iterable implementiert. Da this auch * Iterator implementiert, geben wir this zurueck */ public Iterator iterator() { return this; }

* Die folgenden drei sind aus Iterator */

public Auto next() {

}

// gibt das aktuelle Auto zurueck und zaehlt weiter: Auto auto = autos.get(index); index++; return auto;

public boolean hasNext() { return index < autos.size(); }

public void remove() { // wir haben ja schon in next() hochgezaehlt: autos.remove(index - 1); } } class Auto { public void waschen() { System.out.println("Wasche " + this); }; }

06. Sitzung, 16.05.2007: Binäre Suche und Comparable Binäre Suche ist ein effizienter Algorithmus zur Suche in einer sortierten Liste. Es ist eine Umsetzung der algorithmischen Strategie 'Teile und Herrsche': Es wird nicht einfach von vorne nach hinten alles durchsucht, sondern die Sortierung wird ausgenutzt um grosse Teile der Liste zu ueberspringen. Zur Ermittlung des gesuchten Elements muss dabei das Element mit Elementen in der Liste verglichen werden. Zahlen koennen mit > und < verglichen werden, komplexe Objekte jedoch (alles andere, auch Strings z.B.) werden durch die Implementierung des Interface 'Comparable' vergleichbar. Dies geschieht ueber die compareTo-Methode. Die Reihenfolge spielt dabei eine Rolle, wie beim Vergleich mit < und >. Man kann sich merken: die Elemente und das verwendete Zeichen sind in beiden Faellen gleich, d.h. a < b entspricht a.compareTo(b) < 0 und umgekehrt. Eine eigene Klasse, etwa ein Buch, kann durch die Implementierung von Comparable damit genauso binaer gesucht werden wie ein String (s.u.). /** * Eine kleine Klasse zur Darstellung eines Buchs, implementiert Comparable und * ueberschreibt toString(). Kann durch Comparable wie Strings mithilfe von * compareTo binaer gesucht werden. * */ public class Book implements Comparable { String author;

Integer year;

public Book(String author, Integer year) { this.author = author; this.year = year; }

public String toString() { return this.author + " " + this.year; }

public int compareTo(Book other) { // wenn der Autor gleich ist… if (other.author.equals(this.author)) { // … vergleichen wir das Jahr… return this.year.compareTo(other.year); } // ansonsten vergleichen wir den Autor: return this.author.compareTo(other.author); }

/** * Verwendung der Book-Objekte in einer List, sortieren nach unseren * Kriterien und binaere Suche in der Liste. */ public static void main(String[] args) { Book b1 = new Book("Hans", 1981); Book b2 = new Book("Hans", 1980); Book b3 = new Book("Anna", 1980); Book b4 = new Book("Anna", 1970); /* * Dank der ueberschriebenen toString-Methode kommt hier was * informatives bei raus: */ System.out.println(b1); List list = new ArrayList(); list.add(b1); list.add(b2); list.add(b3); list.add(b4);

/* * Dank der implementiertern compareTo-Methode, sortiert die * sort-Methode in Collections nach den von uns definierten Kriterien: */ Collections.sort(list); /* In der sortierten Liste koennen wir nun binaer suchen: */ int pos = search(b2, list); System.out.println("Eintrag " + b2 + " gefunden an Index " + pos + " in Liste " + list); }

private static int search(Book b1, List list) { int left = 0; /* * Hier lag unser Problem in der letzten Sitzung: als rechte Begerenzung * haben wir den letzen Index genommen (size()-1), aber beim Bilden der * Mitte weiter unten haben wir immer abgerundet (indem wir * Integer-Werte geteilt haben). Das funktioniert auch, allerdings muss * man dann als rechte Begrenzung die Laenge nehmen (size()), sonst wird * durch das Abrunden nie das letzte Element angesehen und die Schleife * bleibt fuer immer beim Vorletzen. */ int right = list.size() - 1; while (left < right) { /* * Wenn man hier immer aufrunden, mithilfe Math.ceil(), klappte es * mit dem letzten Index als rechter Begrenzung. Wenn man oben die * Laenge nimmt, koennte man hier wieder einfach mit Integer-Werten * arbeiten und damit immer abrunden. */ int middle = (int) Math.ceil((left + right) / 2.0); Book current = list.get(middle); if (current.compareTo(b1) == 0) { return middle; } else if (current.compareTo(b1) > 0) { right = middle; } else if (current.compareTo(b1) < 0) { left = middle; } } return -1; } }

07. Sitzung, 24.05.2007: Rekursion: Baumkonstruktion und Traversierung Wir haben einen binaeren Baum konstruiert, anschliessend traversiert und dabei im Dot-Format ausgegeben, aus dem das frei verfuegbare Programm GraphViz eine graphische Darstellung des Baums erstellen kann. Die Struktur unserer Implementierung des Baums als rekursive Datenstruktur:

Implementiert sah das Ganze so aus: public class Tree { public static void main(String[] args) { // Wir erzeugen den Baum und fuegen Knoten ein: Tree tree = new Tree(); tree.add(tree.root); tree.add(tree.root); tree.add(tree.root); tree.add(tree.root); // Export im Dot-Format von GraphViz ( http://www.graphviz.org/ ): System.out.println("digraph {"); tree.export(tree.root); System.out.println("}"); }

}

Node root; // Zaehler fuer die IDs der Knoten: int counter = 1; public Tree() { this.root = new Node(counter); } public void add(Node node) { // wenn links frei ist, dort einfuegen: if (node.left == null) { node.left = new Node(++counter); return; } // wenn rechts frei ist, dort einfuegen: else if (node.right == null) { node.right = new Node(++counter); return; } /** * wenn beides nicht der Fall ist, weiter (hier einfach immer links, * beim binaeren Suchbaum waere hier zu entscheiden, wo es weitergeht. */ add(node.left); } public void export(Node node) { /** * fuer jeden vorhandenen Knoten die Verbindungen zu seinen Kindern, * falls vorhanden, ausgeben: */ if (node != null) { if (node.left != null) System.out.println(node.id + "->" + node.left.id); if (node.right != null) System.out.println(node.id + "->" + node.right.id); } else

}

return; // und dann sowohl links wie rechts weitermachen: export(node.left); export(node.right);

class Node { Node left, right; int id; public Node(int id) { this.id = id; } }

Laesst man das laufen, gibt es folgende Ausgabe: digraph { 1->2 1->3

2->4 2->5 } Dieses Format ist das Eingabeformat fuer das Programm GraphViz ( http://www.graphviz.org ), welches daraus eine graphisch Darstellung generieren kann, fuer diese Ausgabe etwa:

08. Sitzung, 14.06.2007: Sortierverfahren Elementare Sortierverfahren Bubble sort: Immer Nachbarn tauschen, bis alles sortiert ist; Sehr schlechte Laufzeit: O(n^2), nur gut wenn sowieso fast schon sortiert. Im schlimmsten Fall wird n * n also n^2 mal getauscht und verglichen. Unsere Implementierung von Bubblesort aus dem Kurs: private static void bubblesort(List l) { boolean sorted = false; while (!sorted) { sorted = true; for (int i = 0; i < l.size() - 1; i++) { String s1 = l.get(i); String s2 = l.get(i + 1); if (s1.compareTo(s2) > 0) { sorted = false; l.set(i, s2); l.set(i + 1, s1); } } } }

Selection sort: Sortierte Teilliste erweitern durch Suche nach dem naechsten Element in der unsortierten Teilliste; Laufzeit ebenfalls O(n^2), allerdings wird hoechstens n mal getauscht (allerdings werden pro n alle uebrigen Elemente geprueft, daher wird etwa n * n also n^2 mal verlichen). Effizientes Sortieren mit Quicksort Quicksort in CLRS ( http://theory.lcs.mit.edu/~clr/ ): QUICKSORT(A, p, r) if p < r then q