HUMBOLDT-UNIVERSITÄT ZU BERLIN MATHEMATISCH - NATURWISSENSCHAFTLICHE FAKULTÄT II INSTITUT FÜR INFORMATIK ARBEITSGRUPPE SPEZIFIKATION, VERIFIKATION UND TESTTHEORIE PROF. DR. HOLGER SCHLINGLOFF WS 2007/2008 Skriptum zur Vorlesung Praktische Informatik 1 Inhalt Kapitel 0: Einführung ................................................................................................................. 3 0.1 Inhalt der Vorlesung, Organisatorisches, Literatur .......................................................... 3 0.2 Einführungsbeispiel .......................................................................................................... 6 0.3 Was ist Informatik? ........................................................................................................ 13 Kapitel 1: Mathematische Grundlagen ..................................................................................... 17 1.1 Mengen, Multimengen, Tupel, Funktionen, Halbordnungen ......................................... 17 1.2 Induktive Definitionen und Beweise .............................................................................. 20 1.3 Alphabete, Wörter, Bäume, Graphen ............................................................................. 23 Kapitel 2: Informationsdarstellung........................................................................................... 28 2.1 Bits und Bytes, Zahl- und Zeichendarstellungen ........................................................... 28 2.2 Sprachen, Grammatiken, Syntaxdiagramme .................................................................. 35 2.3 Darstellung von Algorithmen ......................................................................................... 40 Kapitel 3: Rechenanlagen......................................................................................................... 45 3.1 Historische Entwicklung ................................................................................................ 45 3.2 von-Neumann-Architektur ............................................................................................. 53 3.3 Aufbau PC/embedded system, Speicher ........................................................................ 57 Kapitel 4: Programmiersprachen und –umgebungen ............................................................... 61 4.1 Programmierparadigmen ................................................................................................ 61 4.2 Historie und Klassifikation von Programmiersprachen ................................................. 64 4.3 Java ................................................................................................................................. 65 4.4 Programmierumgebungen am Beispiel Eclipse ............................................................. 66 Kapitel 5: Applikative Programmierung .................................................................................. 66 5.1 einfache, geschachtelte Rekursion, Terminierung ......................................................... 66 5.2 rekursive Datenstrukturen .............................................................................................. 66 5.3 Methoden und Parameterübergabemechanismen ........................................................... 66 Kapitel 6: Konzepte imperativer Sprachen .............................................................................. 67 6.1 Variablen, Datentypen, Ausdrücke ................................................................................ 67 6.2 Anweisungen und Kontrollstrukturen ............................................................................ 71 6.3 Sichtbarkeit und Lebensdauer von Variablen ................................................................ 72 Hinweise zur Nutzung dieses Skriptes: Achtung, drucken Sie das Skript (noch) nicht aus! Es wird parallel zur Vorlesung erstellt und laufend aktualisiert. Wenn Sie jetzt schon ihren Drucker bemühen, ist das entweder eine Verschwendung von Papier (weil Sie es später nochmal drucken) oder sie erhalten ein evtl. inkonsistentes Dokument (wenn Sie verschiedene Teile zusammenheften). Mit Abschluss des Semesters wird Ihnen das gesamte Skript zur Verfügung stehen! Dieses Dokument enthält außerdem Hyperlinks zu weiteren Quellen im Internet, die in Word oder mit dem Acrobat Reader durch Anklicken abrufbar sind. Da solche Hyperlinks schnell veralten, sind sie nur mit einem (*) gekennzeichnet. HS, 21.10.2007 Kapitel 0: Einführung 0.1 Inhalt der Vorlesung, Organisatorisches, Literatur Die Vorlesung „Praktische Informatik 1“ hat laut der Studienordnung Diplom Informatik aus dem Jahr 2003 folgende Lern- und Qualifikationsziele: 1. Grundlagen: Grammatiken; von-Neumann-Rechner; Klassifikation von Programmiersprachen 2. Softwareentwicklung: SW-Qualitätsmerkmale; Phasen und Dokumente; Abstraktion und Dekomposition; SWArchitektur: UML; Komponentenarten; Fallbeispiele 3. Konzepte (imperativer) Programmiersprachen: Variablen: Sichtbarkeit, Lebensdauer; Datentypen (einfach, strukturiert); Ausdrücke; Prioritäten; Anweisungen (einfach, strukturiert); Methoden als Abstraktion; Parameter: value~, reference~; Rekursion – Iteration; Sprachvergleich +Kritik. 4. Datenstrukturen und Algorithmen: Listen, Bäume, Sortieren und Suchen, Komplexität 0(n) von Algorithmen. 5. Konzepte der Objektorientierung: ADT-Objekte-Klassen, Vererbung, Sichtbarkeit, Klassenvariablen, ~methoden, abstrakte Klassen, Überladung, Polymorphie; dynamisches Binden, Ausnahmebehandlung; Ereignisse, API (ausgewählte Klassen); Applets 6. Programmierfertigkeiten: Typische Programmbeispiele Daraus ergeben sich im kommentierten Vorlesungsverzeichnis dieses Semesters folgende Inhalte der Vorlesung: Mathematische Grundlagen; Informationsdarstellung und Informationsverarbeitung; Rechnerarchitekturen; Programmiersprachen und -paradigmen; objektorientierte Programmierung; Datenstrukturen und Algorithmen; Softwaretechnik; Modellierung und Systementwicklung Die Vorlesung (4 SWS) ist nur mit begleitender Übung (2 SWS), Praktikum (2 SWS), Selbststudium, Vorlesungsmitschrift, Hausaufgaben (in Gruppen bearbeitet, korrigiert und bewertet, in der Übung besprochen) sinnvoll. Als Prüfung findet eine Abschlussklausur statt; die Zulassung zur Klausur ist an die Erreichung einer bestimmten Punktzahl in Übungen und Praktikum gebunden. Für die erfolgreiche Teilnahme gibt es 12 Studienpunkte nach dem ECTS-System (European Credit Transfer System). Literaturhinweise Leider gibt es kein einzelnes Buch, welches genau den Stoff der Vorlesung enthält. Als Literatur zur Vorlesung empfehlen ich das Buch von Gumm und Sommer. (Aktuell: 7. Auflage; die 6. Auflage gibt es zum Teil im Sonderangebot) Als Ergänzung dazu ist nachfolgend eine Liste relevanter Lehrbücher angegeben.. Lehrbücher: Einführung in die Informatik M. Broy: Informatik, eine grundlegende Einführung. Band 1: Programmierung und Rechnerstrukturen, Band 2: Systemstrukturen und theoretische Informatik. SpringerLehrbuch (+ Aufgabensammlung) (Monumentalwerk) P. Levi, U. Rembold: Einführung in die Informatik für Naturwissenschaftler und Ingenieure, Hanser, (konzise) G. Goos: Vorlesungen über Informatik (vier Bände: Bd. 1: Grundlagen und funktionales Programmieren, Bd. 2: Objektorientiertes Programmieren und Algorithmen, Bd. 3: Berechenbarkeit, formale Sprachen, Spezifikationen, Bd. 4: Paralleles Rechnen und nicht-analytische Lösungsverfahren) F. L. Bauer, G. Goos: Informatik - Eine einführende Übersicht, 4. Auflage. Bd. 1+2, Springer („der Klassiker“, etwas veraltet) L. Goldschlager, A. Lister: Informatik, Eine moderne Einführung. Hanser Studienbücher, (ebenfalls etwas veraltet; in der Bibliothek 40 Ex. vorhanden) F. Kröger: Einführung in die Informatik – Algorithmenentwicklung. Springer Lehrbuch(formale Grundlagen, keine OO-Konzepte, als Ergänzung empfohlen) H. Balzert: Lehrbuch Grundlagen der Informatik (als Ergänzung der Vorlesungsthemen) H.-J. Appelrath, J. Ludewig: Skriptum Informatik - Eine konventionelle Einfürung. Teubner/VdF (+ Aufgabensammlung) (Betonung auf „konventionell“!) A. Aho, J. Ullman: Informatik - Datenstrukturen und Konzepte der Abstraktion. (deutsche Fassung von: Foundations of Computer Science) Thomson Publishing / Computer Science Press (für Fortgeschrittene) R. Sedgewick: Algorithmen in Java (auch für Fortgeschrittene) Lehrbücher: Einführung in Java In der Vorlesung „praktische Informatik 1“ und besonders im zugehörigen Praktikum sollen auch Programmierkenntnisse unterrichtet werden. Trotzdem ist diese Veranstaltung auf keinen Fall ein Programmierkurs; es wird erwartet, dass sich die Studierenden selbständig in programmiersprachliche Details an Hand geeigneter Lehrmaterialien (Bücher oder Online-Handbücher) einarbeiten. Von der nachstehenden Einführungsliteratur empfehle ich das Buch von Barnes und Kölling über die Java-BlueJProgrammierumgebung. Als Ergänzung dazu kann entweder das Buch von Bell und Parr oder das von Bishop dienen. Wer sich intensiver mit Java auseinander setzen möchte, dem sei das Buch von Gosling als ultimative Referenz empfohlen. J. Bishop: Java lernen (dt. Ausgabe von Java Gently), Addison Wesley / Pearson, 2. Aufl. 2001 (südafrikanisches Flair) K. Arnold, J. Gosling, D. Holmes: Die Programmiersprache Java (dt. Ausgabe von The Java Programming Language). Addison-Wesley 1996 („die Referenz“) D. Barnes, M. Kölling: Objektorientierte Programmierung mit Java (deutsche Ausgabe von: Objects First with Java - A Practical Introduction using BlueJ). Prentice Hall / Pearson, Sept. 2003 (für Vorlesung empfohlen) D. Bell, M. Parr: Java für Studenten – Grundlagen der Programmierung. Prentice Hall / Pearson, 3. Aufl. 2003 (systematischer Java-Lehrgang) E.-E. Doberkat, S. Dißmann: Einführung in die objektorientierte Programmierung mit Java. Oldenbourg-Verlag, 2. Auflage 2002 (Wiener Hofzwerge betreiben Informatik) H. W. Lang: Algorithmen in Java. Oldenbourg-Verlag 2003 Küchlin, Weber: Einführung in die Informatik – Objektorientiert mit Java 0.2 Einführungsbeispiel Um sich der Frage zu nähern, was eigentlich praktische Informatik ist, betrachtet man am besten ein Beispiel. Ein bekanntes Problem der Informatik ist das Problem des Handlungsreisenden (Travelling Salesman Problem, TSP). Ein Handlungsreisender muss bei seiner Arbeit Kunden besuchen, die in verschiedenen Orten wohnen. Aufgabe der Sekretärin ist es nun, eine Tour zu planen, die ihn zu jedem Kunden genau einmal führt und am Schluss wieder zum Ausgangspunkt zurück bringt. Natürlich sollte die Tour optimal sein, d.h., möglichst wenig Ressourcen verbrauchen. Abhängig davon, welchen Begriff von Ressource man zu Grunde legt, gibt es verschiedene Varianten der Aufgabenstellung. Beim allgemeinen TSP gehen wir davon aus, dass der Handlungsreisende z.B. mit dem Flugzeug unterwegs ist: da es nicht immer von jedem beliebigen Punkt zu jedem anderen eine direkte Flugverbindung gibt, muss die Sekretärin das Streckennetz der Fluggesellschaften und die jeweiligen Flugzeiten (oder Ticketpreise) berücksichtigen. Beim euklidischen TSP bewegt sich der Handlungsreisende zu Fuß oder mit dem Auto, d.h. er kommt überall hin und die Kosten sind proportional zu den Entfernungen zwischen den Punkten auf der Landkarte. Das metrische TSP ist eine Verallgemeinerung des euklidischen und ein Spezialfall des allgemeinen TSP: zwischen je zwei Punkten besteht eine Verbindung, und die Dreiecksungleichung gilt (die Summe der Kosten von A nach B und der Kosten von B nach C ist größer gleich der Kosten von A nach C). Hier ist ein Beispiel für das allgemeine TSP: Die beiden angegebenen Lösungen haben die Länge 33 und 22. Welches ist die optimale Tour? Hier ist ein Beispiel für das euklidische TSP (nach *): Dieses euklidische TSP hat 250 Punkte. Angegeben sind eine Näherungslösung (Weglänge 12,91) und eine optimale Lösung (Weglänge 12,48). Was ist nun eine geeignete Methode, um das Problem zu lösen? Eine häufige Herangehensweise der Informatik ist es, ein Problem auf ein einfacheres zurückzuführen. Wir wissen, wie die Lösung eines TSP mit nur 2 Punkten (Firmensitz und ein Kunde) aussieht: Der Handlungsreisende muss hin- zurückfahren. Angenommen, wir wissen, wie wir eine Tour mit n Punkten löst. Dann kommen wir zu einer Lösung für (n+1) Punkten, indem wir einen beliebigen Punkt zunächst weglassen, eine optimale Tour für n Punkte konstruieren, und den weggelassenen Punkt dann als erstes Ziel in die Tour einfügen. Das ist natürlich noch nicht die optimale Lösung, aber wenn man das für alle Punkte der Reihe nach macht und die Tour mit der minimalen Länge wählt, bekommt man dadurch das Optimum. Diesen Algorithmus könnte man etwa wie folgt notieren: Wir nehmen an, der Startpunkt (Firmensitz) sei fest gegeben und notieren eine Tour durch die Folge der zu besuchenden Punkte (ohne den Startpunkt S) und durch die zugehörige Länge. Tour minTour (Punktmenge M) { Wenn M einelementig (M={A}) dann Rückgabe ((A, 2* |SA|)) Sonst { // Berechne die kürzeste Tour rekursiv Sei minT eine neue Tour, wobei zunächst Punkte (minT) = undefiniert, Länge(minT)=unendlich; Für jedes x aus M { Tour rek = minTour (M-{x}); Sei A der erste Ort von rek; Sei Tour try gegeben durch Punkte (try) = append(x, Punkte(rek))); Länge (try) = Länge(rek) - |SA| + |Sx| + |xA|; Wenn Länge(try) < Länge(minT) {minT=try}; } Rückgabe minT; } } Wenn wir den Ablauf dieser rekursiven Funktion z.B. für die Punkte {ABC} betrachten, stellen wir folgende Aufrufe fest: minTour ({ABC}) ruft minTour({BC}) ruft minTour({C}) und minTour({B}). minTour({AC}) ruft minTour({C}) und minTour({A}). minTour({AB}) ruft minTour({B}) und minTour({A}). Das bedeutet, ein Aufruf mit n Punkten stützt sich auf n Aufrufe mit (n-1) Punkten ab, jeder davon stützt sich auf (n-1) Aufrufe mit (n-2) Punkten ab usw. Insgesamt gibt es n n * (n - 1) * (n - 2) * * 3 * 2 *1 i n! i 1 Aufrufe. Im Wesentlichen konstruiert der Algorithmus sämtliche Permutationen der Folge der Punkte. Die Anzahl der Permutationen von n Elementen ist n!. Wir sagen, der Algorithmus hat die Komplexität O(n!). Nachfolgende Tabelle gibt einen Überblick über das Wachstum verschiedener Funktionen. n 1 2 3 10 50 256 1000 10000 log(n) 256*n n2 17*n3 2n n! nn 0,0 256 1 17 2 1 1 0,3 512 4 136 4 2 4 0,5 768 9 459 8 6 27 1,0 2560 100 17000 1024 3628800 10000000000 1,7 12800 2500 2125000 1,1259E+15 3,04141E+64 8,88178E+84 2,4 65536 65536 285212672 1,15792E+77 #ZAHL! #ZAHL! 3,0 256000 1000000 1,7E+10 1,0715E+301 #ZAHL! #ZAHL! 4,0 2560000 100000000 1,7E+13 #ZAHL! #ZAHL! #ZAHL! Wenn wir den Algorithmus auf einem schnellen Pentium DualCore ausführen, der bis zu 4 Milliarden Operationen pro Sekunde ausführt, dann müssen wir für n=10 etwa eine Millisekunde warten. Für n=50 erhöht sich unsere Wartezeit allerdings auf etwa 1028=3.000.000.000.000.000.000.000.000.000 oder 3 Quatrilliarden Jahre. Das heisst, für große Werte von n ist der Algorithmus praktisch nicht verwendbar. Auf der anderen Seite ist es ein offenes Problem, ob es tatsächlich einen substantiell besseren Algorithmus gibt! Wer einen Algorithmus entdeckt, welcher das TSP mit einer polynomialen Anzahl von Schritten löst (abhängig von n), wird mit Sicherheit weltberühmt werden. Natürlich gibt es einige Tricks, um die Laufzeit des Algorithmus zu verbessern. Zum Beispiel fällt auf, das während der Rekursion der gleiche Aufruf mehrmals stattfindet. Hier kann man sich mit dem Abspeichern von Zwischenergebnissen behelfen. Dann kann man die Suche stark parallelisieren. Auch hängt es sehr stark von der Implementierung ab, ob man den rekursiven Aufruf naiv oder raffiniert implementiert. Mit solchen Tricks ist es vor kurzem (im Jahr 2004) gelungen, ein TSP mit 24.978 schwedischen Städten zu lösen(*)! Der Weltrekord liegt laut Wikipedia (*) bei der Lösung eines Planungsproblems für das Layout integrierter Schaltkreise mit 33.810 Knoten. Zur Lösung solcher Probleme werden Netze von über hundert Hochleistungscomputern mit einer Gesamtsumme von über 20 Jahren Rechenzeit verwendet. Zum Vergleich: Im Jahr 1977 lag der Rekord noch bei 120 Städten! Was macht man aber nun, wenn man das Problem in der Praxis (zum Beispiel in einer mittelständischen Reisebüro-Software) lösen will, wo man kein Hochleistungs-Rechnernetz zur Verfügung hat? Die Antwort der praktischen Informatik heisst Heuristik. Das Wort „Heuristik“ kommt aus der Seefahrt und bezeichnete früher Verfahren, um seine Position annähernd zu bestimmen. Heute bezeichnen wir damit Näherungsverfahren, die zwar nicht die optimale Lösung, aber eine hinreichend genaue Annäherung liefern. Heuristik 1: nächster Nachbar Bei dieser Heuristik sucht der Reisende, ausgehend von einem beliebigen Punkt, zunächst den nächsten Nachbarn des aktuellen Punktes auf der Landkarte. Dann bewegt er sich zu diesem und wendet die Heuristik erneut an. Wenn er alle Kunden besucht hat, fährt er nach Hause zurück. Obwohl diese Heuristik sehr häufig im täglichen Leben angewendet wird, liefert sie schlechte Resultate, da der Heimweg und andere unterwegs „vergessene“ Punkte meist hohe Kosten verursachen. Heuristik 2: gierige Dreieckstour-Erweiterung Die Sekretärin wählt zunächst zwei Kunden willkürlich aus und startet mit einer einfachen „Dreieckstour“. Diese wird dann nach und nach erweitert, und zwar wird jede Stadt da in die Tour eingefügt, wo sie am besten „passt“, d.h., wo sie die gegebene Tour am wenigsten erweitert. Solche Strategien nennt man „gierig“, da sie nur auf lokale und Optimierung ausgerichtet sind und das Gesamtergebnis nicht berücksichtigen. In der geschilderten Form sind noch zwei Zufallselemente enthalten: Die Wahl der ursprünglichen Dreieckstour, und die Reihenfolge, in der die Knoten hinzugefügt werden. Heuristik 3: Hüllen-Erweiterung Eine andere Variante dieser Heuristik startet mit der konvexen Hülle aller Punkte (d.h., den Punkten, die am weitesten außen liegen), und fügt der Reihe nach diejenigen Knoten hinzu, die die wenigsten Kosten verursachen. Das heisst, es werden der Reihe nach die Knoten hinzugefügt, die den geringsten Abstand von der bisher konstruierten Tour haben. Das ist wieder ein „gieriger“ Algorithmus, und zwar in doppelter Hinsicht: bei der Auswahl der Punkte und bei der Bestimmung der Einfügestelle. Heuristik 4: inverse Hüllen-Erweiterung Wieder starten wir mit der konvexen Hülle aller Punkte und fügen der Reihe nach diejenigen Knoten hinzu, die die meisten Zusatzkosten verursachen. Das heisst, es werden die Knoten hinzugefügt, die am weitesten von der bisherigen Tour entfernt liegen. Dadurch wird das Gesamtergebnis (die Form der Tour) frühzeitig stabilisiert. Heuristik 5: Lin-Kernighan Hier versucht man, eine bestehende Tour zu verbessern, indem man zwei Kantenpaare AB und CD sucht und durch die Kanten AC und BD ersetzt (so genannte 2-opt-Strategie). Im endgültigen Lin-Kernighan-Verfahren werden nicht nur Kantenpaare, sondern Mengen von Kanten ersetzt. Wie wir sehen, führen konträre Ideen manchmal beide zu guten Ergebnissen. Es ist sehr schwer, die „Güte“ von Heuristiken abzuschätzen, da die Leistung immer vom verwendeten Beispiel abhängt. Für jede der genannten Heuristiken lassen sich Beispiele konstruieren, so dass das Ergebnis schlecht ist (die doppelte Länge der optimalen Tour oder noch mehr hat). Ist es nicht möglich, eine Heuristik zu finden, die „auf jeden Fall“ ein akzeptabeles Resultat liefert? Auch hier hat die praktische Informatik Beiträge zu liefern. S. Arora konstruierte 1996 einen „nahezu linearen“ randomisierten Algorithmus für das euklidische TSP, der für eine beliebige Konstante c eine (1+1/c)-Approximation in O(n log(n)O(c)) Zeit konstruiert (*). Das heisst, z.B. für c=10 bekommt man eine Tour, die höchstens 10% schlechter als die optimale ist, indem wir jeden Knoten durchschnittlich log(n)10 mal betrachten. Wie wir oben gesehen haben, ist log(n) „fast“ eine Konstante (in allen praktischen Fällen kleiner als 5). Daher ist dies „fast“ ein linearer Algorithmus. Der Algorithmus basiert auf raffinierten geometrischen Überlegungen, nämlich einer baumartigen Zerlegung der Ebene in Quadrate und einer Normierung der Schnittkanten der Verbindungslinien zwischen den Punkten. 0.3 Was ist Informatik? Das Wort „Informatik“ ist ein Kunstwort, welches aus den Bestandteilen „Information“ und „Automatik“ zusammengesetzt ist. Der Begriff „Informatique“ wurde 1962 in Frankreich von P. Dreyfuss geprägt und 1968 vom Forschungsminister Stoltenberg in Berlin bei der Eröffnung einer Tagung übernommen.(*, *). Da die Informatik also eine vergleichsweise junge Wissenschaft ist, die eine stürmische Entwicklung durchläuft, gibt es die verschiedensten Definitionen davon, was unter Informatik zu verstehen ist. Als Beispiel sei hier die Studienordnung der HU von 2003 angeführt: (1) Die Informatik erforscht die grundsätzlichen Verfahrensweisen der Informationsverarbeitung und die allgemeinen Methoden der Anwendung solcher Verfahren in den verschiedensten Bereichen. Ihre Aufgabe ist es, durch Abstraktion und Modellbildung von speziellen Gegebenheiten sowohl der technischen Realisierung existierender Datenverarbeitungsanlagen als auch von Besonderheiten spezieller Anwendungen abzusehen und dadurch zu den allgemeinen Gesetzen, die der Informationsverarbeitung zugrunde liegen, vorzustoßen sowie Standardlösungen für Aufgaben der Praxis zu entwickeln. Die Informatik befasst sich deshalb mit — der Struktur, der Wirkungsweise, den Fähigkeiten und den Konstruktionsprinzipien von Informations- und Kommunikationssystemen und ihrer technischen Realisierung. — Strukturen, Eigenschaften und BeschreibungsmögIichkeiten von Informationen und von Informationsprozessen, — Möglichkeiten der Strukturierung, Formalisierung und Mathematisierung von Anwendungsgebieten sowie der Modellbildung und Simulation. Dabei spielen Untersuchungen über die Effizienz der Verfahren und über Sinn und Nutzen ihrer Anwendung in der Praxis eine wichtige Rolle. Andere Fachbereiche haben ähnliche Festlegungen des Studieninhaltes. Als minimaler Konsens für den Begriff „Informatik“ kann dabei die Definition angesehen werden, welche sich aus der Wortbedeutung ergibt: Informatik ist die Wissenschaft der automatischen Verarbeitung von Informationen. (Im Buch von Gumm/Sommer: „Informatik ist die Wissenschaft von der maschinellen Informationsverarbeitung“.) In dieser Definition sind ein paar weitere undefinierte Grundbegriffe enthalten: „Information“, „Verarbeitung“, „automatisch“ oder „maschinell“. Der Begriff „Information“ ist ein metaphysischer Grundbegriff, mit dem wir uns noch näher beschäftigen werden. An dieser Stelle sei nur bemerkt, dass wir unter „Information“ eine Beschreibung irgendeines Sachverhaltes der uns umgebenden (materiellen oder ideellen) Welt verstehen wollen. Vom Wortstamm her ist eine „Information“ etwas, was in eine bestimmte Form gebracht worden ist, also auf eine bestimmte Weise repräsentiert wird (wenn wir eine Information zur Kenntnis nehmen, bringen wir unseren Verstand in einen bestimmten Zustand). Sehr einfache, wenig strukturierte Informationen bezeichnen wir als Daten (daher der altertümliche Begriff „EDV“), eine Menge komplexer Informationen über ein zusammenhängendes Gebiet bezeichnen wir als Wissen. Unter der „Verarbeitung“ von Informationen verstehen wir den Prozess der Umformung, d.h. der Veränderung von Informationen aus einer Form in eine andere. Da Informatik sich als Wissenschaft mit der Verarbeitung von Informationen beschäftigt, sind ihr Gegenstand die Verfahren, mit denen diese Umformung bewerkstelligt wird: solche Verfahren nennt man Algorithmen. Häufig erfolgt heutzutage der räumliche Transport von Informationen dadurch, dass sie beim Absender in eine einfachere Form gebracht (codiert) werden, durch einen elementaren physikalischen Prozess (elektrische Ströme, Funkwellen etc.) übermittelt und beim Empfänger wieder in die ursprüngliche Form zurückübersetzt (decodiert) werden. Daher betrachtet man heute auch die Erforschung von Techniken zur Übertragung von Informationen als Teilgebiet der Informatik. Der dritte undefinierte Grundbegriff aus der obigen Definition ist „automatisch“. Damit soll ausgedrückt werden, dass sich die Informatik nicht mit der Informationsverarbeitung durch Menschen oder andere Lebewesen beschäftigt, sondern durch Automaten, d.h. vom Menschen konstruierte Maschinen. Daher gehört zur Informatik auch das Wissen um den Aufbau und die Entwicklung von Technologien zur Konstruktion informationsverarbeitender Geräte. Aus historischen Gründen bezeichnet man diese Geräte oft auch als „Rechner“ (numerische Berechnungen waren die ersten automatisierten informationsverarbeitenden Prozesse) oder „Computer“. Daher hat sich im Englischen der Begriff „computer science“ für die Informatik durchgesetzt. Dieser Begriff ist allerdings etwas missverständlich, da er suggerieren könnte, dass Informatik die „Wissenschaft von den informationsverarbeitenden Geräten“ ist. Einige Leute verwenden daher die Bezeichnung „computing science“. Aus der Bestimmung des Gegenstands der Informatik ergibt sich unmittelbar, dass die Informatik viele Bezüge zu anderen Disziplinen hat: Der Gleichklang zum Wort „Mathematik“ ergibt sich nicht von ungefähr. Man kann mit Fug und Recht behaupten, dass die Informatik aus der Mathematik erwachsen ist, so wie die Mathematik ihre Wurzeln in der philosophischen Logik hat. Abgesehen davon, dass die Automatisierung numerischer Berechnungen schon immer ein ureigenstes Interesse der Mathematik war, ist auch die Beschäftigung mit abstrakten Begriffen wie „Berechnungsverfahren“ oder „Umformung“ ein Gegenstand der Mathematik. Viele Pioniere der Informatik (Pascal, Leibniz, Babbage, Turing, von Neumann, …) waren Mathematiker oder Logiker und haben sich mit den theoretischen Grundlagen automatischer Berechnungsverfahren beschäftigt, bevor es überhaupt Computer gab. Die zweite Wurzel der Informatik ist die Elektrotechnik. Erst durch den Einsatz elektrischer Schaltungen und Verfahren nach dem zweiten Weltkrieg (durch Zuse, Aiken und andere) wurde gegenüber den davor existierenden mechanischen Rechnern (Hollerith) ein so großer Durchbruch erzielt, dass man über numerische Rechnungen hinausgehen konnte. Da praktisch alle heute existierenden informationsverarbeitenden Prozesse in Automaten auf der Bewegung von elektrischen Ladungen beruhen, ist klar, dass auch heute noch eine enge Verwandschaft zwischen Informatik und Elektrotechnik besteht. Auf der anderen Seite gehören Computer heute mit zu den wichtigsten strombetriebenen Geräten, weshalb sich die Elektrotechnik heute gerne auch als „Informationstechnik“ bezeichnet, Durch die Inhalte der Informatik ergeben sich weiterhin eine ganze Reihe von Querbezügen zu anderen Disziplinen. Wenn Informationsverarbeitung zur Steuerung mechanischer Geräte, etwa von Robotern oder Fertigungsstraßen, genützt wird, müssen Informatiker mit Maschinenbauern und Produktionstechnikern zusammenarbeiten. Durch den Einsatz von Computern zur Übertragung von Informationen per Funkwellen ist eine enge Beziehung zur Nachrichtentechnik gegeben. Wegen der Notwendigkeit der Interaktion von Automaten und Menschen (auf Benutzungs- und Konstruktionsebene) muss die Informatik auf Grundlagen und Ergebnisse der Psychologie, Linguistik, Kommunikationswissenschaften und anderer Fächer zurückgreifen. Da in fast allen Fächern informationsverarbeitende Prozesse vorkommen, die bislang entweder von Menschen durchgeführt wurden oder wegen des hohen Arbeitsaufwandes gar nicht durchgeführt werden konnten, werden Methoden der Informatik in diesen Fächern für die Automatisierung der Verarbeitung von Informationen angewendet. Dadurch haben sich eine Reihe spezialisierter Studiengänge gebildet, die so genannten Bindestrich-Informatiken, die die Anwendung der Informatik in anderen Fächern betonen. Zu nennen sind hier Wirtschafts-Informatik, Bio-Informatik, Medien-Informatik, Geo- Informatik, Umwelt-, Rechts- oder Medizininformatik, und viele mehr. Wichtig: Mit einer Ausbildung als Diplom-Informatiker kann man sich später für jede dieser Disziplinen weiter qualifizieren! Aus den geschilderten Wurzeln ergibt sich die heute übliche Struktur der Informatik: Man teilt sie ein in theoretische Informatik praktische Informatik technische Informatik, und angewandte Informatik. Die theoretische Informatik beschäftigt sich mit den formalen Grundlagen, die praktische Informatik mit den Verfahren, die technische Informatik mit den Maschinen zur Verarbeitung von Informationen. In der angewandten Informatik werden die Anwendungen der Informationsverarbeitung für andere Fächer (z.B. Robotik, Bioinformatik, medizinische Bildverarbeitung) untersucht. Oftmals wird die angewandte Informatik als Teil der praktischen Informatik betrachtet; an einigen Universitäten studiert man im Grundstudium zunächst ein beliebiges Nebenfach und spezialisiert sich dann im Hauptstudium auf die angewandte Informatik in diesem Nebenfach. Geschichte der Informatik Informatik im eigentlichen Sinne gibt es erst seit dem Endes des zweiten Weltkrieges. Die Wurzeln der Informatik reichen dagegen bis ins Mittelalter bzw. ins Altertum zurück: 300 v. Chr: Euklid entwickelt sein Verfahren zur Bestimmung des größten gemeinsamen Teilers (ggT) um 820: Al-Chwarizmi fasst in einem Buch Lösungen zu bekannten mathematischen Problemen zusammen. 1524: A. Riese veröffentlicht ein Buch über die Grundrechenarten 17-18.Jh.: G. W. Leibniz (1646-1714) entwickelt das Dualsystem (1679) und baut eine Rechenmaschine (1673/1694), Pascal, Schickard u.a. entwickeln ebenfalls mechanische Rechenmaschinen, Babbage konzipiert „difference engine“, Ada Lovelace die erste Programmiersprache dafür Ende 19./ Anf. 20. Jh.: Formalisierung der logischen und mathematischen Grundlagen durch Frege (“Begriffschrift”, 1879), Russell u. Whitehead (“Prinicipia mathematica", 1910-13), Peano u.a. Ende 19./ Mitte 20. Jh.: Perfektionierung mechanischer Rechenmaschinen; 1930-40: Theorie der Berechenbarkeit, Vollständigkeits- und Entscheid-barkeitssätze (Gödel, Turing, Tarski, Church, Kleene, Post, Markov u.a.) 1930-40: erste elektromechanische Computer: Zuses Z1 (1936), Z3 (1943), Aikens Mark1 (1944), Eckert+Mauchlys ENIAC (1946) 1948-49: Konrad Zuse entwickelt seinen “Plankalkül”, C. Shannon seine “Informationstheorie”, J. v. Neumann entwickelt den nach ihm benannten Rechnertyp: Daten und Befehle werden gemeinsam im Rechner gespeichert und ähnlich behandelt. 1955: Erfindung des Transistors 1959-60: erste “höhere Programmiersprachen”: J. McCarthy entwickelt die funktionale Programmiersprache LISP und begründet die “Artificial Intelligence”, Fortran (Masch.bau), Cobol (BWL) und Algol (Math) werden definiert. 1969-70: Entwicklung universaler Programmiersprachen wie Algol68 und PL/I ab 1970: Informatikstudium in Deutschland ab 1980: Objektorientierte Sprachen und Systeme ab 1990: Internet (Gopher, Mosaic), Mobilfunk (1991 D-Netz, 1994 E-Netz), WLAN (ca. 1995), PDAs (1997), Java (1995), Java2 (2002) Die Informatik ist heute in fast alle Aspekte unseres Lebens vorgedrungen: Einen Großteil ihres Studiums werden Sie mit „e-Learning“ verbringen, die notwendigen Informationen beschaffen Sie sich im Internet. Vielleicht bestellen Sie hier auch Waren oder vergleichen zumindest bei eBay die Preise. Dokumente (Briefe, Steuererklärungen usw.) verfassen Sie natürlich am Computer; mit Ihren Kommilitonen nebenan und dem Onkel in Amerika tauschen Sie e-Mails aus, die den Empfänger in wenigen Sekunden erreichen. Wahrscheinlich haben Sie auch ein Mobiltelefon, vielleicht sogar ein Notebook mit WLAN, oder einen elektronischen Organizer. Ihr Bankkonto wird von einem Computer geführt, Bargeld holen Sie am Geldautomaten, vielleicht habe Sie auch eine elektronische Brieftasche. Wenn Sie mit dem Auto nach Hause fahren, begleiten Sie bis zu 80 eingebaute Steuergeräte, das Auto ist weitgehend von Robotern gebaut worden. Sogar die Schnittmuster Ihrer Kleidung wurden vom Computer optimiert. Ihre Armbanduhr, Foto- oder Filmapparat, Ton- und Bildwiedergabegeräte sind schon längst nicht mehr mechanisch, ganz zu schweigen von der Türschließanlage, Fahrstuhlsteuerung, Kühlschrank, Mikrowelle, Waschmaschine, und anderen Geräten zu Hause. Das ist aber keinesfalls das Ende der Entwicklung. In ein paar Jahren wird Sie die Türschließanlage an Ihrem Aussehen und Fingerabdruck erkennen, Sie werden sich vielleicht wie im Roman „per Anhalter durch die Galaxis“ mit dem Fahrstuhl unterhalten, der Kühlschrank wird Vorräte selbsttätig nachbestellen, der Herd wird sich Rezepte aus dem Internet holen und die Waschmaschine wird wissen, wie heiß die Wäsche gewaschen werden muss. Alle diese „Wunder der Technik“ werden möglich durch systematische Vorschriften für die Verarbeitung von Informationen (Algorithmen, Programme) und Maschinen, die diese Vorschriften ausführen können (Computer, Prozessoren). Natürlich können wir uns in einer Vorlesung nicht mit allen oben genannten Anwendungen beschäftigen, aber die zentralen Gesichtspunkte die in allen gleichermaßen vorhanden sind, bilden den Gegenstand der Vorlesung: Algorithmen und ihre Ausführung auf Rechenanlagen. Das zentrale Ziel der Vorlesung ist es, eine „algorithmische Denkweise“ zu vermitteln: Ein Verständnis dafür, wann und wie ein (informationsbezogenes) Problem mit welchem Aufwand durch eine Maschine gelöst werden kann. Inhaltlich gibt die Vorlesung einen Überblick über das Gebiet der praktischen Informatik. Dazu gehören unter anderem folgende Themen: Repräsentation von Informationen in Rechenanlagen programmiersprachliche Konzepte Methoden der Softwareentwicklung Algorithmen und Datenstrukturen Korrektheit und Komplexität von Programmen In Teil 2 der Vorlesung (im Sommersemester) werden diese Themen ergänzt und vertieft. Kapitel 1: Mathematische Grundlagen 1.1 Mengen, Multimengen, Tupel, Funktionen, Halbordnungen Eine Menge ist eine Zusammenfassung von (endlich oder unendlich vielen) verschiedenen Dingen unserer Umwelt oder Vorstellungswelt, welche Elemente dieser Menge genannt werden. Wir schreiben x M , um auszusagen, dass das Ding x Element der Menge M ist. Andere Sprechweisen: x ist in M enthalten, oder M enthält x . Um auszudrücken, dass x nicht in M enthalten ist, schreiben wir x M . Die Schreibweise x1 ,..., xn M steht für x1 M und … und xn M , und {x1 ,..., xn } ist die Menge, die genau die Elemente x1 ,..., xn enthält. Beispiele für Mengen sind: : Die Menge der natürlichen Zahlen 1,2,3,… 0: Die Menge der Kardinalzahlen (natürlichen Zahlen einschließlich der Null): 0,1,2,3,… : Menge der ganzen Zahlen (integer) …, -3, -2, -1, 0, 1, 2, 3, … , : Menge der rationalen bzw. reellen Zahlen oder { }: leere Menge oder boolean = {true, false} oder {1,0} oder {tt, ff} oder {w, f} oder {L,O}: Menge der Wahrheitswerte byte = {-128,…,127}: Menge der ganzen Zahlen zwischen -128 und 127 {Adam, Eva}: Menge der ersten Menschen {A, B, C, …, Z}: Menge der Großbuchstaben im lateinischen Alphabet Die Menge M 1 ist eine Teilmenge von M 2 ( M 1 M 2 ), wenn jedes Element von M 1 auch Element von M 2 ist. Die Mengen M 1 und M 2 sind gleich ( M 1 M 2 ), wenn sie die gleichen Elemente enthalten (Extensionalitätsaxiom). Für jede (endliche) Menge M bezeichnet | M | die Anzahl ihrer Elemente. Neben der Aufzählung ihrer Elemente können Mengen durch eine charakterisierende Eigenschaft gebildet werden (Komprehensionsaxiom). Beispiel: byte = {x | -128x und x127}. Auf diese Weise können auch unendliche Mengen gebildet werden. Die unbeschränkte Verwendung der Mengenkomprehension kann zu Schwierigkeiten führen (Russells Paradox: „die Menge aller Mengen, die sich nicht selbst enthalten“), daher erlaubt man in der axiomatischen Mengenlehre nur gewisse Eigenschaften. Auf Mengen sind folgende Operationen definiert: Durchschnitt: M 1 M 2 {x | x M1 x M 2 } . Vereinigung: M 1 M 2 {x | x M1 x M 2 } . Differenz: M1 M 2 {x | x M1 x M 2 } . Beispiele: 0= 0 , 0= , – 0 =, 0 – =0. Durchschnitt und Vereinigung sind kommutativ und assoziativ. Daher kann man diese Operationen auf beliebige Mengen von Mengen ausweiten: {M1 , M 2 ,..., M n } {x | x M1 x M 2 ... x M n } . - iI M i {x | x M i für ein i I } . Von Mengen kommt man zu „Mengen höherer Ordnung“ durch die Potenzmengenbildung: Wenn M eine Menge ist, so bezeichnet (M ) oder 2 M die Menge aller Teilmengen von M . Cantor bewies, dass die Potenzmenge einer Menge immer mehr Elemente enthält als die Menge selbst. Speziell gilt für jede endliche Menge M : Wenn | M | n , so ist | ( M ) | 2 n . Beispiel: () {} , ({}) {,{}} und ({ A, B, C}) {,{ A},{B},{C},{ A, B},{ A, C},{B, C},{ A, B, C}} . Multimengen Während Mengen die grundlegenden Daten der Mathematik sind, hat man es in der Informatik oft mit Multimengen zu tun, bei denen Elemente „mehrfach“ vorkommen können. Beispiele sind eine Tüte mit roten, gelben und grünen Gummibärchen, die Vornamen der Studierenden dieser Vorlesung, die Multimenge der Buchstaben eines bestimmten Wortes, usw. Multimengen können notiert werden, indem man zu jedem Element die entsprechende Vielfachheit angibt, z.B. ist {A:3, B:1, N:2} die Multimenge der Buchstaben im Wort BANANA. Formal können Multimengen definiert werden als Funktionen von einer Grundmenge in die Menge 0 der Kardinalzahlen, siehe unten. Folgen Aus Mengen lassen sich durch Konkatenation Tupel und Folgen bilden. Der einfachste Fall ist dabei die Paarbildung mit dem kartesischen Produkt. Wenn M 1 und M 2 Mengen sind, so bezeichnet M1 M 2 {( x, y) | x M1 y M 2 } die Menge aller Paare von Elementen, deren erster Bestandteil ein Element aus M 1 und deren zweiter eines aus M 2 ist. Da die runden Klammern für vielerlei Zwecke verwendet werden, verwendet man manchmal zur Kennzeichnung von Paaren auch spitze oder, besonders in Programmiersprachen, eckige Klammern. Beispiele: ={(1,tt),(1,ff),(2,tt),(2,ff), (3,tt),…}, =, {0}={(1,0), (2,0), (3,0), …}, 0={(1,0), (1,1), (1,2), …, (2,0), (2,1), …} Eine Verallgemeinerung ist das n-stellige kartesische Produkt, mit dem n-Tupel gebildet werden: M 1 M 2 M n {( x1 , x2 ,..., xn ) | x1 M 1 x2 M 2 ... xn M n } 2-Tupel sind also Paare, statt 3-, 4- oder 5-Tupel sagt man auch Tripel, Quadrupel, Quintupel usw. Die zur Produktbildung umgekehrten Operationen, mit denen man aus einem Produkt die einzelnen Bestandteile wieder erhält, bezeichnet man als Projektionen: i ( x1 , x2 ,..., xn ) | xi Falls alle M i gleich sind (M 1 M 2 M n M ) , so schreiben wir statt M M M auch M n und nennen ( x1 , x2 ,..., xn ) eine Folge oder Sequenz oder Liste der Länge n über M . Wichtige Spezialfälle sind n 1 und n 0 . Im ersten Fall ist die einelementige Folge (x) etwas anderes als das Element x. Im zweiten Fall ist die leere Folge () unabhängig von der verwendeten Grundmenge. Achtung: Die leere Folge ist nicht zu verwechseln mit der leeren Menge! M n enthält nur Sequenzen einer bestimmten fest vorgegebenen Länge n. Unter M * verstehen wir die Menge, die alle beliebig langen Folgen über M enthält: M * {M i | i N} . Wenn wir nur nichtleere Folgen betrachten wollen, schreiben wir M : M M* M0 . Relationen und Funktionen Eine Relation zwischen zwei Mengen M 1 und M 2 ist eine Teilmenge von M 1 M 2 . Wenn zum Beispiel M={Anna, Beate} und J={Claus, Dirk, Erich}, so ist liebt={(Anna, Claus), (Beate, Dirk), (Beate, Erich)} eine Relation. Relationen schreibt man meist in Infixnotation, d.h., statt (Beate, Dirk)liebt schreibt man (Beate liebt Dirk). Falls M 1 M 2 M , so sagen wir, dass die Relation über M definiert ist. Typische Beispiele sind die Relationen und = über den natürlichen Zahlen, oder die Verbindungsrelation zwischen Städten im Streckennetz der deutschen Lufthansa. Eine Relation R heisst (links-)total, wenn es zu jedem x M1 ein y M 2 mit (xRy) gibt. Sie heisst (rechts-)eindeutig, wenn es zu jedem x M1 höchstens ein y M 2 mit (xRy) gibt. Eine eindeutige Relation nennt man Abbildung oder partielle Funktion, eine totale und eindeutige Relation heißt Funktion. Bei Funktionen schreiben wir f : M1 M 2 und f ( x) y für f M 1 M 2 und ( x, y ) f . Die Menge der x M1 , für die es ein y M 2 mit (xRy) gibt, heißt der Definitionsbereich oder Urbildbereich (domain) der Abbildung; die Menge der y M 2 , für die es ein x M1 mit (xRy) gibt, heißt der Wertebereich oder Bildbereich (range) der Funktion oder Abbildung. Genau wie oben lassen sich auch die Begriffe Relation und Funktion verallgemeinern. Eine nstellige Relation zwischen den Mengen M 1 , M 2 , , M n ist eine Teilmenge von M 1 M 2 M n . Einstellige Relationen heißen auch Prädikate. Auch für Prädikate schreiben wir (Px) anstatt von (x) P. Eine n-stellige Funktion f von M 1 , M 2 , , M n nach M ist eine (n+1)-stellige Relation zwischen M 1 , M 2 , , M n und M , so dass für jedes nTupel ( x1 , x2 ,..., xn ) M 1 M 2 M n genau ein y M existiert mit ( x1 , x2 ,..., xn , y) f . Eine Funktion f : M M M M heißt (n-stellige) Operation auf M . Beispiele für zweistellige Operationen sind + und * auf , , und . Die Differenz – ist auf , und eine Operation, auf ist sie nur partiell (nicht total). Die Division ist in jedem Fall nur partiell. Typische Prädikate auf natürlichen Zahlen sind prim oder even. Wir haben Funktionen als spezielle Relationen definiert, Es gibt auch die Auffassung, dass der Begriff „Funktion“ grundlegender sei als der Begriff „Relation“, und dass Relationen eine spezielle Art von Funktionen sind. Sei M 1 M 2 . Dann ist die charakteristische Funktion : M2 von M 1 in M 2 definiert durch (x) true falls x M1 und (x) false falls x M1 . Mit Hilfe der charakteristischen Funktion kann jede Relation zwischen den Mengen M 1 , M 2 , , M n als Funktion von M 1 , M 2 , , M n nach aufgefasst werden. Diese Auffassung findet man häufig in Programmiersprachen, bei denen Prädikate als boolesche Funktionen realisiert werden. Ordnungen Die Relation R über M heißt reflexiv, wenn für alle x M gilt, dass xRx . irreflexiv, wenn für kein x M gilt dass xRx . transitiv, wenn für alle x, y, z M mit xRy und yRz gilt, dass xRz . symmetrisch, wenn für alle x, y M mit xRy gilt, dass yRx . antisymmetrisch, wenn für alle x, y M mit xRy und yRx gilt x y . Eine reflexive, transitive, symmetrische Relation heißt Äquivalenzrelation. Eine reflexive, transitive, antisymmetrische Relation heißt Halbordnung oder partielle Ordnung. Eine irreflexive, transitive, antisymmetrische Relation heißt strikte Halbordnung. Eine partielle Ordnung heißt totale oder lineare Ordnung, wenn für alle x, y M gilt, dass xRy oder yRx . Bei einer totalen Ordnung lassen sich alle Elemente „der Reihe nach“ anordnen. Die Relation ist eine totale Ordnung auf natürlichen und reellen Zahlen, nicht aber auf komplexen Zahlen: dort ist sie nur eine Halbordnung. Ein einfacheres Beispiel für eine Halbordnung über Zahlen ist die Relation „ist Teiler von“. Beispiel für reflexive, aber nicht antisymmetrische Relation: Beispiel für eine antisymmetrische, aber nicht reflexive Relation: 1.2 Induktive Definitionen und Beweise Für fast alle in der Informatik wichtigen Datentypen besteht ein direkter Zusammenhang zwischen ihrer rekursiven Definition (ihrem rekursiven Aufbau) und induktiven Beweisen von Eigenschaften dieser Daten. Wir wollen uns diese Dualität am Beispiel der natürlichen Zahlen betrachten. Die natürlichen Zahlen lassen sich durch die folgenden so genannten Peano-Axiome definieren: 1 ist eine natürliche Zahl. Für jede natürliche Zahl gibt es genau eine natürliche Zahl als Nachfolger. Verschiedene natürliche Zahlen haben auch verschiedene Nachfolger. 1 ist nicht der Nachfolger irgendeiner natürlichen Zahl. Sei P eine Menge natürlicher Zahlen mit folgenden Eigenschaften: o 1 ist in P o Für jede Zahl in T ist auch ihr Nachfolger in P. Dann enthält T alle natürlichen Zahlen. Das letzte dieser Axiome ist das so genannte Induktionsaxiom. Es wird oft in der Form gebraucht, dass P eine Eigenschaft natürlicher Zahlen ist: Sei P eine Eigenschaft natürlicher Zahlen, so dass P(1) gilt und aus P(i) folgt P(i+1). Dann gilt P für alle natürlichen Zahlen. Der erste Mathematiker, der einen formalen Beweis durch vollständige Induktion angab, war der italienische Geistliche Franciscus Maurolicus (1494 -1575). In seinem 1575 veröffentlichten Buch „Arithmetik“ benutzte Maurolicus die vollständige Induktion unter anderem dazu, zu zeigen, dass alle Quadratzahlen sich als Summe der ungeraden Zahlen bis zum doppelten ihrer Wurzel ergeben: 1 + 3 + 5 + ... + (2n-1)=n*n Beweis: Sei P die Menge natürlicher Zahlen, die diese Gleichung erfüllen. Um zu beweisen, dass P alle natürlichen Zahlen enthält, müssen wir zeigen 1=1*1 Wenn 1 + 3 + 5 + ... + (2n-1)=n*n, dann 1 + 3 + 5 + ... + (2n-1)+(2(n+1)-1)=(n+1)*(n+1) Die Wahrheit dieser Aussagen ergibt sich durch einfaches Ausrechnen. Hier ist ein geringfügig komplizierteres Beispiel zum selber machen: Die Summe der Kubikzahlen bis n ist das Quadrat der Summe der Zahlen bis n. 2 n i i i 1 i 1 Ein wichtiger Gesichtspunkt beim Induktionsaxiom ist, dass die natürlichen Zahlen als „induktiv aufgebaut“ dargestellt werden gemäß den folgenden Regeln: n 3 1 . Wenn i , dann auch i+1 . Außer den so erzeugten Objekten enthält keine weiteren Zahlen. Mit anderen Worten, jede Zahl wird erzeugt durch die endlich-oft-malige Anwendung der Operation (+1) auf die Zahl 1. Das Induktionsaxiom erlaubt es, Funktionen über den natürlichen Zahlen rekursiv zu definieren. Eine rekursive Definition nimmt dabei auf sich selbst Bezug. Solche Definitionen können leicht schief gehen („das Gehalt berechnet sich immer aus dem Gehalt des letzten Jahres plus 3%“ oder „Freiheit ist immer die Freiheit der Andersdenkenden“ oder „GNU is short for »GNU is Not Unix«“). Ein Begriff, der durch solch eine zirkuläre Definition erklärt wird, ist nicht wohldefiniert. Eine Funktion ist nur dann wohldefiniert, wenn sich der Funktionswert eindeutig aus den Argumenten ergibt. Das Prinzip der vollständigen Induktion erlaubt es nun, eine Funktion über den natürlichen Zahlen dadurch zu deklarieren, dass man den Funktionswert für n=1 angibt, und indem man zeigt, wie sich der Funktionswert für (n+1) aus dem Funktionswert für n berechnen lässt. Wenn man nämlich für P die Aussage „der Funktionswert ist eindeutig bestimmt“ einsetzt, so besagt dass Induktionsprinzip, dass dann der Funktionswert für alle natürlichen Zahlen eindeutig bestimmt ist. Zum Beispiel lässt sich die Fakultätsfunktion n!=1*2*…*n ohne „Pünktchen“ dadurch definieren, dass wir festlegen 1! = 1 Wenn n!=x, dann ist (n+1)!=(n+1)*x Eine andere Schreibweise der zweiten Zeile ist (n+1)!=(n+1)*n! Da in dieser Formel „n“ nur ein Stellvertreter für eine beliebige Zahl ist, können wir auch schreiben Wenn n>1, dann ist n!=n*(n-1)! Diese Schreibweise ist sehr nahe an der Schreibweise in Programmiersprachen, beispielsweise (in der elementaren Sprache des Programms bc): define fac(n){ if (n==1) return(1) else return(n*fac(n-1)) } In der vorgestellten Fassung erlaubt es das Induktionsprinzip nur, bei der Definition von f(n) auf den jeweils vorherigen Wert f(n-1) zurückzugreifen. Eine etwas allgemeinere Fassung ist das Prinzip der transfiniten Induktion: Sei P eine Eigenschaft natürlicher Zahlen, so dass für alle x gilt: Falls P(y) für alle y<x, so auch P(x). Dann gilt P für alle natürlichen Zahlen. Dieses Prinzip gilt nicht nur für die natürlichen Zahlen, sondern für beliebige fundierte Ordnungen (in denen es keine unendlich langen absteigenden Ketten gibt). Der Induktionsanfang ergibt sich dadurch, dass es keine kleinere natürliche Zahl als 1 gibt und für die 1 daher nichts vorausgesetzt werden kann. Im Induktionsschritt erlaubt uns dieses Prinzip, auf beliebige vorher behandelte kleinere Zahlen zurückzugreifen. Das Standardbeispiel sind hier die Fibonacci-Zahlen (nach Leonardo di Pisa, filius Bonacci, 1175-1250, der das Dezimalsystem in Europa einführte) 1, falls n 2 fib(n) fib(n 1) fib(n 2), sonst oder, in der programmiersprachlichen Fassung, define fib(n){ if(n<=2) return(1) else return(fib(n-1)+fib(n-2))} Die Werte dieser Funktion sind, der Reihe nach, 1,1,2,3,5,8,13,21,… und sollen das Bevölkerungswachstum von Kaninchenpaaren nachbilden. Als Beispiel für einen Beweis, der auf mehrere Vorgänger zurückgreift, zeigen wir die Formel von Binet: n n 1 1 5 1 5 . fib(n) 5 2 2 1 5 1 5 Als Lemma benötigen wir, dass für gilt 1 2 , und ebenso für . 2 2 Das sieht man durch einfaches Ausrechnen, ebenso die Gültigkeit der Aussage für n=1,2. 1 n 1 Damit können wir als Induktionsannahme voraussetzen, dass fib(n 1) n1 und 5 1 n2 fib(n 2) n 2 . Mit fib(n) fib(n 1) fib(n 2) ergibt sich 5 1 n 1 fib(n) n2 n1 n2 , d.h., 5 1 1 2 n2 1 n fib(n) ( 1) n2 ( 1) n 2 2 n 2 n , was zu zeigen 5 5 5 war. Die Berechnung der Fibonacci-Zahlen mit der Formel von Binet geht im Allgemeinen erheblich schneller als mittels der rekursiven Definition: define binet(n){ return((((1+sqrt(5))/2)^n - ((1-sqrt(5))/2)^n)/sqrt(5))} binet(50) ergibt sofort 12586269025 Im Allgemeinen ist es nicht immer möglich, solch eine geschlossene (nichtrekursive) Formel für eine rekursiv definierte Funktion zu finden. Wir haben das Induktionsprinzip für natürliche Zahlen und der fundierten Ordnungsrelation < angewendet. Dies ist nur ein Spezialfall des folgenden allgemeinen Prinzips für induktiv erzeugte Mengen. Das sind Mengen, die definiert werden durch die explizite Angabe gewisser Elemente der Menge, Regeln zur Erzeugung weiterer Elemente aus schon vorhandenen Elementen der Menge sowie der expliziten oder impliziten Annahme, dass die Menge nur die so erzeugten Elemente enthält. Für induktiv erzeugte Mengen gilt folgendes allgemeine Induktionsprinzip: Sei P eine Eigenschaft, die Elemente der Menge haben können oder nicht, so dass P für alle explizit angegebenen Elemente der Menge gilt, und P für alle gemäß den Bildungsregeln erzeugten Elemente gilt, falls es für die bei der Erzeugung verwendeten Elemente gilt. Dann gilt P für alle Elemente der Menge. Beispiele für dieses Erzeugungsprinzip werden später betrachtet. 1.3 Alphabete, Wörter, Bäume, Graphen Unter einem Alphabet A versteht man eine endliche Menge von Zeichen A={a1, …, an}. Das bekannteste Beispiel ist sicher das lateinische Alphabet mit den Zeichen A, B, C, …, Z. Aber bereits davon gibt es verschiedene Varianten, man denke nur an das deutsche Alphabet mit Umlauten ä, ö, ü und der Ligatur ß. Im Laufe der Zeit haben sich bei den Völkern Hunderte von Alphabeten gebildet, von Keilschriften und Hieroglyphen bis hin zu Runen- und Geheimschriften (*). Das chinesische Alphabet umfasst etwa 56000 Zeichen, im Alltag kann man mit 6.000 Schriftzeichen schon relativ gut auskommen; der chinesische Zeichensatz für Computer enthält 7.445 Schriftzeichen. Der ASCII-Zeichensatz enthält 128 bzw. (in der erweiterten Form) 256 Druckzeichen, siehe Tabelle. Man beachte, dass in manchen Alphabeten das Leerzeichen als ein Zeichen enthalten ist; als Ersatzdarstellung wählt man häufig eine Unterstrich-Variante. Ein für die Informatik wichtiges Alphabet ist die Menge der Wahrheitswerte. Eine (endliche) Folge wA* von Zeichen über einem Alphabet A heißt Wort oder Zeichenreihe (string) über A. Normalerweise schreibt man, wenn es sich um Wörter handelt, statt w = (a1,a2,…,an) kurz w = "a1a2…an", manchmal werden die Anführungszeichen auch weggelassen. Die leere Zeichenreihe wird mit dem Symbol oder mit "" bezeichnet. Über Wörtern ist die Konkatenation (Hintereinanderschreibung) als Operation definiert: Wenn v = a1a2…an und w = b1b2…bm, dann ist v°w = a1a2…anb1b2…bm. Da die Operation ° assoziativ ist ((u ° v) ° w = u ° (v ° w)), wird das Operationssymbol ° manchmal auch einfach weggelassen. Die leere Zeichenreihe ist bezüglich + ein neutrales Element (w ° = ° w = w). Eine Menge mit einer assoziativen Operation und einem neutralem Element nennt man auch Monoid; da die Menge A* (mit der Operation ° und dem neutralen Element ) keinen weiteren Einschränkungen unterliegt, heißt sie auch der freie Monoid über A (wenn a1a2…an = b1b2…bm, so ist n=m und a1= b1 und… und an= bn). Die Menge der Wörter über einem gegebenen Alphabet lässt sich auch induktiv erzeugen: A* Wenn aA und wA*, so ist (a°w)A*. Hierbei bezeichnet (a°w) diejenige Zeichenreihe, die als erstes Zeichen a enthält und danach das Wort w. Alternativ dazu hätten wir Wörter induktiv durch das Anfügen (append) von Zeichen an Zeichenreihen erzeugen können. Diese Charakterisierung der Menge der Zeichenreihen erlaubt es, induktive Beweise zu führen und rekursive Funktionen über Wörtern zu definieren. Sei first(w) die partielle Funktion, die zu einem nichtleeren Wort dessen erstes Zeichen liefert, und rest(w) die Funktion, die das Wort ohne das erste Zeichen liefert. Hier sind ein paar rekursiv definierte Funktionen über Wörtern. define length(w){ if(w==epsilon) return(0) else return(1+lenght(rest(w)))} liefert die Länge eines Wortes. Die Konkatenation kann rekursiv wie folgt definiert werden: define conc(v,w){ if (w==epsilon) return(v) else return(conc(append(v,first(w)),rest(w)))} define inverse(w){ if(w==epsilon) return(w) else return(inverse(rest(w))°first(w)))} liefert das umgedrehte Wort, also etwa EEFFAC zu CAFFEE. define replace(w,a,b){ if(w==epsilon) return(w) else if(first(w)==“a”) return(“b”°replace_a_b(rest(w))) else return(first(w)°replace(rest(w),a,b)} ersetzt jedes a in w durch b. Wir werden später noch ähnliche solche Funktionen kennen lernen. Wenn w=u°v, so sagen wir, dass u ein Anfangswort von w ist. Wenn w=v1°u°v2, so nennen wir u ein Teilwort von w. Auch die Anfangswortrelation lässt sich leicht induktiv definieren: define anfangswort(w,u){ if(u==epsilon) return(true) else if(first(w)!= first(u)) return(false) else return(anfangswort(rest(w), rest(u)))} Auf Alphabeten ist häufig eine totale Ordnungsrelation erklärt; meist wird diese durch die Reihenfolge der Aufschreibung der Zeichen unterstellt. Wenn A ein Alphabet mit einer totalen Ordnungsrelation ist, so kann zur lexikographischen Ordnung auf A* ausgeweitet werden: Sei x= x1x2…xn und y= y1y2…ym. Dann gilt x y, wenn x ein Anfangswort von y ist, oder es gibt ein Anfangswort z von x und y (x=z°x’, y=z°y’) und first(x’) <first(y’). (a < b. bedeutet a b. und nicht a = b) Beispiele für lexikographisch geordnete Wörter über dem lateinischen Alphabet sind "ANTON" < "BERTA", "AACHEN" < "AAL", "AAL" < "AALBORG" und < "A". Es ist nicht schwer zu sehen, dass die lexikographische Ordnung eine totale Ordnung ist. Die Buchstaben des deutschen Alphabets sind nicht linear geordnet (a und ä stehen nebeneinander, ß ist nicht eingeordnet), daher entspricht die Reihenfolge der Wörter in einem deutschen Lexikon nicht der lexikographischen Ordnung. Bäume Bäume sind – neben Tupeln, Folgen und Wörtern – eine weitere in der Informatik sehr wichtige Datenstruktur. In der induktiven Definition von Zeichenreihen besteht ein Wort w aus der Konkatenation von first(w) mit rest(w). Ein Binärbaum ist dadurch gekennzeichnet, dass es zwei verschiedene „Reste“ gibt: den linken und den rechten Unterbaum. Daraus ergibt sich folgende induktive Definition der Menge der Binärbäume über einem gegebenen Alphabet A: A^ Wenn aA und lA^ und rA^, so ist (a,l,r)A^. a heißt Wurzel, l und r sind Unterbäume des Baumes (a,l,r). Die Wurzeln von l und r heißen die Kinder oder Nachfolger von a. Ein Baum y ist Teilbaum eines Baumes x, wenn x=y oder y Teilbaum eines Unterbaumes von x ist. Wenn y nichtleerer Teilbaum von x ist, so sagen wir, die Wurzel von y ist ein Knoten von x. Ein Knoten ohne Nachfolger (d.h. ein Teilbaum der Gestalt (a, , ) ) heißt Blatt. Als Beispiel für Bäume betrachten wir Formelbäume über dem Alphabet (x,y,z,+,*). Die Formel x*y + x*z (mit „Punkt-vor-Strich-Regelung“) kann durch den Baum (+,(*,(x, , ),(y, , )),(*,(x, , ),(z, , ))) repräsentiert werden. Übersichtlicher ist eine graphische Darstellung: + * x * y x z Wir werden später verschiedene Algorithmen, die auf Bäumen basieren, kennen lernen. Es ist klar, dass sich die obige Definition direkt auf Binärbäume über einer beliebigen Grundmenge verallgemeinern lässt. Eine weitere nahe liegende Erweiterung sind n-äre Bäume, bei denen jeder Knoten entweder keinen oder n Nachfolger hat. Wenn wir erlauben, dass jeder Knoten eine beliebige (endliche) Zahl von Nachfolgern haben kann, sprechen wir von endlich verzweigten Bäumen. Aufrufbäume Eine spezielle Art von (endlich verzweigten) Bäumen sind die Aufrufbäume einer rekursiven Funktion. Die Wurzel eines Aufrufbaumes ist der Name der Funktion mit den Eingabewerten. Die Nachfolger jeden Knotens sind die bei der Auswertung aufgerufenen Funktionen mit ihren Eingabewerten. Beispiel: inverse(„ABC“) ) rest(„ABC“) inverse(„BC“) first(„ABC“) + rest(„BC“) inverse(„C“) first(„BC“) + rest(„C“) inverse(„“) first(„C“) + (In diesem Baum haben wir die Funktionen „==“ und „if“ nicht weiter berücksichtigt.) Beim verkürzten Aufrufbaum lässt man alle Knoten weg außer denen, die die rekursive Funktion selbst betreffen. Beispiel: fib(5) fib(3) fib(4) fib(3) fib(2) fib(2) fib(2) fib(1) fib(1) Unter der Aufrufkomplexität einer rekursiven Funktion verstehen wir die Anzahl der Knoten im verkürzten Aufrufbaum. Die Zeit, die benötigt wird, um eine rekursive Funktion zu berechnen, hängt im Wesentlichen von der Aufrufkomplexität ab. Als Beispiel betrachten wir die Aufrufkomplexität der Fibonacci-Funktion. Aus obigem Beispiel ist sofort klar: 1, falls n 2 fibComp(n) 1 fibComp(n 1) fibComp(n 2), sonst Die rekursive Formulierung hilft leider noch nicht, die Aufrufkomplexität abzuschätzen. Per Induktion nach n zeigen wir: fibComp(n) 2 * fib(n) 1 . Für n=1,2 ist dies klar, für n>2 gilt fibComp(n) 1 fibComp(n 1) fibComp(n 2) I .V . 1 2 * fib(n 1) 1 2 * fib(n 2) 1 2 * ( fib(n 1) fib(n 2)) 1 2 * fib(n) 1 Mit der früher bewiesenen Gleichung von Binet erhalten wir n n 2 1 5 1 5 1. fibComp(n) 5 2 2 Eine Wertetabelle für einige Zahlenwerte ist nachfolgend angegeben. Daraus folgt: wenn in einer Sekunde 10.000 Aufrufe erfolgen, benötigt die Rechnung für n=100 etwa 2,2 Milliarden Jahre! (üblicherweise ist vorher der Speicher erschöpft). n 3 5 10 20 30 40 50 60 70 80 90 100 fibComp(n) 3 9 109 13529 1.6*106 2*108 2.5*1010 3*1012 3.8*1014 4.6*1016 5.7*1018 7*1020 Graphen Während ein Wort in der Informatik nur eine spezielle Art von Folgen ist, versteht man unter einem Graphen nur eine spezielle Art von Relationen: Ein Graph ist die bildliche Darstellung einer binären Relationen über einer endlichen Grundmenge. Die Elemente der Grundmenge werden dabei in Kreisen (Knoten) gezeigt. Zwischen je zwei Knoten zeichnet man einen Pfeil (eine Kante), falls das betreffende Paar von Elementen in der Relation enthalten ist. Beispiel: B A C D Dies ist die Relation {(A,B),(B,C),(C,B),(C,A),(B,D),(C,D)}. Für symmetrische Relationen weisen die Pfeile immer in beide Richtungen; man spricht hier von ungerichteten Graphen. Eine Alternative zur obigen Definition besteht darin, einen Graphen als Tupel (V,E) zu definieren, wobei V eine endliche Menge von Knoten (vertices) und E eine endliche Menge von Kanten (edges) ist, so dass zu jeder Kante genau ein Anfangs- und ein Endknoten gehört. Knoten, die nicht Endknoten sind, heißen Quelle, Knoten, die nicht Anfangsknoten sind, heißen Senke im Graphen. Knoten, die weder Anfangs- noch Endknoten sind, heißen isoliert. Eine dritte Art der Definition von Graphen ist durch die so genannte Adjazenzmatrix: Nach dieser Auffassung ist ein Graph eine endliche Matrix (Tabelle) mit booleschen Werten. Die Zeilen und Spalten der Tabelle sind dabei mit der Grundmenge beschriftet; ein Eintrag gibt an, ob das entsprechende Paar (Zeile, Spalte) in der Relation enthalten ist oder nicht. A B C D A B X X X C D X X X Im Gegensatz zu Bäumen können Graphen Zyklen enthalten, daher existiert keine einfache induktive Definition. Umgekehrt können endlich verzweigte Bäume als zyklenfreie Graphen mit nur einer Quelle betrachtet werden. Kapitel 2: Informationsdarstellung 2.1 Bits und Bytes, Zahl- und Zeichendarstellungen (siehe Gumm/Sommer * Kap.1.2/1.3) Damit Informationen von einer Maschine verarbeitet werden können, müssen sie in der Maschine repräsentiert werden. Üblich sind dabei Repräsentationsformen, die auf Tupeln oder Folgen über der Menge aufbauen. Ein Bit (binary digit) ist die kleinste Einheit der Informationsdarstellung: es kann genau zwei Werte annehmen, z. B. 0 oder 1. Genau wie es viele verschiedene Notationen der Menge gibt, gibt es viele verschiedene Realisierungsmöglichkeiten eines Bits: an/aus, geladen/ungeladen, weiss/schwarz, magnetisiert/entmagnetisiert, reflektierend/lichtdurchlässig, … Lässt eine Frage mehrere Antworten zu, so lassen sich diese durch eine Bitfolge (mehrere Bits) codieren. Beispiel: Die Frage, aus welcher Himmelsrichtung der Wind weht, lässt 8 mögliche Antworten zu. Diese lassen sich durch Bitfolgen der Länge 3 codieren: 000 = Nord 001 = Nordost 010 = Ost 011 = Südwest 100 = Süd 101 = Südost 110 = West 111 = Nordwest Offensichtlich verdoppelt jedes zusätzliche Bit die Anzahl der möglichen Bitfolgen, so dass es genau 2n mögliche Bitfolgen der Länge n gibt (| |=2 | n|=2n) Ein Byte ist ein Oktett von Bits: 8 Bits = 1 Byte. Oft betrachtet man Bytefolgen anstatt von Bitfolgen. Ein Byte kann verwendet werden, um z.B. folgendes zu speichern: ein codiertes Zeichen (falls das Alphabet weniger als 28 Zeichen enthält) eine Zahl zwischen 0 und 255, eine Zahl zwischen -128 und +127, die Farbcodierung eines Punkts in einer Graphik, genannt „Pixel“ (picture element) Gruppen von 16 Bits, 32 Bits, 64 Bits bzw. 128 Bits werden häufig als Halbwort, Wort, Doppelwort bzw. Quadwort bezeichnet. Leider gibt es dafür unterschiedliche Konventionen. Zwischen 2-er und 10-er Potenzen besteht (näherungsweise) der Zusammenhang: 210 = 1024 1000 = 103 Für Größenangaben von Dateien, Disketten, Speicherbausteinen, Festplatten etc. benutzt man daher folgende Präfixe: k = 1024 = 210 103 (k = Kilo) 2 20 M = 1024 = 1048576=2 106 (M = Mega) 3 30 9 G = 1024 = 2 10 (G = Giga) 4 40 12 T = 1024 = 2 10 (T = Tera) P = 10245 = 250 1015 (P = Peta) 6 60 18 E = 1024 = 2 10 (E = Exa) Die Ungenauigkeit der obigen Näherungsformel nimmt man dabei in Kauf. Mit 1 GByte können also entweder 230 = 10243 = 1.073.741.824 oder 109 = 1.000.000.000 Bytes gemeint sein. Anhaltspunkte für gängige Größenordnungen von Dateien und Geräten: eine Notiz: ~200 B ein Brief: ~3 kB ein Dos-Programm: ~300 kB Diskettenkapazität: 1,44 MB ein Windows-Programm: ~ 1 MB ein Musiktitel: ~40 MB (im MP3-Format ~4 MB) Zip-Diskette: ~120 MB Hauptspeichergröße: 128-512 MB CD-ROM Kapazität: ~ 680 MB DVD (Digital Versatile Disk): ~ 4,7 bzw. ~ 9 GB Festplatte: ~10-80 GB. Für Längen- und Zeiteinheiten werden auch in der Informatik die gebräuchlichen Vielfachen von 10 benutzt. So ist z.B. ein 400 MHz Prozessor mit 400106 = 400.000.000 Hertz (Schwingungen pro Sekunde) getaktet. Das entspricht einer Schwingungsdauer von 2,510-9 sec, d.h. 2,5 ns. Der Präfix n steht hierbei für nano, d.h. den Faktor 10-9. Weitere Präfixe für Faktoren kleiner als 1 sind: m = 1/1000 = 10-3 µ = 1/1000000 = 10-6 n = 1/1000000000 = 10-9 p = ... = 10-12 f = ... = 10-15 (m = Milli) (µ = Mikro) (n = Nano) (p = Pico) (f = Femto) Beispiele: 1mm = 1 Millimeter; 1 ms = 1 Millisekunde. Für Längenangaben wird neben den metrischen Maßen eine im Amerikanischen immer noch weit verbreitete Einheit verwendet: 1" = 1 in = 1 inch = 1 Zoll = 2,54 cm = 25,4 mm. Teile eines Zolls werden als Bruch angegeben. Beispiel - Diskettengröße: 3 1/2". Darstellung natürlicher Zahlen, Stellenwertsysteme Die älteste Form der Darstellung von Zahlen ist die Strichdarstellung, bei der jedes Individuum durch einen Strich oder ein Steinchen repräsentiert wird (calculi=Kalksteinchen, vgl. kalkulieren). Bei dieser Darstellung ist die Addition besonders einfach (Zusammen- oder Hintereinanderschreibung von zwei Strichzahlen), allerdings wird sie für große Zahlen schnell unübersichtlich. Die Ägypter führten deshalb für Gruppen von Strichen Abkürzungen ein (*,*). Daraus entstand dann das römische Zahlensystem: Aus Übersichtlichkeitsgründen werden die großen Zahlen dabei zuerst geschrieben; prinzipiell spielt in solchen direkten Zahlensystemen die Position einer Ziffer keine Rolle. Die Schreibweise IV = 5-1 ist erst viel später entstanden! Direkte Zahlensysteme haben einige Nachteile: Die Darstellung großer Zahlen kann sehr lang werden, und arithmetische Operationen lassen sich in solchen Systemen nur schlecht durchführen. In Indien (und bei den Majas) wurde ein System mit nur zehn verschiedenen Ziffernsymbolen verwendet, bei der die Position jeder Ziffer (von rechts nach links) ihre Wertigkeit angibt. Die wesentliche Neuerung ist dabei die Erfindung der Zahl Null (ein leerer Kreis). Der schon genannte Muhammed ibn Musa al-Khwarizmi verwendete das Dezimalsystem in seinem Arithmetikbuch, das er im 8. Jahrhundert schrieb. Bereits im 10. Jahrhundert wurde das System in Europa eingeführt, durchsetzen konnte es sich jedoch erst im 12. Jahrhundert mit der Übersetzung des genannten Arithmetikbuchs ins Lateinische (durch Fibonacci, siehe oben).(*) Wir haben schon erwähnt, dass das Dualzahlen- oder Binärsystem mit nur zwei Ziffern in Europa 1673 von Gottfried Wilhelm Leibnitz (wieder-)erfunden wurde. Allgemein gilt: In Stellenwertsystemen wird jede Zahl als Ziffernfolgen xn-1... x0 repräsentiert, wobei - bezogen auf eine gegebene Basis b - jede Ziffer xi einen Stellenwert (xi*bi) bekommt: n 1 xn1 x0 b xi b i i 0 Werden dabei nur Ziffern xi mit Werten zwischen 0 und b-1 benutzt, so ergibt sich eine eindeutige Darstellung; dafür werden offenbar genau b Ziffern benötigt. Zu je zwei Basen b und b' gibt es eine umkehrbar eindeutige Abbildung, die [xn-1... x0]b und [ yn'-1... y0]b' mit 0 xi b und 0 yi b' ineinander überführt. Beispiele: b = 2: b = 3: b = 8: b = 10: [10010011]2 = 1*27 + 1*24 + 1*21 + 1*20 = [147]10 [12110]3 = 1*34 + 2*33 + 1*32 + 1*31 = [147]10 [223]8 = 2*82 + 2*81 + 3*80 = [147]10 [147]10 = 1*102 + 4*101 + 7*100=[10010011]2 Wichtige Spezialfälle sind b=10, 2, 8 und 16. Zahlen mit b=8 bezeichnet man als Oktalzahlen. Unter Sedezimalzahlen (oft auch Hexadezimalzahlen genannt) versteht man Zifferndarstellungen von Zahlen zur Basis 16. Sie dienen dazu, Dualzahlen in komprimierter (und damit leichter überschaubarer) Form darzustellen und lassen sich besonders leicht umrechnen. Je 4 Dualziffern werden zu einer „Hex-ziffer“ zusammengefasst. Da man zur Hexadezimaldarstellung 16 Ziffern benötigt, nimmt man zu den Dezimalziffern 0 ... 9 die ersten Buchstaben A ... F hinzu. Beispiele: Umwandlung von Dezimal - in Oktal- / Hexadezimalzahlen und umgekehrt: [1]10=[1]8=[1]16=[1]2 … [7]10=[7]8=[7]16=[111]2 [8]10=[10]8=[8]16=[1000]2 [9]10=[11]8=[9]16=[1001]2 [10]10=[12]8=[A]16=[1010]2 [11]10=[13]8=[B]16=[1011]2 [12]10=[14]8=[C]16=[1100]2 [13]10=[15]8=[D]16=[1101]2 [14]10=[16]8=[E]16=[1110]2 [15]10=[17]8=[F]16=[1111]2 [16]10=[20]8=[10]16 [17]10=[21]8=[11]16 … [32]10=[40]8=[20]16 [33]10=[41]8=[21]16 … [80]10=[100]8=[50]16 … [160]10=[240]8=[A0]16 … [255]10=[377]8=[FF]16 [256]10=[400]8=[100]16 [1000]10=[3E8]16 … [4096]10=[10000]8=[1000]16 … [10000]10=[2710]16 … [45054]10=[AFFE]16 [65535]10=[177777]8=[FFFF]16 [106]10=[F4240]16 [4294967295]10=[FFFFFFFF]16 Diese Umrechnung ist so gebräuchlich, dass sie von vielen Taschenrechnern bereit gestellt wird. Um mit beliebig großen natürlichen oder ganzen Zahlen rechnen zu können, werden diese als Folgen über dem Alphabet der Ziffern repräsentiert. Diese Darstellung wird beispielsweise im bc verwendet. Numerische Algorithmen mit solchen Repräsentationen sind allerdings häufig komplex, und in vielen Anwendungen wird diese Allgemeinheit nicht benötigt. Daher werden Zahlen oft als Dual- oder Binärwörter einer festen Länge n repräsentiert. Mit Hilfe von n Bits lassen sich 2n Zahlenwerte darstellen: die natürlichen Zahlen von 0 bis 2n - 1 oder die ganzen Zahlen zwischen -2n-1 und 2n-1 - 1 oder ein Intervall der reellen Zahlen (mit begrenzter Genauigkeit) Beispiel: Länge 4 8 16 32 darstellbare Zahlen 0 .. 15 0 .. 255 0 .. 65535 0 .. 4 294 967 295 Darstellung ganzer Zahlen Für die Darstellung ganzer Zahlen wird ein zusätzliches Bit (das "Vorzeichen-Bit") benötigt. Mit Bitfolgen der Länge n kann also (ungefähr) der Bereich [-2n-1 .. 2n-1] dargestellt werden. Nahe liegend ist die dabei Vorzeichendarstellung: Das erste Bit repräsentiert das Vorzeichen (0 für '+' und 1 für '-') und der Rest den Absolutwert. Nachteile dieser Darstellung: • Die Darstellung der 0 ist nicht mehr eindeutig. • Beim Rechnen "über die 0" müssen umständliche Fallunterscheidungen gemacht werden, Betrag und Vorzeichen sind zu behandeln. Beispiel:3 + (-5) = 0011+1101=1010 Eine geringfügige Verbesserung bringt die Einserkomplement-Darstellung, bei der jedes Bit der Absolutdarstellung umgedreht wird. Meist wird jedoch die so genannte Zweierkomplement-Darstellung (kurz: 2c) benutzt. Sie vereinfacht die arithmetischen Operationen und erlaubt eine eindeutige Darstellung der 0. Bei der Zweierkomplementdarstellung gibt das erste Bit das Vorzeichen an, das 2. bis n-te Bit ist das Komplement der um eins verringerten positiven Zahl. Die definierende Gleichung für die Zweierkomplementdarstellung ist: [-xn-1... x0]2c + [xn-1...x0]2c = [10...0]2c = 2n+1 Um zu einer gegebenen positiven Zweierkomplement-Zahl die entsprechende negative zu bilden, invertiert man alle Bits und addiert 1. Die Addition kann mit den üblichen Verfahren berechnet werden (Beweis: eigene Übung!). Beispiele: n = 4: +5 [0101]2c , also -5 (1010 + 0001)2c = [1011]2c 3 + (-5) =[0011]2c +[1011]2c =[1110]2c 4 + 5 =[0100]2c +[0101]2c [1001]2c = -7 Achtung!!! In Java gibt es, abhängig von der Länge n, vier verschiedene Datentypen für ganze Zahlen: Datentyp byte mit dem Wertebereich -27 = -128 ...127= 27-1 Datentyp short mit dem Wertebereich -215 = -32768 ...32767= 215-1 Datentyp int mit dem Wertebereich -231 = - 2147483648 ... 2147483647= 231-1 Datentyp long mit dem Wertebereich -263 =- 9223372036854775808 ... 9223372036854775807= 263-1 Beachte: Ein Zahlbereichsüberlauf wird nicht abgefangen und kann beispielsweise zur Folge haben, dass die nagelneue Rakete abstürzt! Darstellung rationaler und reeller Zahlen Prinzipiell kann man rationale Zahlen als Paare ganzer Zahlen (Zähler und Nenner) darstellen. Ein Problem ist hier die Identifikation gleicher Zahlen: (7,-3)=(-14,6). Hier müsste man nach jeder Rechenoperation mit dem ggT normieren; das wäre sehr unpraktisch. Daher werden in der Praxis rationale Zahlen meist wie reelle Zahlen behandelt. Für relle Zahlen gilt: Es gibt überabzählbar viele reelle Zahlen . Also gibt es auch reelle Zahlen, die sich nicht in irgend einer endlichen Form aufschreiben lassen, weder als endlicher oder periodischer Dezimalbruch noch als arithmetischer Ausdruck oder Ergebnis eines Algorithmus. Echte reelle Zahlen lassen sich also nie genau in einem Computer speichern, da es für sie definitionsgemäß keine endliche Darstellung gibt. Die so genannten „reals“ im Computer sind mathematisch gesehen immer Näherungswerte für reelle Zahlen mit endlicher Genauigkeit. Für reelle Zahlen gibt es die Festkomma- und die Gleitkommadarstellung. Bei der Festkommadarstellung steht das Komma an einer beliebigen, festen Stelle. Für x=[xn-1xn-2…x1x0x-1x-2…x-m]2 ist x n 1 x i m i 2 i . Diese Darstellung gestattet nur einen kleinen Wertebereich und hat auch sonst einige Nachteile (Normierung von Zahlen erforderlich). Daher verwendet man sie in elektronischen Rechenmaschinen nur in Ausnahmefällen. Ziel der Gleitkommadarstellung (IEEE754 Standard) ist es, ein möglichst großes Intervall reeller Zahlen zu umfassen, die Genauigkeit der Darstellung an die Größenordnung der Zahl anzupassen: bei kleinen Zahlen sehr hoch, bei großen Zahlen niedrig. Daher speichert man neben dem Vorzeichen und dem reinen Zahlenwert - der so genannten Mantisse - auch einen Exponenten (in der Regel zur Basis 2 oder 10), der die Kommaposition in der Zahl angibt. - Das Vorzeichenbit v gibt an, ob die vorliegende Zahl positiv oder negativ ist. - Die Mantisse m besteht aus einer n–stelligen Binärzahl m1....mn - Der Exponent e ist eine L-stellige ganze Zahl (zum Beispiel im Bereich -128 bis +127), die angibt, mit welcher Potenz einer Basis b die vorliegende Zahl zu multiplizieren ist. Das Tripel (v, m, e) wird als (-1)v * m * be-n interpretiert. Bei gegebener Wortlänge von 32 Bit verwendet man beispielsweise 24 Bit für Vorzeichen und Mantisse (n=23) sowie L=8 Bit für den Exponenten. Beispiele mit n=L=4: (0, 1001, 0000) = 1 * 9 * 2-4 = [0,1001]2=0,5625 und (1, 1001, 0110) = -1 * 9 * 26-4 = [-100100]2 = -36 und (0, 1001, 1010) = 1 * 9 * 2-6-4 = 0,00878906… Anstatt wie in diesen Beispielen e in Zweierkomplementdarstellung abzuspeichern, verwendet man die sogenannte biased-Notation: E=e+e’, wobei e’=2L-1-1. Damit ist 0E(2L-1) positiv, und (v, m, E) wird als (-1)v * m * bE -(e’+n) interpretiert. Zum Beispiel ist für L=4 der Wert von e’=7, also ein Exponent E=1010 entspricht e=3. Damit ergibt sich (0, 1001, 1010) = 9 * 23-4 = 4,5. Bei 32-Bit Gleitkommazahl mit 8-Bit Exponent gilt: e’=127, bei 64-Bit Gleitkommazahlen ist e’=1023. In Java gibt es zwei Datentypen für Gleitpunktzahlen: Datentyp float (32 Bit) mit dem Wertebereich (+/-)1.4*10-45..3.4*1038.(7 relevante Dezimalstellen: 23 Bit Mantisse, 8 Bit Exponent) Datentyp double (64 Bit) mit dem Wertebereich (+/-)4.9*10-324..1.79*10308.(15 relevante Stellen: 52 Bit Mantisse, 11 Bit Exponent) Zeichendarstellung Zeichen über einem gegebenen Alphabet A werden meist als Bitfolgen einer festen Länge nlog(|A|) codiert. Oft wird n so gewählt, dass es ein Vielfaches von 4 oder von 8 ist. Beispiel: Um Texte in einem Buch darzustellen, benötigt man ein Alphabet von 26 Kleinbuchstaben, ebenso vielen Großbuchstaben, einigen Satzzeichen wie etwa Punkt, Komma und Semikolon und Spezialzeichen wie "+", "&", "%". Daher hat eine normale Schreibmaschinentastatur eine Auswahl von knapp hundert Zeichen, für die 7 Bit ausreichen. Bereits in den 1950-ern wurden die Codes ASCII und EBCDIC hierfür entwickelt. (siehe ASCII-Tabelle oben). Mit einem Byte lassen sich 256 Zeichen codieren. Viele PCs benutzen den Code-Bereich [128 .. 255] zur Darstellung von sprachspezifischen Zeichen wie z.B. "ä" (ASCII 132), "ö" (ASCII 148) "ü" (ASCII 129) und einigen Sonderzeichen anderer Sprachen. Leider ist die Auswahl der sprachspezifischen Sonderzeichen eher zufällig und bei weitem nicht ausreichend für die vielfältigen Symbole fremder Schriften. Daher wurden von der „International Standardisation Organisation“ (ISO) verschiedene ASCII-Erweiterungen normiert. In Europa ist dazu die 8Bit ASCII-Erweiterung „Latin-1“ nützlich, die durch die Norm ISO8859-1 beschrieben wird. Mit steigender Speicherverfügbarkeit geht man heutzutage von 8-Bit Codes zu 16-Bit Codes über. Hier gibt es zwei häufig genannte Standards: UCS und Unicode. - [ISO10646]: "Information Technology -- Universal Multiple-Octet Coded Character Set (UCS) -- Part 1: Architecture and Basic Multilingual Plane", ISO/IEC 106461:1993. - [UNICODE]: "The Unicode Standard: Version 2.0", The Unicode Consortium, Addison-Wesley Developers Press, 1996. Siehe auch http://www.unicode.org In den aktuellen Spezifikationen sind beide Codes zeichengleich. Die ersten 128 Zeichen dieser Codes sind ASCII-kompatibel, die ersten 256 Zeichen sind kompatibel zu dem oben genannten Code ISO-Latin-1. Darüber hinaus codieren sie alle gängigen Zeichen dieser Welt. Herkömmliche Programmiersprachen lassen meist keine Zeichen aus ASCII-Erweiterungen zu. Java erlaubt als erste der weit verbreiteten Sprachen die Verwendung beliebiger UnicodeZeichen. Allerdings heißt dies noch lange nicht, dass jede Java-Implementierung einen Editor zur Eingabe von Unicode mitliefern würde. Zeichenketten (strings) werden üblicherweise durch Aneinanderfügen einzelner codierter Zeichen repräsentiert. Beispiel: Dem Text "Hallo Welt" entspricht die Zeichenfolge "H", "a", "l", "l", "o", " ", "W", "e", "l", "t" Diese wird in ASCII folgendermaßen codiert: 072 097 108 108 111 032 087 101 108 116. In Hexadezimal-Schreibweise lautet diese Folge: 48 61 6C 6C 6F 20 57 65 6C 74. Dem entspricht die Bitfolge: 01001000 01100001 01101100 01101100 01101111 00100000 01010111 01100101 01101100 01110100. Obwohl diese Repräsentation sicher nicht die speichereffizienteste ist, ist sie weit verbreitet; bei Speichermangel greift man eher auf eine nachträgliche Komprimierung als auf andere Codes zurück. Darstellung sonstiger Informationen Natürlich lassen sich in einem Computer nicht nur Bits, Zahlen und Zeichen repräsentieren, sondern z.B. auch visuelle und akustische Informationen. Für Bilder gibt es dabei prinzipiell zwei verschiedene Verfahren, nämlich als Vektor- und als Pixelgrafik. Auch für Töne gibt es verschiedene Repräsentationsformen: als Folge von Noten (Midi), Schwingungsamplituden (wav, au) oder komprimiert (mp3). Diese Repräsentationsformen lassen sich jedoch auffassen als strukturierte Zusammensetzungen einfacher Datentypen; wir werden später noch detailliert auf verschiedene Strukturierungsmöglichkeiten für Daten eingehen. Wichtig: Der Bitfolge sieht man nicht an, ob sie die Repräsentation einer Zeichenreihe, einer Folge von ganzen Zahlen oder reellen Zahlen in einer bestimmten Genauigkeit ist. Ohne Decodierungsregel ist eine codierte Nachricht wertlos! 2.2 Sprachen, Grammatiken, Syntaxdiagramme Die obigen Darstellungsformen sind geeignet zur Repräsentation einzelner Objekte. Häufig steht man vor dem Problem, Mengen von gleichartigen Objekten repräsentieren zu müssen. Für endliche Mengen kann dies durch die Folge der Elemente geschehen; für unendliche Mengen geht das im Allgemeinen nicht. Daher muss man sich andere (symbolische) Darstellungsformen überlegen. Darstellung von Sprachen Ein besonders wichtiger Fall ist die Darstellung von unendlichen Mengen von Wörtern, die einem bestimmten Bildungsgesetz unterliegen; zum Beispiel die Menge der syntaktisch korrekten Eingaben in einem Eingabefeld, oder die Menge der Programme einer Programmiersprache. Eine Sprache ist eine Menge von Wörtern über einem Alphabet A. - z.B. {a, aab, aac} - z.B. Menge der grammatisch korrekten Sätze der dt. Sprache - z.B. Menge der JAVA-Programme Man unterscheidet zwischen natürlichen Sprachen wie z.B. deutsch und englisch und formalen Sprachen wie z.B. JAVA, C++ oder der Menge der Primzahlen in Hexadezimaldarstellung. Da Leerzeichen in der Lehre von den formalen Sprachen genau wie andere Zeichen behandelt werden, gibt es hier keinen Unterschied zwischen Wörtern und Sätzen. Unter Syntax versteht man die Lehre von der Struktur einer Sprache Welche Wörter gehören zur Sprache? Wie sind sie intern strukturiert? z.B. Attribut, Prädikatverbund, Adverbialkonstruktion Unter Semantik versteht man die Lehre von der Bedeutung der Sätze Welche Information transportiert ein Wort der Sprache? In der Linguistik betrachtet man manchmal noch den Begriff Pragmatik, darunter versteht man die Lehre von der Absicht von sprachlichen Äußerungen Welchen Zweck verfolgt der Sprecher mit einem Wort? Berühmtes Beispiel für den Unterschied zwischen Semantik und Pragmatik ist die BeifahrerAussage „Die Ampel ist grün“. Grammatiken Grammatiken sind Werkzeuge zur Beschreibung der Syntax einer Sprache. Eine Grammatik stellt bereit A : Alphabet oder „Terminalzeichen“ H : Hilfssymbole = syntaktische Einheiten (<Objekt<, <Attribut>,..) oder „Nonterminalzeichen“ Aus A und H bildet man Satzformen (Schemata korrekter Sätze) z.B. “<Subjekt> <Prädikat> <Objekt>”, “ Heute <Prädikat> < Subjekt> <Objekt> “ R: Ableitungsregeln – erlaubte Transformationen auf Satzformen s: Ein ausgezeichnetes Hilfssymbol („Gesamtsatz”, „Axiom“) Formal ist eine Grammatik ein Tupel G = [A, H, R, s], wobei A und H Alphabete sind mit A H = Ø R (A H)+ (A H)* sH Die Relation R wird meist mit dem Symbol oder ::= (in Infixschreibweise) notiert. Zwischen Satzformen definieren wir eine Relation –> (direkte Ableitungsrelation) w–>w’, falls w = w1°u°w2, w’ = w1°v°w2 und (u,v) R Die Ableitungsrelation => ist die reflexiv-transitive Hülle der direkten Ableitungsrelation: w=>w’, falls w=w’ oder es gibt ein v (A H)* mit w–>v und v=>w’ Die von der Grammatik G beschriebene Sprache LG ist definiert durch LG = { w | s => w und w A* } Beispiel A={“große“, “gute“, “jagen“, “lieben”, “Katzen”, “Mäuse”} H={<attribut>,<objekt>,<prädikatsverband>,<satz>,<subjekt>,<substantiv>,<verb>} s=<satz> R: <satz> <subjekt> <prädikatsverband> “.” <subjekt> <substantiv> <subjekt> <attribut> <substantiv> <attribut>“gute” <attribut>“große” <substantiv>“Katzen” <substantiv>“Mäuse” <prädikatsverband><verb> <objekt> <verb>“lieben” <verb>“jagen” <objekt><substantiv> <objekt><attribut> <substantiv> Beispiel für Generierung: <satz> –> <subjekt> <prädikatverband> “.” –> <attribut> <substantiv> <verb> <objekt> “.” –> “gute” “Katzen” “jagen” “Mäuse” “.” Beispiel für Akzeptierung: “Katzen lieben große Mäuse <– <substantiv> <verb> <attribut> <substantiv> <– <subjekt> <verb> <objekt> <– <subjekt> <prädikatverband> <– <satz> .” “.” “.” “.” Noch ein Beispiel <S> <E> <S> <S> “+” <E> <E> <T> <E> <E> “•” <T> <T> <F> <T> “-” <F> <F> “x” <F> “0” <F> “1” <F> “(“ <S> “)” Ableitung aus dem Beispiel: <S> => “1•(1+x)+0•-1” <S> –> <S> “+” <E> –> <E> “+”<E> “•” <T> –> <E>“•” <T> “+”<T>“ •” “-” <F> –> <T>“•” <F> “+” <F>“•” “-” “1” –> <F>“•” “ (“ <S> “)” “+” “0” “•” “-” “1” –> “1” “•” “(“ <S>“+” <E> “)” “+” “0” “•” “-” “1” –> “1” “•” “(“ <E>“+” <T> “)” “+” “0” “•” “-” “1” –> “1” “•” “(“ <T>“+” <F> “)” “+” “0” “•” “-” “1” –> “1” “•” “(“ <F>“+” “x” “)” “+” “0” “•” “-” “1” –> “1” “•” “(“ “1” “+” “x” “)” “+” “0” “•” “-” “1” Grammatiktypen – Die Chomsky-Hierarchie Die Lehre von den Grammatiken wurde vom amerikanischen Linguisten Noam Chomsky (*) („America's most prominent political dissident“, (*)) in den späten 1950-ern entwickelt. Chomsky unterscheidet vier Typen von Grammatiken: Typ 0 - beliebige Grammatiken Typ 1 - kontextsensitive Grammatiken o Ersetzt wird ein einziges Hilfssymbol, nichtverkürzend o Alle Regeln haben die Form u+h+w u+v+w mit h H o u , w heißen linker bzw. rechter Kontext o Sonderregel für das leere Wort Typ 2 – kontextfreie Grammatiken o Ersetzt wird ein einziges Hilfssymbol, egal in welchem Kontext o Alle Regeln haben die Form h w mit h H Typ 3 – reguläre Grammatiken o Wie Typ 2, in w kommt aber max. ein neues Hilfssymbol vor, und zwar ganz rechts Abhängig vom Typ spricht man auch von einer Chomsky-i-Grammatik. Beispiele (A={a, b, c}, H={s, x, y, z, …}): Chomsky-0-Regeln sind z.B. die folgenden: xyz ::= zyx, abc ::= x, axby ::= abbxy Eine Chomsky-0-Grammatik für {anbncn | n>0} erhält man durch folgende Regeln: s::= xs’z; s’ ::= s’s’; s’ ::= abc; ba ::= ab; ca ::= ac; cb ::= bc; xa ::= ax; x ::= y; yb ::= by; y ::= z; zc ::= cz; zz ::= xyz ::= xyxyz (Chomsky-1-Regel) Anwendung: axyzxa -> axyxyza -> axyxyxyza -> … y ::= yxy (Chomsky-2-Regel) x::= ax; x::= b (Chomsky-3-Sprache a*b ) Eine Sprache heißt Chomsky-i-Sprache, wenn es eine Chomski-i-Grammatik für sie gibt, aber keine Chomsky-(i-1)-Grammatik. Die vier Typen bilden eine echte Hierarchie, d.h., für i=0,1,2 kann man jeweils eine Sprache finden, die durch eine Chomsky-i-Grammatik, nicht aber durch eine Chomsky-i+1-Grammatik beschreibbar ist. Es gilt: - Mit beliebigen Grammatiken lassen sich alle berechenbaren Sprachen beschreiben - Kontextsensitive Grammatiken sind algorithmisch beherrschbar - Für die meisten Programmiersprachen wird die Syntax in Form kontextfreier Grammatiken angegeben. - Einfache Konstrukte innerhalb von Programmiersprachen (z.B. Namen, Zahlen) werden durch reguläre Grammatiken beschrieben. Aufschreibkonventionen für kontextfreie Grammatiken Backus-Naur-Form (BNF) verwendet u ::= v | w als Abkürzung für {u ::= v, u ::= w} Beispiel: <S> ::= <Expression> | <S> + <Expression> <Expression> ::= <Term> | <Expression> • <Term> <Term> ::= <Factor> | - <Factor> <Factor> ::= x | 0 | 1 | ( <S> ) Erweiterte Backus-Naur-Form (EBNF) löst direkte Rekursion durch beliebigmalige Wiederholungsklammern {} auf Beispiel: S = Expression { “+” Expression } Expression = Term { “•” Term } Term = [ “-” ] Factor Factor = “x” | “0” | “1” | “(” S “)” Manchmal verwendet man auch zählende Wiederholungen ...i mit der Bedeutung „…mindestens i und höchstens j mal“, wobei j auch durch einen Stern ersetzt werden kann * 1 (für „beliebig mal“). Es gilt {x} x 0 und x x 0 . j Beispiele: a30 { , a, aa, aaa} ; [a]*2 = {aa, aaa, aaaa, …} Syntaxdiagramme Syntaxdiagramme werden zur anschaulichen Notation von Programmiersprachen verwendet. Reguläre Ausdrücke Für reguläre Sprachen gibt es die Möglichkeit, sie durch reguläre Ausdrücke aufzuschreiben. Das sind Ausdrücke, die gebildet sind aus der leeren Sprache, die keine Wörter enthält den Sprachen bestehend aus einem Wort bestehend aus einem Zeichen des Alphabets der Vereinigung von Sprachen (gekennzeichnet durch +) Konkatenation (gekennzeichnet durch Hintereinanderschreibung, oder ;) beliebiger Wiederholung (gekennzeichnet durch *) Beispiele: (aa)* (gerade Anzahl von a) aa* (mindestens ein a, auch a+ geschrieben) ((a + b)b)* (jedes zweite Zeichen ist b) Reguläre Ausdrücke werden z.B. in Editoren wie dem Emacs verwendet, um bestimmte zu suchende Zeichenreihenmengen zu repräsentieren. Beispiel für eine Suche ist: „Suche eine Zeichenreihe, die mit c beginnt und mit c endet und dazwischen eine gerade Anzahl von a enthält.“ Das Kommando replace-regexp \(aa\)+ b ersetzt jedes Paar „aa“ durch „b“ Endliche Automaten sind Syntaxdiagramme ohne Abkürzungen, d.h. es gibt keine „Kästchen“. Ein endlicher Automat hat genau einen Eingang und mehrere mögliche Ausgänge. a Beispiele: b a a a a b Üblicherweise werden bei Automaten die Kreuzungen als Kreise gemalt und Zustände (states) genannt, die Zustände werden mit Pfeilen verbunden (sogenannten Transitionen, transitions), die mit Terminalsymbolen beschriftet sind. a a a a b a b Ein fundamentaler Satz der Theorie der formalen Sprachen besagt, dass mit regulären Ausdrücken genau die durch reguläre Grammatiken definierbaren Sprachen beschrieben werden können, welches wiederum genau die Sprachen sind, die durch endliche Automaten beschrieben werden können. 2.3 Darstellung von Algorithmen Ein Algorithmus ist ein präzises, schrittweises und endliches Verfahren zur Lösung eines Problems oder einer Aufgabenstellung (insbesondere zur Verarbeitung von Informationen, vgl. Kap. 0). Das bedeutet, an einen Algorithmus sind folgende Anforderungen zu stellen: Präzise Beschreibung (relativ zu den Kommunikationspartnern) der zu bearbeitenden Informationen und ihrer Repräsentation Explizite, eindeutige und detaillierte Beschreibung der einzelnen Schritte (relativ zu der Person oder Maschine, die den Algorithmus ausführen soll) Endliche Aufschreibung des Algorithmus, jeder Einzelschritt ist in endlicher Zeit ausführbar, und jedes Ergebnis wird nach endlich vielen Schritten erzielt. Um Algorithmen von einem Computer ausführen zu lassen, müssen sie (genau wie andere Informationen) in einer für die jeweilige Maschine verständlichen Form repräsentiert werden. Eine solche Repräsentation nennt man Programm. Zum Beispiel kann das eine Folge von Maschinenbefehlen sein, die ein bestimmter Rechner ausführen kann. Die tatsächliche Ausführung eines Algorithmus bzw. Programms nennt man einen Prozess. Sie findet auf einem (menschlichen oder maschinellen) Prozessor statt. Ein Algorithmus bzw. Programm terminiert, wenn seine Ausführung nach einer endlichen Zahl von Schritten (Befehlen) abbricht. Ein Algorithmus (bzw. ein Programm) heißt deterministisch, wenn für jeden Schritt der nächste auszuführende Schritt eindeutig definiert ist. Er bzw. es heißt determiniert, wenn die Berechnung nur ein mögliches Ergebnis hat. Beispiel: Wechselgeldbestimmung Aufgabe: Bestimmung des Wechselgeldes w eines Fahrscheinautomaten auf 5 Euro bei einem Preis von p Euro (zur Vereinfachung fordern wir, p sei ganzzahliges Vielfaches von 10 Cent; es werden nur 10-, 20- und 50-Cent Münzen zurückgegeben). Der Algorithmus ist nicht determiniert! Damit das Ergebnis eindeutig bestimmt ist, legen wir zusätzlich fest: „es sollen möglichst wenige Münzen zurückgegeben werden“. Als erstes muss die Repräsentation der Schnittstellen festgelegt werden: p könnte zum Beispiel als natürliche Zahl in Cent, als rationale Zahl oder als Tupel (Euro, Cent) repräsentiert werden, und w als Multimenge oder Folge von Münzen. Dann erfolgt die Aufschreibung des Algorithmus. 1. Darstellung in natürlicher Sprache: „Die Rückgabe enthält maximal eine 10-Cent-Münze, zwei 20-Cent-Münzen, und den Rest in 50-Cent-Münzen. 10-Cent werden zurückgegeben, falls dies nötig ist, um im Nachkommabereich auf 0, 30, 50, oder 80 Cent zu kommen. Eine oder zwei 20-Cent-Münzen werden zurückgegeben, um auf 0, 40, 50 oder 90 Cent zu kommen. Wenn der Nachkommaanteil 0 oder 50 ist, werden nur 50-Cent-Münzen zurückgegeben.“ Ein Vorteil der natürlichen Sprache ist, dass sie sehr flexibel ist und man damit (fast) alle möglichen Ideen aufschreiben kann. Nachteile bei der Darstellung von Algorithmen in natürlicher Sprache sind, dass keine vollständige formale Syntax bekannt ist, d.h. es ist nicht immer leicht festzustellen, ob eine Beschreibung syntaktisch korrekt ist oder nicht, und dass die Semantik weitgehend der Intuition überlassen bleibt. Dadurch bleibt immer eine gewisse Interpretationsfreiheit, verschiedene Prozessoren können zu verschiedenen Ergebnissen gelangen. 2. Darstellung als nichtdeterministischer Pseudo-Code: Sei w={} Multimenge; Solange p<5 tue [] erste Nachkommastelle von p {2,4,7,9}: p = p + 0.10, w = w {10c} [] erste Nachkommastelle von p {1,2,3,6,7,8}: p = p + 0.20, w = w {20c} [] erste Nachkommastelle von p {0,1,2,3,4,5}: p = p + 0.50, w = w {50c} Ergebnis w Auch Pseudo-Code verwendet keine feste Syntax, es dürfen jedoch nur solche Sprachelemente verwendet werden, deren Bedeutung (Semantik) klar definiert ist. Damit ist die Menge der möglichen Ergebnisse zu einer Eingabe eindeutig bestimmt. Berechnungsbeispiel: p=3.20, w={} p=3.70, w={50c} p=3.80, w={50c, 10c} p=4.30, w={50c, 10c, 50c} p=4.50, w={50c, 50c, 10c, 20c} p=5.00, w={50c, 50c, 50c, 20c, 10c} 3. Darstellung mathematisch-rekursiv: Wechselgeld (p) = {}, falls p=5 0,50+Wechselgeld (p+0.50), falls p5 und 5-p 0.50 0,20+Wechselgeld (p+0.20), falls p5, 5-p < 0.50 und 5-p 0.20 0,10+Wechselgeld (p+0.10), sonst Beispiel zum Berechnungsablauf: Wechselgeld(3.20) =0.50+Wechselgeld(3.70) =0.50+0.50+Wechselgeld(4.20) =0.50+0.50+0.50+Wechselgeld(4.70) =0.50+0.50+0.50+0.20+Wechselgeld(4.90) =0.50+0.50+0.50+0.20+0.10+Wechselgeld(5.00) =0.50+0.50+0.50+0.20+0.10 Die Syntax und Semantik in dieser Aufschreibung folgt den üblichen Regeln der Mathematik bzw. Logik, ist fest definiert, aber jederzeit erweiterbar. In der Schreibweise der Informatik könnte man dasselbe etwa wie folgt aufschreiben: Wechselgeld (p) = Falls 5-p=0 dann Rückgabe({}) sonst falls 5-p 0.50 dann Rückgabe(0.50+Wechselgeld(p+0.50)) sonst falls 5-p 0.20 dann Rückgabe(0.20+Wechselgeld(p+0.20)) sonst Rückgabe(0.10+Wechselgeld(p+0.10)) Diese Notation lässt sich unmittelbar in funktionale Programmiersprachen übertragen. Als BC-Programm aufgeschrieben sieht das etwa so aus: define wg(p){ if (5-p==0) return(0) else if (5-p>=0.50) return(100+wg(p+0.50)) else if (5-p>=0.20) return(10+wg(p+0.20)) else return(1+wg(p+0.10))} Hier wählen wir zur Darstellung des Ergebnisses eine dreistellige natürliche Zahl, deren erste Stelle die Anzahl der 50-Cent-Münzen, die zweite die Anzahl der 20-Cent-Münzen und die letzte die Anzahl der 10-Cent-Münzen angibt. wg(2.70) 411 wg(3.10) 320 4. Darstellung als Ablaufdiagramm oder Flussdiagramm. Start Int(Fract(p)*10) {1, 6}? w={} Int(Fract(p)*10) {2,4,7,9}? n n y w+={20c} p +=0.2 y w+={10c} p +=0.1 p<5? y w+={50c} p +=0.5 Int(Fract(p)*10) {1, 6}? n y w+={20c} p +=0.2 Stop Ein Ablaufdiagramm ist prinzipiell ein endlicher Automat! Zusätzlich dürfen jedoch Variablen, Bedingungen, Zuweisungen, Unterprogramme und andere Erweiterungen verwendet werden. Ausführungsbeispiel: p=3.20, w={} p=3.30, w={10c} p=3.50, w={10c, 20c} p=4.00, w={10c, 20c, 50c} p=4.50, w={10c, 20c, 50c, 50c} p=5.00, w={10c, 20c, 50c, 50c, 50c} 5. Darstellung als Java-Programm (mit heftigem Gebrauch von Mathematik) static int fünfzigCentStücke(double p) { return (int)((5 - p) * 10 / 5) ;} static int zehnCentStücke(double p) { int cent = (int)((p-(int)(p)) * 100); return((cent==20 || cent==40 || cent==70 || cent==90) ? 1 : 0) ;} static int zwanzigCentStücke(double p) { int tencent = (int)((p-(int)(p)) * 10); int[] coins = {0,2,1,1,0,0,2,1,1,0}; return coins[tencent] ;} Syntax und Semantik sind eindeutig definiert, jedes in Java aufschreibbare Programm ist determiniert und deterministisch (Ausnahme: Verwendung paralleler Threads). Wie man in diesem Fall auch leicht sieht, terminiert das Programm auch für jede Eingabe. Eine wichtige Frage ist die nach der Korrektheit, d.h. berechnet das Programm wirklich das in der Aufgabenstellung verlangte? n 6. Darstellung als Assemblerprogramm In der Frühzeit der Informatik wurden Rechenmaschinen programmiert, indem eine Folge von Befehlen angegeben wurde, die direkt von der Maschine ausgeführt werden konnte. Assemblersprachen sind Varianten solcher Maschinensprachen, bei denen die Maschinenbefehle in mnemotechnischer Form niedergeschrieben sind. Häufig verwendete Adressen (Nummern von Speicherzellen) können mit einem Namen versehen werden und dadurch referenziert werden. Viele Assemblersprachen erlauben auch indirekte Adressierung, d.h. der Inhalt einer Speicherzelle kann die Adresse einer anderen Speicherzelle. Die in einer Assemblersprache verfügbaren Befehle hängen stark von der Art der zu programmierenden Maschine ab. Typische Assemblerbefehle sind z.B. mov rx ry (Bedeutung: transportiere / kopiere den Inhalt von rx nach ry) oder jgz rx lbl (Bedeutung: wenn der Inhalt von rx größer als Null ist, gehe zur Sprungmarke lbl) // Eingabe: Preis in Register p in Cent (wird zerstört) // Ergebnisse in fc, zc, tc (fünfzigCent, zwanzigCent, tenCent) mov 0, fc mov 0, zc mov 0, tc loop: mov p, ac // lade p in den Akkumulator sub ac, 450 // subtrahiere 450 jgz hugo // jump if greater zero to hugo add ac, 500 // addiere 500 mov ac, p // speichere Akkumulator nach p mov fc, ac add ac, 1 mov ac, fc // fc := fc + 1 goto loop hugo: mov p, ac // wie oben sub ac, 480 jgz erna add ac, 500 mov ac, p mov zc, ac add ac, 1 mov ac, zc goto hugo erna: mov p, ac sub ac, 490 jgz fertig mov 1, tc fertig: 7. Darstellung als Maschinenprogramm Ein Maschinenprogramm ist eine Folge von Befehlen, die direkt von einer Maschine eines dafür bestimmten Typs ausgeführt werden kann. Jedem Assemblerbefehl entspricht dabei eine bestimmte Anzahl von Bytes im Maschinenprogramm. Programmiersprachen Ein Programm ist, wie oben definiert wurde, die Repräsentation eines Algorithmus in einer Programmiersprache. Die Menge der syntaktisch korrekten Programme einer bestimmten Programmiersprache (JAVA, C, Delphi, …) wird im Allgemeinen durch eine kontextfreie Grammatik beschrieben. Maschinen- und Assemblersprachen sind dabei sehr einfache Sprachen (die sogar durch reguläre Grammatiken definiert werden könnten). Das Programmieren in einer Maschinensprache oder Assembler ist außerordentlich mühsam und sehr fehleranfällig. Höhere Programmiersprachen sind nicht an Maschinen orientiert, sondern an den Problemen. Programme, die in einer höheren Programmiersprache geschrieben sind, können nicht unmittelbar auf einem Rechner ausgeführt werden. Sie werden entweder von einem speziellen Programm interpretiert (d.h., direkt ausgeführt) oder von einem Compiler in eine Folge von Maschinenbefehlen übersetzt und erst dann ausgeführt. Bei einigen Prgrammiersprachen (Java, C#, USCD-Pascal) erfolgt die Übersetzung zunächst in die Maschinensprache einer virtuellen Maschine, d.h. einer nicht in Hardware realisierten Maschine, welche daher unabhängig von einer speziellen Hardwaretechnologie ist. Die Ausführung von Maschinenprogrammen der virtuellen auf einer realen Maschine erfolgt von speziellen Interpretern (der natürlich für jede reale Maschine neu entwickelt werden muss). Die Sprachen, die einer maschinellen Behandlung nicht zugänglich sind und von Menschen in Programme überführt werden müssen, nennen wir Spezifikationssprachen. Kapitel 3: Rechenanlagen Um Programme auszuführen, ist ein Prozessor erforderlich, der die einzelnen Schritte tätigt. Das kann ein Mensch oder eine Maschine (auf mechanischer, elektronischer oder biochemischer Basis) sein, oder sogar ein anderes Programm, welches eine Ausführungsmaschine nur simuliert. 3.1 Historische Entwicklung Die Entwicklung und den Aufbau moderner Rechner begreift man besser, wenn man sich ihre historischen Wurzeln betrachtet. 1842 Charles Babbage / Ada Lovelace: „Die analytische Maschine“; Konzept einer programmierbaren mechanischen Rechenanlage zur Lösung von Differentialgleichungen. Dieser „erste Computer der Weltgeschichte“ wurde jedoch nie realisiert, da Kosten, Machbarkeit und Haltbarkeit nicht einschätzbar waren 1936: Alonzo Church (1903-1995): „lambda-Kalkül“, Begriff der berechenbaren Funktion 1936: Alan Turing: Computer als universelle Maschine; Äquivalenz von Programm und Daten („Turing-Maschine“) 1941 Konrad Zuse: Z3: vollautomatischer, programmierbarer, in binärer Gleitkommarechnung arbeitender Rechner mit Speicher und einer Zentralrecheneinheit aus Telefonrelais 1946 Johan von Neumann (EDVAC-Report): konkrete Vorschläge für Aufbau („vonNeumann-Computer“) Programmierparadigmen: - Analytische Maschine: Rechnen als Durchführung arithmetischer Operationen - lambda-Kalkül, Lisp-Maschine: Rechnen als Termersetzung - Turing-Maschine (vgl. ThI): Rechnen als Schreiben von Zeichen auf ein Band - von Neumann: Rechnen als Modifikation von Wörtern im Speicher. Die analytische Maschine Babbage’s „analytische Maschine“ (*) war das „Nachfolgemodell“ der „Differenzmaschine“ (1821-1833), die arithmetische Berechnungen durchführen können sollte, aber nie funktionierte. Zitat eines Zeitgenossen (L. F. Menabrea, *): “Mr. Babbage has devoted some years to the realization of a gigantic idea. He proposed to himself nothing less than the construction of a machine capable of executing not merely arithmetical calculations, but even all those of analysis, if their laws are known.” Historisches Vorbild waren mit so genannten JaquardLochkarten „programmierbare“ Webstühle (mit bis zu 24000 Karten). Die Rechenmaschine sollte ein „Mill“ genanntes Rechenwerk, ein „Store“ genanntes Speicherwerk (1000 fünfzigstellige Zahlen), Lochkartenleser und -stanzer als Ein- und Ausgabe und einen Drucker als Ausgabe enthalten. Die Maschine sollte mit Dampf angetrieben und frei programmierbar sein. Ein Programm sollte drei Kartentypen enthalten: Operationskarten enthalten mögliche Operationen: Addition, Subtraktion, Multiplikation und Division. Die Maschine hat einen Schalter für den auszuführenden Operationstyp, der in seiner Stellung bleibt bis er durch eine Operationskarte umgestellt wird. Zahlenkarten enthalten numerische Konstanten und dienen als externer Speicher, damit nicht alle benötigten Zahlen im (teuren) Speicherwerk bereit gehalten werden müssen. Auf einer Zahlenkarte steht jeweils neben dem Wert auch die Nummer des Speichers, in welchen dieser Wert geschrieben werden soll. Zwischenergebnisse können von der Maschine auf Karten gestanzt und später wieder eingelesen werden. Variablenkarten steuern den Transfer von Werten aus dem Store zur Mill und zurück („Adressierung“). Die Maschine besitzt zwei Operandenregister („Ingress-Achsen“, je zweimal 50 Stellen: I1 und I1´, I2 und I2´) und ein Resultatregister („Egress-Achse“, zweimal 50 Stellen: E und E´); es gibt Karten zum Transport einer Variable (eines Speicherwerts) in die Ingress-Achsen und zum Transport der Egress-Achse in den Speicher. Spezielle Karten sind kombinatorische und Indexkarten, die im Kartenstapel vor- und zurückblättern können und somit Sprünge realisieren. Für Verzweigungen gibt es einen „Alarmhebel“, der hochgesetzt wird, falls bei einer arithmetischen Operation ein Überlauf oder eine Division durch Null auftritt das Ergebnis einer arithmetischen Operation ein anderes Vorzeichen hat als das erste Argument (d.h. Egress-Achse E hat ein anderes Vorzeichen als die Ingress-Achse I1 ) Ferner gibt es Kontrollkarten wie „Stopp“ und „Pause“. Aus den vorhandenen Dokumenten lässt sich rekonstruieren, dass für die analytischen Maschine folgende Befehle vorgesehen waren (Ausschnitt): <Programm> ::= {Karte} <Karte> ::= <Zahlkarte> | <Opkarte> | <Varkarte> <Zahlkarte> ::= “N”[z ]13 _ [“+|“-][z ]50 0 Die Zahl wird an der bezeichneten Stelle in den Speicher eingetragen <Opkarte> ::= “+” | “-” | “*” | “/” Die Operation wird für nachfolgende Befehle eingestellt <Varkarte> ::= <Transferkarte> | <Kartenkarte> | <Atkarte> <Transferkarte> ::= (“L”|“Z”|“S”)[z ]13 [“´”] Lzzz: Transfer des Inhalts der Variable zzz in die Mill Ingress Achse Zzzz: Wie Lzzz, wobei Variable zzz gleichzeitig auf Null gesetzt wird Szzz: Transfer der Egress-Achse in Variable zzz Falls ein ´ nach der Adresse steht, sind die zweiten 50 Stellen betroffen Transfer in I2 löst die Ausführung der eingestellten Operation aus <Kartenkarte> ::= “C”(“F”|“B”)(“+”|“?”)[z ]50 0 blättert die angegebene Zahl von Karten vor (F) oder zurück (B) + bedeutes unbedingtes, ? bedingtes Blättern (falls Alarmhebel hochgesetzt) <Atkarte> ::= “B” | “H” | “P” B läutet eine Glocke um den Operateur zu verständigen H hält die Maschine an (keine weiteren Karten werden gelesen) P druckt den Wert der Egress-Achse auf dem Druckapparat Beispiel: drucke 17 + 4 N001 +00000000000000000000000000000000000000000000000017 N002 +00000000000000000000000000000000000000000000000004 + L001 L002 P Beispiel: Variable in Speicher 003 := 10000 div 28, Variable 004 := 10000 mod 28, N1 10000 N2 28 / L1 L2 S3' S4 Beispiel: Fakultätsfunktion S2 := S1! N1 6 N2 1 N3 1 * L2 L1 S2 L1 L3 S1 L3 L1 CB?11 Beispiel: drucke Tabelle für f(x) = x2 + 6x + 6, x=1..10 V1 = x V2 = x2 V3 = 6 V4 = x2, x2+6x, x2+6x+6 V5 = 6x N1 10 N3 6 * L1 L1 S4 L1 L3 S5 + L4 L5 S4 L4 L3 S4 … Umformung als f(x) = (x+3)2 – 3 bringt eine Verbesserung: V1 = x V2 = 3 V3 = x+3, (x+3)2, (x+3)2 – 3 N1 10 N2 3 + L1 L2 S3 * L3 L3 S3 L3 L2 S3 Ada Lovelace schlägt etliche solcher Verbesserungs-Transformationen vor. Ähnliche (einfachere) Optimierungen heute im Code-Generator guter Compiler enthalten. Ada Lovelace schreibt: “the Analytical Engine does not occupy common ground with mere `calculating machines´”… “on the contrary, (it) is not merely adapted for tabulating the results of one particular function and of no other, but for developing and tabulating any function whatever. In fact the engine may be described as being the material expression of any indefinite function of any degree of generality and complexity.“ … “It may be desirable to explain, that by the word operation, we mean any process which alters the mutual relation of two or more things, be this relation of what kind it may. This is the most general definition, and would include all subjects in the universe.” … “Supposing, for instance, that the fundamental relations of pitched sounds in the science of harmony and of musical composition were susceptible of such expression and adaptations, the engine might compose elaborate and scientific pieces of music of any degree of complexity or extent.“ … Gedanken zur Universalität mathematischer Funktionen Die Turingmaschine Nahezu 100 Jahre später (1936) untersuchte Alan Turing die Grenzen des Berechenbaren. Die Arbeit „On Computable Numbers, with an Application to the Entscheidungsproblem“ kann als der Beginn der modernen Informatik betrachtet werden. Turing definierte darin eine hypothetische Maschine, die die „Essenz des Rechnens“ durchführen können soll. Eine „berechenbare“ reelle Zahl ist eine, deren (unendliche Folge von) Nachkommastellen endliche Mittel (d.h. durch einen Algorithmus) berechnet werden kann. Turing schreibt: „Computing is normally done by writing certain symbols on paper. We may suppose this paper is divided into squares like a child's arithmetic book. In elementary arithmetic the twodimensional character of the paper is sometimes used. But such a use is always avoidable, and I think that it will be agreed that the two-dimensional character of paper is no essential of computation. I assume then that the computation is carried out on one-dimensional paper, i.e. on a tape divided into squares. I shall also suppose that the number of symbols which may be printed is finite. If we were to allow an infinity of symbols, then there would be symbols differing to an arbitrarily small extent. … The behaviour of the computer at any moment is determined by the symbols which he is observing, and his "state of mind" at that moment. We may suppose that there is a bound B to the number of symbols or squares which the computer can observe at one moment. If he wishes to observe more, he must use successive observations. We will also suppose that the number of states of mind which need be taken into account is finite. The reasons for this are of the same character as those which restrict the number of symbols. If we admitted an infinity of states of mind, some of them will be "arbitrarily close" and will be confused.“ Eine Turingmaschine ist also gegeben durch einen endlichen Automaten zur Programmkontrolle, und ein (potentiell unbegrenztes) Band auf dem die Maschine Zeichen über einem gegebenen Alphabet notieren kann. Zu jedem Zeitpunkt kann die Maschine genau eines der Felder des Bandes lesen (abtasten, „scannen“), und, ggf.. abhängig von der Inschrift dieses Feldes, das Feld neu beschreiben, zum linken oder rechten Nachbarfeld übergehen, und einen neuen Zustand einstellen. Hier ist eine Syntax, die Turings Originalschreibweise nahe kommt. <Turingtabelle> ::= {<Zeile>} <Zeile> ::= <Zustand> <Abtastzeichen> <Operation> <Zustand> <Zustand> ::= <Identifier> <Abtastzeichen> ::= <Zeichen> <Operation> ::= {R | L | P<Symbol>} Dabei wird angenommen, dass für jeden Zustand und jedes Abtastzeichen des Alphabets genau eine Zeile der Tabelle existiert, welche Operation und Nachfolgezustand festlegt. (Turing nennt solche Maschinen “automatisch”, wir nennen sie heute “deterministisch”. In Turings Worten: „If at any stage the motion of a machine is completely determined by the configuration, we shall call the machine an ‚automatic machine’ ... For some purposes we might use machines (choice machines or c-machimes) whose motion is only partially determined by the configuration… When such a machine reaches one of these ambiguous configurations, it cannot go on until some arbitrary choice has been made by an external operator… In this paper I deal only with automatic machines.’’) Da die Tabellen für deterministische Turingmaschinen oft sehr groß werden, darf man Zeilen, die nicht benötigte werden, weglassen, und Zeilen, die sich nur durch das Abtastzeichen unterscheiden, zusammenfassen. In der entsprechenden Zeile sind beliebige Mengen von Abtastzeichen zugelassen. „any“ steht dann für ein beliebiges Abtastzeichen, d.h. für das gesamte Alphabet. Ferner fordert Turing, dass das Alphabet immer ein spezielles Leerzeichen „none“ enthält, und „E“ eine Abkürzung für „P none“ ist.. Beispiel für eine Maschine, die das Muster „0 11 0 11 0 11…“ auf ein leeres Band schreibt: s0 s1 any any P0,R,R s1 P1,R,P1,R,R s0 Ein äquivalentes Programm mit nur einem Zustand s0 ist s0 none P0 s0 s0 0 R,R,P1,R,P1 s0 s0 1 R,R,P0 s0 Beispiel für eine Maschine, die die Sequenz 0 01 011 0111 01111 … erzeugt: Arbeitsweise dieser Maschine: Die von Turing verwendete Tabellenschreibweise für Programme betrachten wir heute als unleserlich. Eine moderne Variante („Turing-Assembler“) wäre etwa: <Turingprogram> ::= {<statement>} <statement> ::= <label>“:” | “print” <symbol> “;” | “left;” | “right;” | “goto” <label>“;” <label> ::= <Identifier> In dieser Notation sähe unser Beispielprogramm etwa so aus: label0: print 0; right; right; print 1; right; print 1; right; right; goto s0; Im Internet sind viele Turingmaschinen-Simulatoren verfügbar, versuchen Sie z.B. http://math.hws.edu/TMCM/java/labs/xTuringMachineLab.html Andere empfohlene Beispiele: http://www.matheprisma.uni-wuppertal.de/Module/Turing/ http://ais.informatik.uni-freiburg.de/turing-applet/turing/TuringMachineHtml.html Zuse Z3 erster voll funktionsfähiger programmierbarer Digitalrechner viele Merkmale moderner Rechner: Relais-Gleitkommaarithmetikeinheit für Arithmetik einem Relais-Speicher aus 64 Wörtern, je 22 bit einem Lochstreifenleser für Programme auf Filmstreifen eine Tastatur mit Lampenfeld für Ein- und Ausgabe von Zahlen und der manuellen Steuerung von Berechnungen. Taktung durch Elektromotor, der Taktwalze antreibt (5rps) Programmiersprache: Plankalkül http://www.zib.de/zuse/Inhalt/Programme/Plankalkuel/Compiler/plankalk.html Von-Neumann-Rechner, EDVAC & ENIAC John von Neumann wurde vor hundert Jahren (im Dezember 1903) in Budapest geboren. 1929 wurde er als jüngster Privatdozent in der Geschichte der Berliner Universität habilitiert. Von Neumann wurde binnen kurzer Zeit weltberühmt durch seine vielfältigen Interessen auf dem Gebieten Mathematik, Physik und Ökonomie. 1930 emigrierte er wegen der Nazis nach USA. In Princeton schuf von Neumann dann mit dem von ihm erdachten Rechnerkonzept die Grundlagen für den Aufbau elektronischer Rechenanlagen, die noch bis heute gültig sind. Er gilt daher als einer der Begründer der Informatik EDVAC, ENIAC (Electronic Numerical Integrator and Computer) Elektronenröhren zur Repräsentation von Zahlen elektrische Pulse für deren Übertragung Dezimalsystem Anwendung: H-Bomben-Entwicklung Programmierung durch Kabel und Drehschalter 3.2 von-Neumann-Architektur 1945 First Draft of a Report on the EDVAC (Electronic Discrete Variable Automatic Computer): Befehle des Programms werden wie zu verarbeitenden Daten behandelt, binär kodiert und im internen Speicher verarbeitet (vgl. Zuse, Turing) Ein von-Neumann-Computer enthält mindestens die folgenden fünf Bestandteile: 1. Input unit (kommuniziert mit der Umgebung) 2. Main memory (Speicher für Programme und Daten) 3. Control unit (führt die Programme aus) 4. Arithmetic logical unit (für arithmetische Berechnungsschritte) 5. Output unit (kommuniziert mit der Umgebung) Rechenwerk (central processing unit, CPU) Steuerwerk (control unit) Rechenwerk (arithmetic logical unit, ALU) Hauptspeicher (Main memory) Eingabe (input) Ausgabe (output) Prinzipien: Der Rechner enthält zumindest Speicher, Rechenwerk, Steuerwerk und Ein/Ausgabegeräte. „EVA-Prinzip“: Eingabe – Verarbeitung – Ausgabe Der Rechner ist frei programierbar, d.h., nicht speziell auf ein zu bearbeitendes Problem zugeschnitten; zur Lösung eines Problems wird ein Programm im Speicher ablegt. Dadurch ist jede nach der Theorie der Berechenbarkeit mögliche Berechnung programmierbar. o Programmbefehle und Datenworte liegen im selben Speicher und werden je nach Bedarf gelesen oder geschrieben. o Der Speicher ist unstrukturiert; alle Daten und Befehle sind binär codiert. o Der Speicher wird linear (fortlaufend) adressiert. Er besteht aus einer Folge von Plätzen fester Wortlänge, die über eine bestimmte Adresse einzeln angesprochen werden können und bit-parallel verarbeitet werden. o Die Interpretation eines SpeicherinhaltsSteuerwerk hängt nur vom aktuellen Kontext (control unit) des laufenden Programms ab. Insbesondere: Befehle können Operanden anderer Befehle sein (Selbstmodifikation)! Der Befehlsablauf wird vom Steuerwerk bestimmt. Er folgt einer sequentiellen Befehlsfolge, streng seriell und taktgesteuert. o Zu jedem Zeitpunkt führt die CPU nur einen einzigen Befehl aus, und dieser kann (höchstens) einen Datenwert verändern (single-instructionsingle-data). o Die normale Verarbeitung der Programmbefehle geschieht fortlaufend in der Reihenfolge der Speicherung der Programmbefehle. Diese sequentielle Programmabarbeitung kann durch Sprungbefehle oder datenbedingte Verzweigungen verändert werden. Die ALU führt arithmetische Berechnungen durch, indem sie ein oder zwei Datenwerte gemäß eines Befehls verknüpft und das Ergebnis in ein vorgegebenes Register schreibt. Zwei-Phasen-Konzept der Befehlsverarbeitung: o In der Befehlsbereitstellungs- und Decodierphase-Phase wird, adressiert durch den Befehlszähler, der Inhalt einer Speicherzelle geholt und als Befehl interpretiert. o In der Ausführungs-Phase werden die Inhalte von einer oder zwei Speicherzellen bereitgestellt und entsprechend den Opcode als Datenwerte verarbeitet. Datenbreite, Adressierungsbreite, Registeranzahl und Befehlssatz als Parameter der Architektur Ein- und Ausgabegeräte sind z.B. Schalter und Lämpchen, aber auch entfernte Speichermedien (Magnetbänder, Lochkarten, Platten, …). Sie sind mit der CPU prinzipiell auf die selbe Art wie der Hauptspeicher verbunden. Zentrale Befehlsschleife (aus *): Vergleiche: Assemblersprache, Ausführung eines Befehles Vor- und Nachteile der von-Neumann-Architektur: minimaler Hardware-Aufwand, Wiederverwendung von Speicher Konzentration auf wesentliche Kennzahlen: Speichergröße, Taktfrequenz - Verbindungseinrichtung CPU – Speicher stellt einen Engpass dar („von-NeumannFlaschenhals“) - keine Strukturierung der Daten, Maschinenbefehl bestimmt Operandentyp „von-Neumann-bottleneck“ John Backus, Turing-Award-Vorlesung 1978: When von Neumann and others conceived it [the von Neumann computer] over thirty years ago, it was an elegant, practical, and unifying idea that simplified a number of engineering and programming problems that existed then. Although the conditions that produced its architecture have changed radically, we nevertheless still identify the notion of "computer" with this thirty [jetzt fast sechzig] year old concept. In its simplest form, a von Neumann computer has three parts" a central processing unit (or CPU), a store, and a connecting tube that can transmit a single word between the CPU and the store (and send an address to the store). I propose to call this tube the von Neumann bottleneck. The task of a program is to change the store in a major way; when one considers that this task must be accomplished entirely by pumping single words back and forth through the von Neumann bottleneck, the reason for its name becomes clear. Wie oben erwähnt, sind auch heute noch die meisten Computer nach der von-NeumannArchitektur konstruiert. Beispiel: Architektur des Intel Pentium-Prozessors (*): Realisierung der einzelnen Baugruppen durch Mengen von Halbleiterschaltern; z.B. eines Halbaddierers (Summe= E1 XOR E2, Übertrag=E1 AND E2): Beschreibung der Hardware auf verschiedenen Ebenen: Physikalische Ebene, TransistorEbene, Gatter-Ebene (s.o.), Register-Ebene, Funktionsebene (siehe TI) Abweichungen und Varianten der von-Neumann-Architektur: Spezialisierte Eingabe-Ausgabe-Prozessoren o z.B. Grafikkarte mit 3D-Rendering o z.B. Modem oder Soundkarte zur Erzeugung von Tönen o z.B. Tastatur-, Netzwerk- oder USB-Controller, die auf externe Signale warten Parallelität zwischen/innerhalb von Funktionseinheiten, z.B. o Blocktransfer von Daten o Pipelining in CPU Duplikation von Funktionseinheiten o z.B. Mehrprozessorrechner, Mehrkern-Architektur: Zwei oder mehr CPU auf einem Chip, Kopplung durch speziellen Memory-Control-Bus; z.B. Pentium Extreme Edition 840 (April 2005), Preis 999 Dollar, „dürfte allerdings nur wenig Käufer finden“; Aktuell: Quadcore, Octocore (Sun UltraSparc, Intel Nehalem 2008); komplexere Speicherstrukturen, z.B. o Register (einzelne Speicherwörter direkt in der CPU) o Caches (schnelle Pufferspeicher) und Bus-Hierarchien für Verbreiterung des Flaschenhalses o Harvard-Architektur (Trennung von Daten- und Befehlsspeicher) komplexere Verbindungsstrukturen o externe Standardschnittstellen: IDE, SCSI, USB, IEEE1394/Firewire/iLink o ISA/PCI/AGP: hierarchischer bzw. spezialisierter Aufbau des Bussystems komplexere Befehle, z.B. o mehrere Operanden o indirekte Adressierung („Adresse von Adresse“) o CISC versus RISC Programmunterbrechung durch externe Signale o Interrupt-Konzept o Mehrbenutzer-Prozesskonzept 3.3 Aufbau PC/embedded system, Speicher Heutige PCs sind meist prinzipiell nach der von-Neumann-Architektur , mit o.g. Erweiterungen, konstruiert. Beispiel (Gumm/Sommer p55) Prinzipiell ist dieser Aufbau auch in den meisten eingebetteten Steuergeräten zu finden. Beispiel: ein Steuergerät im Audi quattro, welches zwei separate Prozessoren für Zündung und Ladung enthält, und ein seriell einstellbares Drehzahlsteuergerät für Elektromotoren. Unterschiede zur „normalen“ Rechnerarchitektur: Ein- und Ausgabegeräte sind Sensoren und Aktuatoren Wandlung analoger in digitale Signale und umgekehrt auf dem Chip (A/D D/A) Der Speicher ist oft nichtflüchtig und manchmal mit dem Prozessor integriert Meist wird nur wenig Datenspeicher benötigt, der Programmspeicher wird nur bei der Produktion oder Wartung neu beschrieben ( andere Speicherkonzepte) Hohe Stückzahlen verursachen Ressourcenprobleme (Speicherplatz) Oft komplizierte Berechnungen mit reellen Zahlen (DSP, digitale Signalprozessoren), spezialisierte ALUs für bestimmte numerische Algorithmen. Speicher Speicher dienen zur temporären oder permanenten Aufbewahrung von (binär codierten) Daten. Sie können nach verschiedenen Kriterien klassifiziert werden Permanenz: flüchtig – nichtflüchtig (bei Ausfall der elektrischen Spannung) o Halbleiterspeicher sind meist flüchtig, magnetische / optische Speichermedien nicht. o So genannte Flash Speicher sind nichtflüchtige Halbleitermedien, die elektrisch beschrieben und gelöscht werden können. Bei Flash-Speichern ist nicht jedes einzelne Bit adressierbar, die Zugriffe erfolgen auf Sektorebene ähnlich wie bei Festplatten. Vorteil: keine mechanisch bewegten Teile. Geschwindigkeit: Zugriffszeit (Taktzyklen oder ns) pro Wort (mittlere, maximale) o Gängige Zugriffszeiten liegen bei 1-2 Taktzyklen für Register in der ALU, 250 ns für einen schnellen Cache, 100-300 für einen Hauptspeicherzugriff, 1050ms für einen Plattenzugriff, 90 ms für eine CD-ROM, Sekundenbereich für Floppy Disk, Minutenbereich für Magnetbänder Preis: Cent pro Byte o Ein 512 MB Speicherbaustein oder CompactFlash kostet 100 Euro (2*10-5 c/B=20c/MB), eine 120GB Festplatte etwa genauso viel (8.3*10-8 c/B =83c/GB), ein 700MB CD-Rohling 25 cent (3.5*10-8c/B=35c/GB) Größe: gemessen in cm2 oder cm3 pro Bit o Papier: 6000 Zeichen/630cm²=10B/cm²; Floppy: 1.44MB/ 80cm2=18 KB/cm2; Festplatte: 10-20 GB/cm2, physikalisch machbar 1000GB/cm2 m o für mechanisch bewegte Speicher muss der Platz für Motor und Bewegungsraum mit berücksichtigt werden. Wie man sieht, sind Geschwindigkeit und Preis umgekehrt proportional. Üblicherweise wird der verfügbare Speicherplatz daher hierarchisch strukturiert. Das Vorhalten von Teilen einer niedrigeren Hierarchieebene in einer höheren nennt man Caching. Cache („Geheimlager“): kleiner, schneller Vordergrundspeicher, der Teile der Daten des großen, langsamen Hintergrundspeichers abbildet („spiegelt“). Konzept: Hauptspeicher = Folge von Tupeln (Adresse, Inhalt) Cache-Speicher = Folge von Quadrupeln Cache-Zeilen: (Index, Statusbit, Adresse, Inhalt) Index: Adresse im Cache Statusbits: modifiziert?, gültig?, exklusiv?, … Adresse: Speicherzelle die gespiegelt wird Falls die CPU ein Datum dat einer bestimmten Adresse adr benötigt, wird zunächst geprüft, ob es im Cache ist (d.h. (idx, sbt, adr, dat) im Cache). Zwei Fälle: Cache Hit: D.h. (idx, sbt, adr, dat) im Cache gespiegelt: dat wird Cache Miss: kein idx mit (idx, adr, …) im Cache. Die Speicherzelle adr mit Inhalt dat muss aus dem Hauptspeicher nachgeladen werden; ggf. muss dafür ein bereits belegter Platz im Cache geräumt werden (Verdrängungsstrategie) SPEICHER 3786: 3787: 3788: 3789: 3790: 3791: 3792: 3793: … 17 "c" 3.1415 3786 "x" 123456 3790 NIL … CACHE c1: 0 c2: 1 c3: 0 1 1 1 0 0 0 3787 3788 3792 "c" 3.1415 3790 c1: 0 c2: 1 c3: 0 1 1 1 0 0 0 3790 3788 3792 "x" 3.1415 3790 Falls jede Hintergrundadresse prinzipiell in jede Cache-Zelle geladen werden kann, ist der Cache assoziativ (fully associative). Vorteil: Flexibilität. Nachteil: gesamter Cache muss durchsucht werden, ob Hit oder Miss. Falls eine Cache-Zelle nur bestimmte Hintergrundadressen abbilden kann, sagen wir der Cache ist direkt abgebildet (direct mapped) Beispiel: C0 für H00, H10, H20, …, C1 für H01, H11, H21, …Suche H63 nur in C3! Vorteil: schnelle Bestimmung ob Hit oder Miss; Nachteil: Unflexibilität Hitrate ist entscheidend für Leistungssteigerung durch Cache; Verdrängungsstrategie beeinflusst Hitrate entscheidend LRU: Least recently used FIFO: First-In, First-Out LFU: Least frequently used Cache Coherency Problem: Mehrere Prozesse greifen auf Speicher zu, jeder Prozess hat einen eigenen Cache: Wie wird die Konsistenz sichergestellt? Kapitel 4: Programmiersprachen und –umgebungen Zur Wiederholung: Informatik ist die Wissenschaft von der automatischen Verarbeitung von Informationen; ein Algorithmus ist ein präzises, schrittweises und endliches Verfahren zur Verarbeitung (Umformung) von Informationen, und ein Programm ist die Repräsentation eines Algorithmus zur Ausführung auf einer Maschine Während v. Neumann noch der Ansicht war, dass „Rechenzeit zu wertvoll für niedere Aufgaben wie Übersetzung“ ist, sind heute dagegen die Personalkosten der Hauptfaktor bei den Softwarekosten. Darüber hinaus ist die Codierung in Maschinensprache oder Assembler sehr fehleranfällig (wie jeder, der die Haufgaben bearbeitet hat, wohl gemerkt haben dürfte). Daher sucht man nach problemorientierten statt maschinenorientierten Beschreibungsformen. Die Verständlichkeit eines Programms ist oftmals wichtiger als die optimale Effizienz. Vorteile höherer Programmiersprachen zeigen sich bei der Erstellung von Programmen: o schnellere Programmerstellung o sichere Programmierung. bei der Wartung von Programmen: o bessere Lesbarkeit o besseres Verständnis der Algorithmen. wenn Programme wiederverwendet werden sollen: o Verfügbarkeit auf vielen unterschiedlichen Rechnern; vom Zielrechner unabhängige Entwicklungsrechner. 4.1 Programmierparadigmen Unter einem Programmierparadigma versteht man ein Sprachkonzept, welches als Muster prägend für die Programme einer bestimmten Gruppe oder Sprache ist. Gängige Programmierparadigmen sind o Funktional / applikativ o Imperativ o Objektorientiert o Logikbasiert o Deklarativ o Visuell / datenflussorientiert o funktionale oder applikative Sprachen betrachten ein Programm als mathematische Funktion f, die eine Eingabe I in eine Ausgabe O überführt: O = f(I). Variablen sind Platzhalter im Sinne der Mathematik (Parameter). Ausführung = Berechnung des Wertes mittels Termersetzung. Beispiele: Lisp, SML, Haskell, ... SML: fun fak (n) = if n < 1 then 1 else n * fak (n-1) LISP: (defun fak (n) (cond (le n 1) 1 (* n (fak (– n 1))))) o imperative Sprachen unterstützen das ablauforientierte Programmieren. Werte werden sog. Variablen (= Speicherplätzen) zugewiesen. Diese können ihren Zustand ändern, d.h. im Laufe der Zeit verschiedene Werte annehmen (vgl. von-Neumann-Konzept). Ausführung = Folge von Variablenzuweisungen. Beispiele: Algol, C, Delphi, ... // Fakultätsfunktion in C: #include <stdio.h> int fakultaet(int n) { int i, fak = 1; for ( i=2; i<=n; i++ ) fak *= i; return fak; } int main(void) { int m, n; for ( n=1; n<=17; n++ ) printf("n = %2d n! = %10d\n", n, fakultaet(n)); } o objektorientierte Sprachen sind imperativ, legen aber ein anderes, in Objekten strukturiertes Speichermodell zugrunde. Objekte können eigene lokale Daten und Methoden speichern, voneinander erben und miteinander kommunizieren. Ausführung = Interaktion von Agenten. Beispiele: SmallTalk, C++, Java //Java Polymorphie-Beispiel import java.util.Vector; abstract class Figur { abstract double getFlaeche(); } class Rechteck extends Figur { private double a, b; public Rechteck ( double a, double b ) { this.a = a; this.b = b; } public double getFlaeche() {return a * b;} } class Kreis extends Figur { private double r; public Kreis( double r ) { this.r = r; } public double getFlaeche() {return Math.PI * r * r; } } public static void main( String[] args ) { Rechteck re1 = new Rechteck( 3, 4 ); Figur kr2 = new Kreis( 8 ); … o logikbasierte Sprachen betrachten ein Programm als (mathematisch-) logischen Term t, dessen Auflösung (im Erfolgsfall) zu gegebenen Eingabewerten I passende Ausgabewerte O liefert: t(I,O) true. Ausführung = Lösen eines logischen Problems. Beispiel: Prolog fak(0,1). fak(N,X):- N > 0, M is N - 1, fak(M,Y), X is N * Y. mutter (eva, maria). mutter (maria, uta). grossmutter(X,Y) :- mutter(X,Z), mutter(Z,Y). ?- grossmutter (eva, uta) yes. ?- grossmutter (uta, eva) no. o deklarative Sprachen betrachten ein Programm als eine Menge von Datendefinitionen (Deklarationen) und darauf bezogenen Abfragen. Ausführung = Suche in einem Datenbestand. Beispiel: SQL SELECT A_NR, A_PREIS As Netto, 0.16 As MwSt, A_PREIS * 1.16 As Brutto, FROM ARTIKEL WHERE A_PREIS <= 100 ORDER BY A_PREIS DESC o datenflussorientierte Sprachen stellen ein Programm dar, indem sie den Fluss der Signale oder Datenströme durch Operatoren (Addierer, Integratoren) beschreiben Ausführung = Transformation von Datenströmen Beispiel: Simulink, Microsoft Visual Programming Language (VPL) 4.2 Historie und Klassifikation von Programmiersprachen Programmiersprachen lassen sich klassifizieren nach Anwendungsgebiet: Ingenieurwissenschaften (Fortran), kommerzieller Bereich (Cobol), künstliche Intelligenz (Prolog, Lisp), Systemsoftware (Assembler, C), Steuerungssoftware (C, Ada, Simulink), Robotik (C, NQC, VPL), Internet / Arbeitsplatzsoftware (Java, C++), Datenbanken (SQL), … Verbreitungsgrad: (derzeit )industrierelevante, überlieferte und akademische Sprachen Historischer Entwicklung: „Stammbaum“ der Programmiersprachen, siehe oben Programmiersprachengeneration: Abstraktionsgrad; o 1. Generation: Maschinensprachen o 2. Generation: Assemblersprachen (x86-MASM, ASEM-51) o 3. Generation: Algorithmische Sprachen (Algol, Delphi, …) o 4. Generation: Anwendungsnahe Sprachen (VBA, SQL, …) o 5. Generation: Problemorientierte Sprachen, KI-Sprachen (Prolog, Haskell, …) Programmierparadigmen (siehe Kapitel 4.1) Verfügbaren Sprachelementen o Kontrollfluss, Datentypen o Rekursion oder iterative Konstrukte o Sequentielle oder parallele Ausführung o Interpretiert oder compiliert o Realzeitfähig oder nicht realzeitfähig o Streng typisiert, schwach typisiert oder untypisiert o Textuell oder graphisch o … Die „babylonische Sprachvielfalt“ der verschiedenen Programmiersprachenverursacht Kosten: Portabilitätsprobleme, Schulungsmaßnahmen, Programmierfehler. Daher gab es immer wieder Versuche, Sprachen zu vereinheitlichen (ADA, ANSI-C, …). Trotzdem kam es in der Forschung immer wieder zu neuen Ideen, neuen Konzepten und in der Folge zu neuen Programmiersprachen. Daher wird es auf absehbare Zeit viele verschiedene Sprachen und Dialekte geben. Für Informatiker ist es deshalb wichtig, die verschiedenen Konzepte zu kennen und sich schnell in neue Sprachen einarbeiten zu können. 4.3 Java Java wurde seit 1995 bei der Firma Sun Microsystems entwickelt. Ursprünglich war die Sprache zur Implementierung von sicheren, plattform-unabhängigen Anwendungen gedacht, die über das Internet verbreitet und bezogen werden können. Ein wichtiges Konzept ist das sog. Applet, eine Mini-Anwendung, die innerhalb einer Web-Seite läuft. Inzwischen sind praktisch alle Internet-Browser „Java-fähig“ und können Applets ausführen. Künftige Anwendungen von Java liegen im Bereich der kommunizierenden Geräte („Internet der Dinge“). Vorzüge von Java sind: o Java ist eine mächtige Programmiersprache, die die Sprachkonzepte herkömmlicher Programmiersprachen wie C oder Pascal mit OO-Konzepten und Konzepten zur Parallelverarbeitung und Netz-Verteilung verbindet. o Java wurde auf der Basis bekannter Sprachen zur System-, zur modularen und OOProgrammierung entwickelt (C, C++, Modula, Smalltalk) o Die Struktur der Sprache ist einfach, aber weniger systematisch als die von Pascal, Modula oder Oberon. o Java hat eine umfangreiche Klassenbibliothek. o Java gilt als robuste Sprache, die viele Fehler vermeiden hilft, z.B. aufgrund ihres Typkonzepts und der automatischen Speicher-bereinigung. o Java-Compiler sind schnell und produzieren effizienten Code. o Im Kern ist Java eine Programmiersprache mit imperativen und rekursiven Konzepten ähnlich wie Pascal oder C. So weist Java die meisten aus diesen Sprachen bekannten Konzepte wie Variablen, Zuweisungen, Datentypen, Ausdrücke, Anweisungen etc. auf - mit einer keineswegs verbesserten Syntax • Viele syntaktische und strukturelle Mängel und Ungereimtheiten sind nur mit der C / C++ Historie zu erklären Verdienste von Java liegen in der Zielsetzung, eine möglichst schlanke Sprache zu schaffen, in dem Ansatz, das Klassenkonzept ins Zentrum der Sprache zu stellen (Java ist eine wirklich “objektorientierte” Sprache) in der Offenheit und uneingeschränkten Portabilität, die Java z. Zt. zum bestgeeigneten Werkzeug für das Programmieren von Netz-Anwendungen macht Java-Historie ab 1991 Bei der Firma Sun wird von J. Gosling und Kollegen auf der Basis von C++ eine Sprache namens Oak (Object Application Kernel) für den Einsatz im Bereich der Haushalts- und Konsumelektronik entwickelt. Ziele: Plattform-Unabhängigkeit, Erweiterbarkeit der Systeme und Austauschbarkeit von Komponenten. 1993 Oak wird im Green-Projekt zum Experimentieren mit graphischen BenutzerSchnittstellen eingesetzt und später (wegen rechtlicher Probleme) in Java umbenannt. Zu diesem Namen wurden die Entwickler beim Kaffeetrinken inspiriert. 1994 Das WWW beginnt sich durchzusetzen. Java wird wegen der Applet-Technologie „die Sprache des Internets“ seit 1995 Sun bietet sein Java-Programmiersystem JDK (Java Development Kit) mit einem Java-Compiler und Interpreter kostenlos an. ab 1996 Unter dem Namen JavaBeans wird eine Komponenten-Architektur für JavaProgramme entwickelt und vertrieben. ab 2001 Eclipse-Projekt: integrierte Entwicklungsumgebung für Java und (darauf aufbauend) andere Sprachen und Systeme. 4.4 Programmierumgebungen am Beispiel Eclipse Siehe Vorlesung am 10.12.2007 (J. Hänsel), Skript wird noch nachgetragen. Kapitel 5: Applikative Programmierung 5.1 einfache, geschachtelte Rekursion, Terminierung – entfällt -– 5.2 rekursive Datenstrukturen – entfällt -– 5.3 Methoden und Parameterübergabemechanismen – entfällt -– Kapitel 6: Konzepte imperativer Sprachen Imperative Sprachen waren historisch die ersten „höheren Programmiersprachen“ (3. Generation). Da Paradigma imperativer Sprachen (Programmieren als strukturierte Folge von Speicheränderungen) entspricht dem Konzept der von-Neumann-Architektur. 6.1 Variablen, Datentypen, Ausdrücke Betrachten wir als Beispiel zwei Java-Implementierungen der Fakultätsfunktion: static int fakRek(int x) { return (x<=0) ? 1 : x * fakRek(x-1); } static int fakIt(int x) { int result = 1; while (x>0) { result *= x--; }; return result; } Typische Sprachelemente imperativer Sprachen, die hier auftreten, sind Methodendeklarationen und Methodenaufrufe, Parameter und lokale Variablen, Terme mit arithmetischen und logischen Operatoren, Zuweisungen und Dekremente, bedingte Terme bzw. Fallunterscheidungen, und Wiederholungsanweisungen. Die einzelnen Sprachelemente werden nachfolgend erläutert. Für weitere Informationen verweisen wir auf den „MiniJavakurs“ unter http://java.sun.com/docs/books/tutorial/java/nutsandbolts/index.html. Variablen haben in typisierten Sprachen wie Java immer einen festgelegten Datentyp. Jede Variable entspricht einer Speicherzelle. Dies gilt natürlich nur, falls die Variable einen Typ hat, der durch ein Speicherwort repräsentiert werden kann; eine Variable eines komplexen Typs entspricht einer Gruppe von Speicherzellen. Es gibt mehrere Arten von Variablen: Methoden-Parameter, lokale Variablen, Instanzvariablen und Klassenvariablen. Methoden haben Parameter (Ein/Ausgabevariablen) Methoden können lokale Variablen haben Jedes Objekt hat Instanzvariablen (nicht statisch) Klassen können Klassenvariablen besitzen (statisch) Instanzvariablen und Klassenvariablen heißen auch die Datenfelder der Klasse. Betrachten wir ein (willkürliches) Beispiel: public class Main { static double wechselkurs = 1.32; int betrag = 7; static int zehnCentStücke(double p) { int cent = (int)((p-(int)(p)) * 100); return((cent==20) ? 1 : 0) ;} public static void main(String[] args) { double p = 2.70; System.out.print("p= "); System.out.println(p); } } Hier ist p in main eine lokale Variable, p in zehnCentStücke ein Parameter, betrag eine Instanzvariable und .wechselkurs eine Klassenvariable. Java kennt folgende primitive Datentypen: • Zahlen: int, byte, short, long, float, double (43.8F, 2.4e5) • Zeichen: char (z.B. `x´) Sonderzeichen `\n´, `\”´, `\\´, `\uFFF0´ • Zeichenreihen (Strings): “Infor” + “matik” • Wahrheitswerte: boolean (true, false) • „Leerer Typ“ void Der leere Typ wird aus formalen Gründen gebraucht, wenn z.B. eine Methode kein Ergebnis liefert. Hier sind einige Beispiele von Datendeklarationen: boolean result = true; char capitalC = 'C'; byte b = 100; short s = 10000; int i = 100000; int decVal = 26; int octVal = 032; int hexVal = 0x1a; double d1 = 123.4; double d2 = 1.234e2; float f1 = 123.4f; String s = „ABCDE“; Benötigt man Variablen für mehrere gleichförmige Daten (d.h. viele Daten mit gleichem Typ), so kann man eine Reihung (Array) deklarieren (*). Die Reihung könnte etwa wie im folgenden Beispielprogramm verwendet werden. class ArrayDemo { public static void main(String[] args) { int[] anArray; // eine Reihung ganzer Zahlen anArray = new int[10]; // Speicherallokation anArray[8] = 500; System.out.println("Element at index 8: " + anArray[8]); } } Der Rumpf einer Java-Methode besteht aus einem Block, d.h. einer Folge von Anweisungen (statements). Bei der Ausführung werden diese der Reihe nach abgearbeitet. Die wichtigste Art von Anweisungen sind Ausdrücke (expressions). Bei ihrer Auswertung liefern sie einen Wert ab und können per Nebeneffekt den Inhalt von Variablen verändern. Hier ist ein Ausschnitt aus der Syntax von Java in BNF, in der Ausdrücke definiert werden (vgl. *, Kap. 18): • • • • • • • Expression::=Expr1 [AssignmentOperator Expr1] Expr1::=Expr2 [? Expression : Expr1] Expr2 ::=Expr3 {Infixop Expr3} Infixop::= + | * | < | == | != | || | && | … Expr3::= PrefixOp Expr3 | ( Expr | Type ) Expr3 | Primary {Selector} {PostfixOp} Prefixop::= ++ | -- | ! | ~ |+ | Primary::= ( Expression ) | Literal | new Creator | Identifier { . Identifier }[ IdentifierSuffix] | … Beispielweise könnten Ausdrücke etwa wie folgt aussehen: 17 + 4 x = x + 1 b = b != !b a += (a = 3) v[i] != other.v[i] cube[17][x+3][++y] "Länge = " + ia.length + "," + ia[0].length + " Meter" Achtung! Java verlangt, dass jede lokale Variable vor ihrer Verwendung initialisiert wurde, und zwar so, dass es statisch erkennbar ist. Diese Bedingung wird vom Compiler mit einer so genannten statischen Analyse überprüft und in Eclipse ggf. bereits beim Tippen des Programmtextes als Fehler markiert. Allerdings kann die statische Analyse nur bedingt erfolgreich sein. Im folgenden Beispiel sind zwar alle Variablen immer korrekt initialisiert, dies wird aber nur im ersten Fall auch erkannt! Eine gute Faustregel ist es, alle Variablen gleich bei der Deklaration zu initialisieren. public class Analyse { public int korrekt() {int x; if (17+4==21){x=1;}; return x; } public int inkorrekt() {int x; int i=5; if (i==5){x=1;}; return x; } } In Java gibt es folgende Operatoren (vergleiche die oben angegebene Syntax): Arithmetische Operatoren: +, -, *, /, %, ++ und –– / und % sind (ganzzahlige) Division und Rest ++ und –– als Post- und Präfixoperatoren; a++ erhöht a um 1 und ergibt a, während ++a den Wert a+1 liefert + dient auch zur Konkatenation von Zeichenreihen! Relationale Operatoren: ==, !=, <, <=, > und >= • • • • • Achtung! == und != vergleichen bei Objekten nur Referenzen! Logische Operatoren: !, &&, ||, &, | und ^ a || b wertet b nur aus falls a nicht gilt Zuweisungsoperatoren = , +=, -=, *=, /=, %, &=, |=, ^=, <<=, >>= und >>>= Fallunterscheidung (dreistellig) x? y: z Bitoperatoren ~, |, &, ^, >>, >>> und << können auch auf int angewendet werden (Bit-Repräsentation) Operatoren new und instanceof a instanceof b gibt an, ob Objekt a eine Instanz der Klasse b oder einer ihrer Unterklassen ist Operatoren für Member- und Array-Zugriff MeineKlasse.meinDatenfeld meinArray[7] Hier sind noch zwei Anmerkungen für Spezialisten: public class Demo { static public int sampleMethod() { int i = 0; boolean b = ++i==i++ | ++i==i++; return i; } static public int sampleMethod2() { int i = 0; boolean b = ++i==i++ || ++i==i++; return i; } } Das Ergebnis von sampleMethod und sampleMethod2 unterscheidet sich, da im zweiten Fall der hintere Teil der Disjunktion nicht ausgewertet wird. Merke: Dies ist kein Beispiel für gute Programmierung! Man sagt, dass ein Ausdruck einen Nebeneffekt (oder auch Seiteneffekt) hat, wenn er neben dem eigentlichen Ergebnis weitere Variablen verändert. Ausdrücke mit Nebeneffekten sind schwer verständlich und sollten deshalb möglichst wenig verwendet werden! public class Rechtsshift { static int i = -64; static int j = i>>2; static int k = i>>>2; } In diesem Beispiel hat i den Wert -64, j den Wert -16 und k den Wert 1073741808. Merke: Bit-Shifts sollten nur mit Vorsicht verwendet werden! Wie bereits oben erwähnt, ist Java eine streng typisierte Sprache. Das bedeutet, Alle Operatoren in Java sind strikt typisiert, d.h., es wird geprüft, ob der Typ dem verlangten entspricht. Z.B. kann << nicht auf float oder String angewendet werden. Zur Typumwandlung gibt es entsprechende Casting-Operatoren; z.B. (int)3.14 // ergibt 3 (double)3.14f // Ausweitung Dabei ist zu beachten, dass bestimmte Typen inkompatibel sind (z.B. boolean und int) Ausdrücke werden gemäß den „üblichen“ Operator-Präzedenz-Regeln ausgewertet. Regeln für die Auswertungsreihenfolge sind: linker vor rechter Operator, dann Operation gemäß Klammerung gemäß Operator-Hierarchie Achtung: Die Reihenfolge der Auswertung von Ausdrücken kann das Ergebnis beeinflussen! Beispiel: int i=2, j = (i=3)*i; // ergibt 9 int i=2, j = i*(i=3); // ergibt 6 Überraschenderweise ist die Multiplikation also nicht kommutativ! Generell ist zu sagen, dass die Verwendung von Nebeneffekten schlechter Programmierstil ist! 6.2 Anweisungen und Kontrollstrukturen In Java gibt es folgende Arten von Anweisungen: • Leere Anweisung ; • Block, d.h. eine mit {…} geklammerte Folge von Anweisungen; Blöcke dürfen auch geschachtelt werden • Lokale Variablendeklaration • Ausdrucksanweisung Zuweisung Inkrement und Dekrement Methodenaufruf Instanzerzeugung Zur Steuerung des Ablaufs werden Fallunterscheidungen verwendet. • if-Anweisung if (ausdruck) anweisung; [else anweisung;] üblicherweise sind die Anweisungen Blöcke. hängendes else gehört zum innersten if • switch-Anweisung switch (ausdruck) { {case constant : anweisung} [default: anweisung] Bei der Auswertung wird zunächst der Ausdruck ausgewertet und dann in den entsprechenden Zweig verzweigt. Switch-Anweisungen sind tief geschachtelten ifAnweisungen vorzuziehen, da sie normalerweise übersichtlicher sind. Kontrollflusselemente • while- und do-Schleife while (ausdruck) anweisung; do anweisung; while (ausdruck); • for-Schleife for (init; test; update) anweisung; init darf mehrere Anweisungen (mit Komma getrennt!) enthalten, auch gar keine for (int i=0; i<10; i++){ … i…;}; • break, continue und return continue verlässt den Schleifendurchlauf break verlässt die Schleife komplett 6.3 Sichtbarkeit und Lebensdauer von Variablen Unter der Sichtbarkeit (Skopus) einer Variablen versteht man den Bereich im Quellcode, in dem dieser Variablenname verwendet werden darf. Der Gültigkeitsbereich ist derjenige Teil der Sichtbarkeit, in der man auf den Inhalt der Variablen zugreifen kann. Die Lebensdauer ist die Zeitspanne, während der für die Variable Speicherplatz reserviert werden muss Für jede der verschiedenen Variablenarten gibt es detaillierte Festlegungen hierzu. Beispiel: public class xyz { int x; public xyz() { x = 1; } public int sampleMethod(int y) { int z = 2; return x + y + z; } } In diesem Beispiel bezieht sich x innerhalb von xyz und in sampleMethod auf die Instanzvariable, y in sampleMethod auf den Parameter, und z auf die lokale Variable. Variablen können sich auch gegenseitig verschatten: Unter Verschattung versteht man die Einschränkung des Gültigkeitsbereichs einer Variablen durch eine andere Variable gleichen Namens. Es gelten folgende Regeln: • Lokale Variablen werden in einem Block (einer Folge von Anweisungen) deklariert und sind nur für diesen verwendbar. • Lokale Variable sind ab ihrer Deklaration bis zum Ende des Blocks oder der Methode sichtbar. • Instanzvariable können von gleichnamigen lokalen Variablen verschattet werden, d.h., sie sind sichtbar aber nicht gültig. Verschattung ist hilfreich, um das Lokalitätsprinzip zu unterstützen (z.B. in Laufanweisungen). In Java ist es nicht erlaubt, dass lokale Variablen sich gegenseitig oder Methodenparameter verschatten Beispiel für Verschattung: public class xyz { int x; public xyz() { x = 1; } public int sampleMethod(int y) { int x = 2; return x + y; } }