Programmieren mit ELAN [1. Aufl.] 978-3-519-02507-8;978-3-322-96681-0

405 35 12MB

German Pages 208 [207] Year 1983

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Programmieren mit ELAN [1. Aufl.]
 978-3-519-02507-8;978-3-322-96681-0

Table of contents :
Front Matter ....Pages 1-5
Front Matter ....Pages 7-7
Der erste Algorithmus — schriftliches Addieren (Leo H. Klingen, Jochen Liedtke)....Pages 8-14
Das erste vollständige Programm — Palindromprüfung (Leo H. Klingen, Jochen Liedtke)....Pages 15-25
Zwischenspiel: ELAN-Grundwortschatz (Leo H. Klingen, Jochen Liedtke)....Pages 26-30
Ein einfaches Standardverfahren — Maximumsuche (Leo H. Klingen, Jochen Liedtke)....Pages 31-34
Ein komplizierterer Algorithmus — Strecke zeichnen (Leo H. Klingen, Jochen Liedtke)....Pages 35-40
Numerisches Rechnen — Wurzel (Leo H. Klingen, Jochen Liedtke)....Pages 41-50
Zwei Übungen — Primzahlen (Leo H. Klingen, Jochen Liedtke)....Pages 51-59
Programmentwicklung unter der Lupe — Filter (Leo H. Klingen, Jochen Liedtke)....Pages 59-67
Front Matter ....Pages 68-68
Prozeduren als Gliederungsmittel — Kioskzentrale (Leo H. Klingen, Jochen Liedtke)....Pages 69-77
Prozeduren mit Parametern — nochmals Kioskzentrale (Leo H. Klingen, Jochen Liedtke)....Pages 78-85
Zwischenspiel: Das Prozedurkonzept (Leo H. Klingen, Jochen Liedtke)....Pages 85-92
Gierige Algorithmen — Bergsteigen (Leo H. Klingen, Jochen Liedtke)....Pages 93-99
Numerik für Mathematiker — Gaußelimination (Leo H. Klingen, Jochen Liedtke)....Pages 100-105
Rekursive Algorithmen — Zahlkonversion und Volkswirtschaft (Leo H. Klingen, Jochen Liedtke)....Pages 106-111
Suchen und Sortieren — binäre Suche und Quicksort (Leo H. Klingen, Jochen Liedtke)....Pages 112-122
Ein Verwaltungsproblem — Abiturzulassung (Leo H. Klingen, Jochen Liedtke)....Pages 123-130
Backtracking — Das Labyrinth zu Knossos (Leo H. Klingen, Jochen Liedtke)....Pages 131-140
Front Matter ....Pages 141-141
Modulares Programmieren mit Paketen — Korrekturhilfe (Leo H. Klingen, Jochen Liedtke)....Pages 142-151
Ein Standardverfahren zur schnellen Suche — Hashing (Leo H. Klingen, Jochen Liedtke)....Pages 152-159
Abstrakte Datentypen und Operatoren — Bruchrechnung (Leo H. Klingen, Jochen Liedtke)....Pages 160-166
Das abschließende Projekt — Polynome (Leo H. Klingen, Jochen Liedtke)....Pages 167-186
Weiterführende Aufgaben (Leo H. Klingen, Jochen Liedtke)....Pages 187-189
Back Matter ....Pages 190-208

Citation preview

MikroComputer-Praxis Herausgegeben von Dr. L. H. Klingen, Bonn, Prof. Dr. K. Menzel, Schwäbisch Gmünd und Prof. Dr. W. Stucky, Karlsruhe

Programmieren mit ELAN Von Dr. Leo H. Klingen, Bonn, und Jochen Liedtke, Bielefeld Mit zahlreichen Abbildungen, Beispielen und übungen

ifi

B. G. Teubner Stuttgart 1983

CIP-Kurztitelaufnahme der Deutschen Bibliothek Klingen, Leo H.: Programmieren mit ELAN I von Leo H. Klingen u. Jochen Liedtke. - Stuttgart : Teubner, 1983 (MikroComputer-Praxis) NE: Liedtke, Jochen: Das Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, besonders die der übersetzung, des Nachdrucks, der Bildentnahme, der Funksendung, der Wiedergabe auf photomechanischem oder ähnlichem Wege, der Speicherung und Auswertung in Datenverarbeitungsanlagen, bleiben, auch bei Verwertung von Teilen des Werkes, dem Verlag vorbehalten. Bei gewerblichen Zwecken dienender Vervielfältigung ist an den Verlag gemäß

§ 54 UrhG eine Vergütung zu zahlen, deren Höhe mit dem Verlag zu vereinbaren ist. ISBN 978-3-519-02507-8 ISBN 978-3-322-96681-0 (eBook) DOI 10.1007/978-3-322-96681-0

©

B. G. Teubner, Stuttgart 1983

Gesamtherstellung : Beltz Offsetdruck, Hemsbach/BergstraBe UmSchlaggestaltung: W. Koch, Sindelfingen

3

Vorwort

Programmieren zu lernen ist ein eingreifender Prozeß, bei welchem man sich eine systematische Betrachtung von Problemstellungen und Lösungswegen aneignen muß. Darin spielen das Erfinden und die Handhabung selbsterdachter Begriffe und Objekte eine zentrale !:tolle. Die in diesem Lernprozeß zur Verfügung stehende Programmiersprache kann dabei mehr oder weniger behilflich sein, je nachdem, wie sie es erleichtert oder erschwert, die erdachten Begriffe und Objekte auszudrücken. Die Anforderungen, die demzufolge an eine Schulsprache zu stellen sind, kann eigentlich nur eine Programmiersprache erfüllen, welche genau für die Ausbildungssituation entworfen wurde. ELAN entstand aus Unzufriedenheit mit vielen Programmiersprachen in didaktischer Hinsicht und Frustration zu einer Zeit (1974), zu der die wenigen zur Ausbildung tauglichen Sprachen nicht auf Kleinrechnern zur Verfügung standen. Jetzt, neun Jahre spä ter, ist es so weit, daß ELAN zumindest in Deutschland von vielen Didaktikern akzeptiert, auf schulgerechten Kleinrechnern implementiert und durch Lehrmaterial unterstützt ist. Daß es soweit gekommen ist, ist nicht zuletzt den beiden Autoren dieses Buches zu verdanken, Jochen Liedtke als Hauptimplementierer des meistverbreiteten ELAN-Compilers und des EUMEL-Systems, sowie OStD Dr. Klingen, der durch seine Argumente und Beispiele vielen Kollegen ELAN zugänglich gemacht hat. Ich hoffe von ganzem Herzen, daß diesem Buch noch viele folgen werden, und daß ELAN für viele junge Leute eine Hilfe zu einem sinnvollen Einstieg in die Informatik geben wird.

Prof. C.H.A. Koster KlJ Nijmegen (NL), im Januar 1983

4

Zur

Einführung

Programmieren, d.h. die Entwicklung von Algorithmen, am Beispiel zu zeigen, ist das zentrale Thema dieses Buches. Dank der Einfachheit und Mächtigkeit von ELAN *) nimmt die eigentliche Beschreibung der Programmiersprache nur einen geringen Teil ein und läßt ihrer Anwendung breiten Raum. Dabei wird versucht, nicht nur die Funktion der Algorithmen, sondern den ganzen Entwicklungsprozeß von der Aufgabenstellung bis zur Lösung vorzuführen. Das Buch bietet dem Anfänger eine Einführung in die Methoden des Programmierens, dem Fortgeschrittenen Anregungen zum Programmierstil und zur Algorithmenkonstruktion und dem interessierten Leser eine Möglichkeit, ELAN "in Aktion" kennenzulernen. Wir hoffen, daß die Entwicklung der Algorithmen auch den Lesern Freude bereitet, die keinen Zugang zu einem ELAN-Computer haben.

Bonn/Bielefeld, im Januar 1983

L. Klingen, J. Liedtke

Anmerkung: Der Text dieses Buches wurde mit Hilfe des EUMELSystems ediert und druckfertig aufbereitet.

ELAN wurde in den Jahren 1974-77 von C.H.A. Koster (damals TU Berlin) und seiner Arbeitsgruppe in Zusammenarbeit mit der GMD Bonn und dem HRZ der Universität Bielefeld entwickelt. Die ELAN-Sprachdefinition findet man im Literaturverzeichnis.

*)

5

Inhaltsverzeichnis

TEIL I Erste Schritte - ganz kleine Algorithmen 1. 2. 3. 4. 5. 6. 7. 8.

Der erste Algorithmus - schriftliches Addieren Das erste vollständige Programm - Palindromprüfung Zwischenspiel: ELAN-Grundwortschatz Ein einfaches Standardverfahren - Maximumsuche Ein komplizierterer Algorithmus - Strecke zeichnen Numerisches ltechnen - Wurzel Zwei Übungen - Primzahlen Programmentwicklung unter der Lupe - Filter

TEIL II Etwas grö&ere Algorithmen 9. Prozeduren als Gliederungsmittel - Kioskzentrale 10. Prozeduren mit Parametern - nochmals Kioskzentrale 11. Zwischenspiel: Das Prozedurkonzept 12. Gierige Algorithmen - Bergsteigen 13. Numerik für Mathematiker - GaUi.limination 14. ltekursive Algorithmen - Zahlenkonversion und Volkswirtschaft 15. Suchen und Sortieren - Binä re Suche und Quicksort 16. Ein Verwaltungs problem - Abiturzulassung 17. Backtracking - Das Labyrinth zu Knossos TEIL III Umfangreichere Vorhaben 18. 19. 20. 21. 22.

Modulares Programmieren mit Paketen - Korrekturhilfe Ein Standardverfahren zur schnellen Suche - Hashing Abstrakte Datentypen und Operatoren - Bruchrechnung Das abschließende Projekt - Polynome Weiterführende Aufgaben

7 8

15 26 31 35 41 51 59 68 69 78 85 93 100 106

112 123 131 141 142 152 160 167 187

Anhang A Systematischer Überblick Anhang B Paket "Schriftliches Addieren"

190 203

Literaturverzeichnis Sachregister

205 206

7

Te i I

Erste Schritte - ganz kleine Algorithmen

Informatik befaßt sich oft mit Problemen, die zu umfangreich sind, als daß ein Mensch sie in allen Aspekten überschauen könnte. Deshalb sind viele Programme so groß und komplex, daß selbst ihre Programmierer sie nicht vollkommen überblicken. Gute Programme sollten für andere verständlich sein (und damit auch für den Autor selbst). Sie sollten ausführlich, klar und möglichst elegant entwickelt werden. Selbstverständlich sollten sie korrekt sein, d.h. in allen nur möglichen Situationen gemäß ihrer Spezifikation arbeiten. Aber gerade das bereitet Schwierigkeiten. Wegen der großen Komplexitä t vieler Programme, der kaum überschaubaren Vielfalt möglicher Kombinationen, werden leicht Fehler bei der Entwicklung gemacht, die dann zu falschen Reaktionen dieser Programme führen - besonders bei seltenen, aber wichtigen Sonderfällen. Es leuchtet ein, daß sich solche Fehler bei sauber strukturierter, verständlicher und klarer Programmentwicklung weniger leicht einschleichen und im Ernstfall einfacher zu finden und zu korrigieren sind. Deshalb merke: Überhaupt Programme schreiben zu können, ist nicht lernenswert! Man muß vielmehr lernen,

~

(und richtige) Lösungen zu entwickeln!

Um das Programmieren zu lernen, müssen wir natürlich mit kleinen Problemen und einfachen Programmen beginnen. Ziel des Lernens soll sein, größere Aufgaben in Angriff nehmen und komplizierte Programme schreiben zu können. Daher müssen wir auch die einfachste Lösung so sorgfältig wie möglich entwickeln. Allerdings werden schon die ersten Beispiele den Anfänger "fordern" und ihm damit (hoffentlich) interessant erscheinen. Selbst der erfahrenere Programmierer wird nicht oft ganz ohne nachzudenken gute Lösungen finden.*)

*) Die Autoren sind sicher, daß sich fast alle in diesem Buch vorgeführten Beispiele noch verbessern lassen!

8

1. Der erste Algorithmus - schriftliches Addieren

Was ist ELAN? ELAN ist eine Programmiersprache, ein Werkzeug zum Entwerfen, Notieren und Beschreiben von Programmen. Ebenso wie andere Kunstsprachen unterscheiden sich Programmiersprachen in einigen Punkten von natürlichen Sprachen: - Die Grammatik und die Bedeutung der Sprachelemente ist präzise definiert. - Das Vokabular ist begrenzt. ELAN besteht aus ungefähr 130 Wörtern und 30 Sonderzeichen. Die Vokabeln und die Grammatik einer Programmiersprache kann man meistens ziemlich einfach lernen. Ähnlich wie bei einer natürlichen Sprache kommt man in vielen (einfacheren) Fällen schon mit einer Teilmenge der gesamten Sprache aus. Das weit größere Problem ist die Anwendung der Programmiersprache. Sie selbst ist schließlich nur ein Werkzeug. Und wie bei jedem Werkzeug lernt man die sinnvolle Handhabung erst beim Gebrauch. Die eigentliche Schwierigkeit besteht darin, in zufriedenstellender Qualität zu programmieren.

Was ist "Programmieren"? In der Überschrift dieses Kapitels- tritt das Wort Algorithmus auf. Damit bezeichnen wir jede exakte (und endliche) Handlungsvorschrift. Ein Algorithmus beschreibt ein Verfahren (z.B. Division) derart, daß ein Prozessor (z.B. ein Grundschüler im Rechenunterricht) es durch strikte Befolgung der einzelnen Anweisungen durchführen kann. Eine solche Beschreibung verwendet üblicherweise wiederum andere Algorithmen als Bausteine (z.B. Multiplikation und Subtraktion). Unter "Programmieren" verstehen wir alle menschlichen Tä tigkeiten, die mit dem Entwurf, der Konstruktion, dem Ändern, Testen und Beweisen von Algorithmen zusammenhängen, die von Computern ausgeführt werden sollen. Dabei geht es im Unterschied zu vielen anderen Gebieten nicht um die direkte Lösung von Problemen, sondern allgemeiner darum, Strategien für die Lösungen aufzustellen.

9

Nehmen wir beispielsweise die Aufgabe: Addiere zwei mehrstellige (positive) ganze Zahlen mittels Stift und Papier! Die direkte Lösung haben wir alle auf der Grundschule gelemt. Beim Programmieren muß dagegen die verwendete Strategie präzise formuliert werden. Wir reagieren auf die Aufgabenstellung also nicht, indem wir selbst addieren, sondem indem wir eine Vorschrift entwerfen, die möglichst genau beschreibt, wie die Addition durchzuführen ist. Dieses "Programm" soll dann ein Computer ausführen können.

Algorithmus

Computer

Das Verfahren selbst haben wir schon oft beim schriftlichen Addieren ausgeführt, so daß wir es eigentlich recht gut kennen. Jetzt müssen wir es möglichst präzise formulieren. Ein erster Versuch: addiere einerstellen ; addiere zehnerstellen und eineruebertrag ; addiere hundertersteilen und zehneruebertrag addiere

Semikolon:

Das Semikolon (';') können wir als "und dann" lesen. Bei unseren Programmen werden sehr hä ufig Konstruktionen benutzt wie tue dies und dann tue jenes Dafür nehmen wir (wie oben) die etwas kürzere Schreibweise tue dies ; tue jenes Neben der Kürze (und damit Übersichtlichkeit) hat diese Notation den Vorteil, unmiß-

10 verständlicher zu sein: man sieht unabhängig von der Wortwahl, was zusammengehörige Einheiten sind, was zuerst und was danach getan werden soll. Solche Einheiten nennen wir Anweisungen. Eine wie oben gezeigte durch I;' getrennte Aufeinanderfolge von Anweisungen heißt auch Abschnitt. Die einzelnen Anweisungen eines Abschnitts sind nacheinander auszuführen - von der ersten bis zur letzten. Punkt:

Den Punkt ('.') verwenden wir, um das Ende eines Algorithmus zu kennzeichnen.

Zurück zu unserem ersten Algorithmus! Bei der Formulierung sind wir auf ein Problem gestoßen: Es ist nicht klar, was "addiere ••• " bedeuten soll. Wann soll das Verfahren abbrechen, d.h. wann ist die Addition fertig? Wir können diese Schwierigkeit umgehen, indem wir uns auf eine bestimmte Stellenzahl beschränken. Bei 3 Dezimalstellen ergibt sich: addiere einerstellen ; addiere zehnerstellen und eineruebertrag ; addiere hundertersteilen und zehneruebertrag nimm hunderteruebertrag als tausenderergebnis •

Hier sehen wir, daß der letzte übertrag zum Schluß noch gesondert zu berücksichtigen ist. Offensichtlich erfordern Anfang und Ende Sonderbehand.lungen, die mittleren Stellen gehen anscheinend immer nach der gleichen Methode. Da wir nun aber beliebig große Zahlen addieren wollen, müssen wir uns von den festen Grenzen lösen, denn unser Algorithmus soll erst bei der höchstwertigen Stelle abbrechen. Da die mittleren Stellen "im wesentlichen gleich" behandelt werden, formulieren wir etwas einfacher: addiere einerstellen ; addiere folgende stellen mit uebertraegen bis zur hoechstwertigen beruecksichtige letzten uebertrag •

Bis jetzt ist das nur ein Trick, die Schwierigkeiten bei der Beschreibung zu umgehen. Trotzdem wird er uns weiterhelfen. Wir haben bisher die verschiedensten überträge (Einer-, Zehner-,usw.) verwendet. Dabei reicht es eigentlich, von einem übertrag zu reden, der in jede Stellenaddition (als übertrag von der vorigen) hineingeht und neu (als übertrag dieser Addition) heraus-

11

kommt. Aus dem Rahmen fiel bisher die erste Stellenaddition, da sie noch keinen übertrag berücksichtigt. Diese Sonderstellung können wir auflösen, wenn wir auch dort einen übertrag - allerdings mit dem Wert 0 - hineinstecken: setze uebertrag auf null ; addiere alle stellen mit uebertrag beruecksichtige letzten uebertrag •

Unsere Schwierigkeiten stecken jetzt in "addiere alle stellen mit uebertrag". Insbesondere ist nicht klar, wie das geschehen soll. Deshalb wollen wir uns jetzt allein darum kümmern, diese Aktion weiter zu verfeinern. Wir haben uns vorher schon überlegt, bei der Einerstelle zu beginnen und bei der höchstwertigen aufzuhören. Dabei können wir jederzeit von einer "aktuellen", d.h. gerade betrachteten Stelle sprechen, so daß sich das Problem "alle" Stellen zu behandeln, durch eine Wiederholung lösen läßt: addiere alle stellen mit uebertrag nimm einerstelle als aktuelle stelle WHILE noch stellen zu addieren REPEAT addiere aktuelle stelle mit uebertrag nimm naechste als aktuelle stelle END REPEAT •

Hier sehen wir, daß für unsere Beschreibung noch mehr Verabredungen getroffen werden müssen als nur das Semikolon. Wir brauchen zur Notation e:ine eigene Sprache - in diesem Buch die Programmiersprache ELAN. Einerseits gibt sie uns ein Gerüst aus festgelegten Vokabeln und Sonderzeichen wie REPEAT

WHlLE

END

mit fester Bedeutung und dazugehörigen grammatischen Regeln, andererseits können wir beliebig viele neue Vokabeln einführen, z.B. addiere alle stellen mit uebertrag Solche Eigenschaften der Programmiersprache beschreiben wir in der gleichen Form wie beim Semikolon:

12

Schlüsselworte und Namen: In ELAN gibt es einige fest vorgebene Schlüsselworte mit fester Bedeutung (z.B. REPEAT). Diese werden alle mit Großbuchstaben geschrieben, Leerzeichen (Blanks) dürfen nicht enthalten sein. Wie wir gesehen haben, müssen wir auch neue Namen vergeben können. Diese Bezeichner a grenze

Um das Intervall zu verkleinern, wird man man als neue 'x grenze' einen Wert innerhalb

49 des alten Intervalls wählen, z.B. die Intervallmitte. Wir müssen allerdings sicher sein, daß die damit automatisch verbundene 'a grenze' auch im alten Intervall bleibt. Das ist glücklicherweise der Fall. Denn da x grenze

*a

grenze

=a

immer gilt, verschieben sich beide Grenzen korrespondierend. Wenn die eine wächst, sinkt die andere und umgekehrt. Wenn man von einem festen Intervall (x, a/x) ausgeht, können 'x grenze' und 'a grenze' ihre Rollen als Ober- und Untergrenze zwar vertauschen, sie verlassen das Intervall aber entweder beide nicht, oder beide zusammen. Also verkleinert sich das Intervall, wenn wir eine neue 'x grenze' aus dem alten Intervall wählen. Damit ergibt sich: beginne mit einem sicheren intervall REAL VAR

x grenze := 1.0 a grenze := a • verkleinere intervall x grenze := mitte a grenze := a / x grenze intervall kann noch sicher verkleinert werden IF x grenze ist untergrenze THEN x grenze< mitte AND mitte< a grenze ELSE a grenze < mitte AND mitte < x grenze FI

mitte: x grenze + (a grenze - x grenze) / 2.0 x grenze ist untergrenze : x grenze < a grenze Die Abbruchbedingung ist komplizierter geworden, da 'x grenze' sowohl Unter- als auch Obergrenze sein kann. Wenn unser Rechner eine gute Arithmetik hat, könnten wir auch folgendermaßen überprüfen, ob 'mitte' wirklich noch innerhalb des Intervalls liegt: intervall kann noch sicher verkleinert werden : mitte x grenze AND mitte a grenze • Diese einfachere Version dürfen wir aber

~

dann verwenden, wenn wir die Rechner-

arithmetik unserer Maschine sehr genau kennen und nachweisen, daß 'mitte' bei der vorliegenden Berechnungsmethode nie Werte außerhalb des Intervalls annimmt!

50

Wertliefernde IF-Auswahl: Wenn die IF-Auswahl einen Wert liefern soll, müssen alle Zweige ('then teil' und 'else teil') einen Wert liefern.

Testen wir unseren Algorithmus auf dem Rechner, sehen wir, daß er mit erheblich weniger Schritten - oft weniger als 10 - zum Ziel kommt. Die Mathematik kennt das Verfahren schon lange als die Heronsche Methode zur Wurzelberechnung. LälH man unseren Entwicldungsweg der Intervallschachtelung beiseite, kann man einen Schritt als x

neu

:=

12 *

(x

alt

+ -~--) x a1t

beschreiben, womit aber noch nichts über das Abbruchkriterium gesagt ist. übungen 16) Ergänzen Sie beide vorgestellten Quadratwurzelverfahren zu kompletten Programmen, indem Sie die notwendigen Ein- und Ausgabeteile hinzufügen. Lassen Sie alle Iterationsschritte mit ihren Zwischenergebnissen ausgeben und vergleichen Sie das Verhalten der beiden Algorithmen. 17) Entwickeln Sie einen Algorithmus zur Berechnung der dritten Wurzel! a) Verwenden Sie dazu die beim ersten Quadratwurzelprogramm verwandte Methode der IntervaIlhalbierung. b) Nehmen Sie den zweiten Quadratwurzelalgorithmus als Vorbild. c) Für Mathematiker: Versuchen Sie, das zweite Programm zu verbessern, indem Sie die neue 'x grenze' nach einer schneller korwergierenden Formel berechnen. 18) Berechnen Sie die Eulersche Zahl e = 2.718 ••• als Grenzwert der Reihe I I I 1 I

Öl

+

Tl

+

21

+

31

+

41

+ •••

Achten Sie dabei auf das Abbruchkriterium!

51

7. Zwei Übungen - Primzahlen

Die nächste Aufgabe, mit der wir uns befassen wollen, ist Finde die Primzahlen bis 1000. Unsere erste Idee ist ganz einfach. Wir testen bis 1000 alle Zahlen, die eine Primzahl sein könnten. Wenn wir eine gefunden haben, wird sie ausgegeben: nimm kleinste primzahl als ersten kandidaten WHILE noch kandidaten zu untersuchen REP drucke falls kandidat primzahl ist ; betrachte naechsten kandidaten ENDREP •

Überlegen wir uns zunächst die Kandidatenauswahl. Die einfachste Lösung: nimm kleinste primzahl als ersten kandidaten INT VAR prim kandidat :: 2 • noch kandidaten zu untersuchen prim kandidat 1000 • ist falsch, denn bei einem Wert

1000 würde sie zwar TRUE liefern, bricht aber mit

Programmfehler ab, weil im linken Teil der OR-Verknüpfung auf ein nicht mehr vorhandenes Listenelement zugegriffen wird. Da solche Situationen bei Feldern häufiger auftreten, gibt es die

CAND/COR-Operatoren: Sie entsprechen den booleschen Operatoren AND und Olt. Diese bedingten (C = Conditional) Fassungen werten den rechten Operanden aber nicht aus, wenn der linke schon allein das Resultat bestimmt: a CAND b : Falls a F ALSE ist, wird b nicht mehr ausgewertet, das Resultat ist FALSE (wie bei AND).

a COR b

Falls a TRUE ist, wird b nicht mehr ausgewertet, das Resultat ist TRUE (wie bei OR).

Damit können wir unser letztes Refinement jetzt richtig formulieren: naechste zahl gefunden oder liste erschoepft : aktuelle primzahl> 1000 GOR in liste [aktuelle primzahl] • Achtung: Die Reihenfolge der Operanden ist hier äußerst wichtig. Anders herum wäre es falsch!

Will man die Obergrenze 1000, bis zu der Primzahlen gefunden werden sollen, variieren, muß man das Programm an verschiedenen Stellen ändern. Da man bei der ROW-Größe aber nur Denoter angeben darf, kann man keine CONST oder VAR anstelle von 1000 benutzen. Man kann aber auch Denoter (jeden Typs) benennen.

58

LET-Vereinbarung: Form: LET Name = Denoter Mehrere Gleichsetzungen können wie bei der Deklaration durch Komma getrennt werden.

So wäre unser Programm besser: LET obergrenze = 1000 konstruiere •••

Überall sonst wird 1000 durch 'obergrenze' ersetzt. Diese Benennung bringt zwei Vorteile: a) Wir können die Obergrenze jetzt ganz einfach ändern. b) Der Name gibt dem Leser zusä tzliche Information über die Bedeutung von 1000.

Übungen 21) Schreiben Sie ein Programm, das Primzahlzwillinge findet. (Das sind Primzahlen mit dem Abstand 2, z.B. 11 und 13.) 22) Für die Tausender-Intervalle bis 10 000 soll eine Primzahlstatistik aufgestellt werden. 23) a) Kombinieren Sie das "Sieb des Erathostenes" mit der Idee, nur ungerade Zahlen zu untersuchen. b) Warum bringt eine Kombination mit der Wechselschrittidee nichts ein? 24) Prüfen Sie die Gleichverteilung der Quasizufallszahlen, die Sie durch 'random (1,20)' erhalten. Verwenden Sie ein ROW, um das Auftreten der einzelnen Zahlen zu zählen. 25) Natürliche Zahlen heißen vollkommen, wenn die Summe aller ihrer echten Teiler einschließlich der 1 wieder die Zahl selbst ergibt (Beispiel: 28 = 1+2+4+7+14). Entwickeln Sie einen Algorithmus, der alle vollkommenen Zahlen bis 10 000 findet. Gehen Sie dabei so sorgfältig argumentierend vor, daß Sie vor dem Test von der Korrektheit des Programms überzeugt sind! 26) Zwei Zahlen heißen befreundet, wenn die erste gleich der echten Teilersumme der zweiten ist und umgekehrt. Nutzen Sie das 'Sieb des Erathostenes' zu einer schnellen Ermittlung der Teilersummen und dadurch zu einer Suche nach befreundeten Zahlen! 27) Mit random(l,49) werden Lottotips erzeugt; in einem Feld von Boolvariablen wird notiert, ob eine Kugel gezogen worden ist oder nicht, weil im Gegensatz zum Würfeln hier alle Ausfälle innerhalb einer Ziehung verschieden sein müssen. Zugleich gibt das Boolfeld eine einfache Möglichkeit zur sortierten Ausgabe der Lottozahlen. Ermitteln Sie über eine Simulation von 100 Ziehungen die ungefähre prozentuale Häufigkeit für das Auftreten von Nachbarzahlen!

59 28) Felder mit Texten (Autokennzeichen) stellen Decks eines Parkhauses dar; über Zufallsgeneratoren werden An-und Abfahrt geregelt. Verweildauer, Gebühren und Tagesbilanz sollen ermittelt werden.

ß'

7

Y

/Y

,,%

/6"

17

.%

19

.?f'

25

;;ff

;7

.J%

29

,$

;JIf

35

~

37

,.)8"'

...w-

~

43

yt

y.f

y

47

ft"

49

$

yt

53

;X

55

~

..sr

~

59

ftY

)Z

)3'

yt

65

Y

2

3

~

11

)d

13

~

~

;d

23

ß

31

.)1

)!5

41

)Yf

.~ 61

5

ß

67

.fo" .JK

;pY

Das Sieb des Erathostenes nach Streichen der Vielfachen von 2 und 3

8. Programmentwickhmg Illlter der Lupe - Filter

Bei den bisherigen Programmentwicklungen haben wir vorgeführt, wie man einen Algorithmus entwickelt, ihn in Refinements zerlegt Illld von oben nach Illlten ("topdown") konstruiert. Dabei ergaben sich die Aufteilungen in Refinements und deren Namen meistens recht natürlich, allerdings nicht zwangsläufig. Jetzt ist die Zeit gekommen, solche Designentscheidungen genauer Illlter die Lupe zu nehmen. (Wir hoffen, der Leser hat die ersten Schwierigkeiten überwunden und schon etwas Gefühl für das Programmieren entwickelt.)

60 Als Beispiel für unsere Diskussion wählen wir die Aufgabe: Aus einer Folge von Elementen sind die zehn mit den größten Werten zu sammeln. Das Beispiel ist groß genug, um Ideen und Prinzipien aufzuzeigen, aber nicht so groß, daß es unübersichtlich wird. Einer einfacheren Version dieses Problems sind wir schon bei der Maximumsuche begegnet. Dort sind wir die Elemente einzeln durchgegangen und haben immer das 'vorläufig groesste' aufbewahrt. Um diese Idee auszubauen, können wir das Charakteristische des damaligen Vorgehens mit einem besseren Modell beschreiben: Wir greifen die Idee eines Filters auf und filtern die Elemente eines nach dem anderen durch. Wenn der Filter bei der einfachen Maximumsuche zum Schluß das größte Element ausgefiltert hatte, müßten wir unser jetziges Problem mit einem größeren Filter lösen können, der die zehn größten in seinen Zellen auffängt. Auf der höchsten Abstraktionsebene könnte der Algorithmus so aussehen: konstruiere einen filter und fuelle ihn mit den ersten elementen; WHILE noch elemente da REPEAT hole neues element ; filtere neues element durch ENDREP • Auf diesem Niveau sollten wir keine konkreten Objekte oder Operationen benutzen. Bei der Konstruktion des Algorithmus sind wir nämlich am Typ unserer Elemente überhaupt nicht interessiert. Auch die Struktur des Filters spielt noch keine Rolle. Wir haben mit einer ganz einfachen Idee angefangen, also sollte das Programm so einfach wie möglich aussehen, aber auch klar und präzise. Alle Details der Implementation haben hier noch nichts zu suchen. An die kann man denken, wenn eine ausreichend niedrige Ebene erreicht ist und die genauen Anforderungen an die l{ealisierung durch die bisherige Entwicklung klar geworden sind. Auf der nächsten Ebene müssen wir 'filtere neues element durch' spezifizieren; denn das ist die einzige komplexe Operation, die wir weiter zerlegen können, ohne an konkrete l{ealisierungen wie ROWs o.ä. zu denken. Die anderen sind auf dieser Ebene Primitiva. filtere neues element durch : IF neues element uebertrifft kleinstes filterelement THEN ersetze kleinstes filterelement durch neues FI •

61 Hier wollen wir etwas über die gewählte Namensgebung (Verbalisierung) nachdenken. Eine andere Möglichkeit wäre: IF neues element > kleinstes filterelement TREN kleinstes filterelement := neues element FI Zuerst sieht das besser aus, weil es leichter lesbar erscheint. Aber diese Notation macht zwei implizite Annahmen: - Der Vergleich zwischen dem neuen Element und dem kleinsten im Filter kann elementweise geschehen, d.h. ohne daß andere Teile des Filters betrachtet werden müssen. Die Ersetzung eines Filterelements kann ohne Änderung des ganzen Filterzustandes geschehen. Beide Voraussetzungen können aus der Entwicklung bisher nicht ohne weiteres abgeleitet werden, und beide würden die Implementationsmöglichkeiten einschränken, insbesondere die zweite. Deshalb geben wir bei dieser Topdown-Entwicklung der ersten Lösung den Vorzug. Nun haben wir ein Niveau erreicht, auf dem wir EntSCheidungen bezüglich der Struktur unseres Filters fällen müssen. Dieser muß erstens aus Zellen bestehen, die Elemente beinhalten können. Es gibt keinen Grund, irgendeine Reihenfolge der Filterzellen vorzuschreiben. (Der Filter muß also nicht unbedingt die Struktur einer Liste bzw. eines ROWs haben.) Jede Zelle muß durch eine Nummer eindeutig identifiziert werden, denn nur so können bestimmte Elemente einfach ersetzt werden. Zum logischen Objekt 'Filter' gehören zwei weitere Dinge. Zum einen braucht man einen Deskriptor, der anzeigt, welche Zellen des Filters schon gefüllt und welche noch leer sind. (Die Elementfolge könnte weniger als 10 Elemente umfassen.) Zum anderen müssen wir den Wert des kleinsten Elements im Filter wissen und es durch ein neues ersetzen können. Das zweite Attribut ist deshalb die Zelle des kleinsten Elements. Mit diesen Ideen können wir die nächsten Refinements formulieren. Dabei müssen wir allerdings genau darauf achten, nicht die Konsistenz des Filters zu zerstören, der aus drei Teilen besteht: - Filterinhalt - Füllstand - Zelle des kleinsten Filterelements

62

neues element uebertrifft kleinstes filterelement : neues element> filterinhalt [zelle des kleinsten elements] • ersetze kleinstes filterlement durch neues : filterinhalt [zelle des kleinsten elements] := neues element bestimme ort des kleinsten filterelements •

Eine andere Möglichkeit wäre: neues element uebertrifft kleinstes filterelement bestimme ort des kleinsten filterelements ; neues element > filterinhalt [zelle des kleinsten elements] • ersetze kleinstes filterelement durch neues : filterinhalt [zelle des kleinsten elements] := neues element

Diese Lösung ist jedoch erheblich schlechter als die erste. Dafür gibt es zwei Gründe. Der erste Grund folgt aus den gewählten Namen, der zweite aus Prinzipien der Abstraktion: -

In der ersten Lösung ändert nur das Refinement mit dem aktionsbehafteten Namen ('ersetze ••• ') den Zustand des Filters. Im zweiten Fall hat das Refinement mit dem Namen, der eine Eigenschaft und keine Aktion ausdrückt

er ••ueber-

trifft ••• '), einen wichtigen Seiteneffekt, nämlich die Aktion 'bestimme ort •••'. Also legen uns schon die Namen die erste Lösung als bessere nahe, denn dort stimmen Namen und tatsächliche Realisierung der Refinements besser überein. - Bei der ersten Lösung zerstört keins der Refinements die Konsistenz des gesamten Filters. Die durch die Ersetzung des kleinsten Elements entstandene Inkonsistenz in der Beschreibung des Filters wird im gleichen Refinement sofort wieder behoben, indem das kleinste Element neu bestimmt wird. Im Gegensatz dazu etabliert bei der zweiten Lösung das erste Refinement die Konsistenz, während das zweite sie wieder zerstört, so daß es sehr wichtig ist, sie immer in der richtigen Reihenfolge aufzurufen. Besonders bei spä teren Änderungen wird das leicht zu Fehlern führen. Bei der ersten Lösung haben wir also besser abstrahiert, die Refinements sind unabhängiger. Wir möchten betonen, daß hier Argumente, die von Prinzipien der Benennung, Abstraktion und Klarheit abgeleitet wurden, zur effizienteren Lösung geführt haben. In der ersten Fassung wird das kleinste Element nämlich nur dann neu bestimmt, wenn es wirklich nötig geworden ist. Man kann daraus allerdings nicht schließen, daß man durch sau-

63 bere Programmentwicklung automatisch zum effizientesten Algorithmus gelangt, in der Regel wird eine gute Strukturierung dafür aber hilfreich sein. Von den verbleibenden Refinements sind nur zwei noch etwas komplizierter. 'konstruiere einen filter und fuelle •••' muß die Konsistenz herstellen, was Inhalt, Füllstand und Zelle des kleinsten Elements angeht. Die Probleme bei der Entwicklung von 'bestimme zelle des kleinsten filterelements' ähneln denen von 'ersetze kleinstes filterelement durch neues': die Konsistenz des Filters muß gewahrt bleiben. Dabei nehmen wir an, daß die Suche des Minimums dem Leser nsch der schon vorher behandelten Maximumsuche keine Schwierigkeiten bereitet. Das ganze Programm sieht nach unseren Überlegungen mit TEXTen als Elementen (alles andere wäre ebenso möglich) folgendermaßen aus: leite zugriff auf die folge der elemente ein ; konstruiere einen filter und fuelle ihn mit den ersten elementen; WHILE noch elemente da REP hole neues element ; filtere neues element durch ENDREP ; liste ausgefilterte elemente auf filtere neues element durch : IF neues element uebertrifft kleinstes filterelement THEN ersetze kleinstes filterelement durch neues FI • neues element uebertrifft kleinstes filterelement : neues element> filterinhalt [zelle des kleinsten elements} • ersetze kleinstes filterelement durch neues filterinhalt [zelle des kleinsten elements} := neues element bestimme zelle des kleinsten filterelements

konstruiere einen filter und fuelle ihn mit den ersten elementen: LET filtergroesse = 10 ; ROW filtergroesse TEXT VAR filterinhalt ; fuelle ihn mit den ersten elementen ; bestimme zelle des kleinsten filterelements •

64 fuelle ihn mit den ersten elementen INT VAR fuellstand :: 0 ; WHILE fuellstand < filtergroesse AND noch elemente da REP hole neues element ; fuellstand INCR 1 ; filterinhalt [fuellstand] := neues element ENDREP • bestimme zelle des kleinsten filterelements INT VAR betrachtete zelle , zelle des kleinsten elements := 1 ; FOR betrachtete zelle FROM 2 UPTO fuellstand REP IF filterinhalt [zelle des kleinsten elements] > filterinhalt [betrachtete zelle] THEN zelle des kleinsten elements := betrachtete zelle FI ENDREP

leite zugriff auf die folge der elemente ein FILE VAR eingabe := sequential file (input, "ottokar") • noch elemente da

NOT eof (eingabe) •

hole neues element : TEXT VAR neues element get (eingabe, neues element)

liste ausgefilterte elemente auf : FOR betrachtete zelle FROM 1 UPTO fuellstand REP putline (filterinhalt [betrachtete zelle]) ENDREP •

65

übungen 29) Modifizieren Sie das Filterprogramm so, daß die zelm längsten unterschiedlichen Worte gefunden werden. 30) Lassen Sie ein ROW 100 INT mit Zufallszahlen füllen, und entwickeln Sie aus folgendem Ansatz einen einfachen Sortieralgorithmus: markiere erste position des rows ; WHILE noch nicht das ganze markiert REP erweitere markierung auf die naechste position halte das markierte wohlsortiert ENDREP •

Verwenden Sie die Invarianzbedingung Der markierte Teil des ROWs ist immer sortiert und alle darin enthaltenen Elemente sind kleiner oder gleich als alle Elemente im nichtmarkierten Teil. Am Ende soll das. ganze ROW aufsteigend sortiert sein. Entwickeln Sie den Algorithmus "beweisend" und begründen Sie ihre Designentscheidungen ausführlich!

Zwei Prinzipien wollen wir zum Abschluß dieses ersten Teils noch einmal betonen: Gute Verbalisierung haben wir versucht, in den Beispielen vorzuführen, und auch in Einzelheiten diskutiert. Man bedenke Was man nicht gut darstellen kann, hat man meistens nicht vollkommen verstanden. Wohlüberlegte Namensgebung ist der erste Schritt zur verständlichen Darstellung. Erst einmal müssen wir uns fragen, was benannt werden soll. Das erübrigt sich für Objekte wie Variablen, die olme Namen nicht existieren können. Bei Denotern (LET-Vereinba-

rung) und Refinements aber hat der Programmierer freie Wahl. Zwei Leitlinien sind:

66 Aktionen oder Teilausdriicke sollten immer dann als Refinement ausgelagert und benannt sein, wenn der Name dem Leser (Programmierer) relevante zusätzliche Information über die Bedeutung liefern kann. (Das gilt für Denoter bzw. LET- Vereinbarungen entsprechend.) Refinements sollen das Programm gliedern! Man soll sich auf höheren Ebenen nicht durch Detailprobleme ablenken lassen, sondern sich voll und ganz auf die Programmlogik der gerade betrachteten Abstraktionsebene konzentrieren. In unseren Beispielen wurde der Algorithmus meistens auf der obersten Ebene vollkommen abstrakt nur mit Refinements formuliert. Die Konkretisierung auf bestimmte Objekte, Datentypen und vorgegebene Operationen fand dann später in diesen Refinements statt. Diese hierarchische Strukturierung ist ein äußerst wichtiges Denk- und Entwicklungsprinzip. Mit zunehmendem Können wird man es (hoffentlich) immer besser anwenden; die einzelnen Ebenen werden aber weiter auseinander liegen, weil man einige der anfänglich noch schwierig erscheinenden Algorithmen mit zunehmender Erfahrung als trivial ansieht. (Wir sehen diese Entwicklung schon von der ersten Besprechung der Maximumsuche in Kapitel 4 bis zum erneuten Einsatz im 'Filter'.) Man verfalle aber auf keinen Fall der Fehleinschä tzung, von einem gewissen Stadium an seien Refinements fast überflüssig. Ein guter Programmierer mit Erfahrung wird sie zwar (für sich selbst) bei unseren Einführungsbeispielen etwas sparsamer verwenden, aber bei komplizierteren Problemen um so mehr einsetzen. Auf die Frage, wie ein Name beschaffen sein soll, kann man eigentlich nur antworten: aussagekräftig, treffend und genau! Er sollte jeden Leser des Programms möglichst leicht faßbar über den Sinn und Zweck des Objekts bzw. der Aktionen unterrichten. Er sollte möglichst viel Information liefern - allerdings nur wesentliche, so daß er das Denken durch Abstraktion erleichtert. Oft (nicht immer) werden Variablen verhältnismäßig kurze, Refinements längere Namen erhalten. Kürze (macht manches übersichtlicher) und Ausführlichkeit (macht manches verständlicher) muß man stets im Einzelfall gegeneinander abwägen. Es lassen sich kaum allgemeingültige Kegeln aufstellen. Wir hoffen jedoch, daß die Beispiele in diesem Buch dem Leser den Weg weisen.

67 Neben guter Verbalisierung ist klare, folgerichtige und geplante Entwicklung für den Erfolg wichtig. Programmieren ist eine Tätigkeit des Kopfes, nicht der Finger auf irgendeiner Tastatur. Darum sollten wir jeden Algorithmus so entwerfen, daß wir wissen, wie und warum er (richtig) funktioniert, bevor wir ihn codieren und am Rechner testen. Solche "Beweise" (nicht ganz im mathematischen Sinne) haben wir in den früheren Kapiteln schon vorgeführt. Gerade bei größeren Programmen wird der Test aber immer noch Fehler ergeben, oder schlimmer: die Fehler treten erst viel spä ter (evtl. nach Jahren) im echten Einsatz auf. Deshalb ist jeder vermiedene oder auf "kaltem" Wege (d.h. vor seinem Auftreten) durch Nachdenken gefundene Fehler Gold wert. Sauberes Entwickeln heißt oft, den Algorithmus parallel mit der Beweisidee zu konstruieren; manchmal führt sogar erst der Beweis zum Programm. Wir haben das u.a. beim Zeichnen einer Strecke in Kapitel 5 vorgeführt. Häufig baut ein solches Vorgehen darauf auf, daß man bestimmte Objekte zueinander passend - konsistent - hält. Formal kann dies durch eine Invarianzaussage (s. Kap. 7) beschrieben werden. Wir wollen dieses Thema hier nicht weiter formalisieren, dem Leser aber empfehlen, sich unter diesem Gesichtspunkt noch einmal mit - N1aximumsuche - Strecke zeichnen - Sieb des Erathostenes zu beschäftigen.

68

Teil II

Etwas größere Algorithmen

Für die heute immer neue Anwendungsfelder erobernde Daterwerarbeitung sind Kleinprogramme (wie "Palindromprüfung) kaum charakteristisch. Um drei Zahlen der Größe nach zu ordnen, kann man in einem sehr kurzen Programm alle Fallunterscheidungen durchgehen. Für 30 Zahlen wird der schlechteste Sortieralgorithmus reichen, aber für 3000 Zahlen wird man sich einen besseren wünschen. Wenn es darum geht, für die Telefonauskunft einer Großstadt ein nach Rufnummern geordnetes Teilnehmerverzeichnis (die Irwertierung des üblichen Telefonbuchs) zu erstellen, muß man u.a. überlegen, ob vielleicht ein spezieller Algorithmus sinrwoll ist, der eine Vorsortierung nach Bezirken ausnutzt. Die Programme werden durch eine Vielfalt möglicher Situationen und die höhere Komplexität der Aufgabenstellungen häufig groß und schwerer überschaubar. Sie verwenden dann viele kleinere Algorithmen für verschiedenste Operationen und verknüpfen sie zu neuen, größerern Einheiten. Damit unterscheiden sich Programme für die Praxis erheblich von den Spielprogrammen des Anfängers. Für die Wartung der Programme ist es aber von erheblicher Bedeutung, daß Übersicht und Lesbarkeit gewahrt bleiben. Nur so lassen sich spä ter gewünschte Änderungen oder Ergänzungen rationell unterbringen. Für jeden neuen Anwender sind Eingriffe in umfangreichere Programme erst möglich, wenn jeder Programmabschnitt seine Bedeutung klar dokumentiert. Deshalb behalten Refinements, die solche Programmabschnitte zusammenfassen, benennen und gliedern, auch für längere Programme ihr Gewicht. Ebenso ist der Abstieg vom Allgemeinen zum konkreten Detail (eben die "Verfeinerung") nach wie vor erwünscht. Viele Schwierigkeiten sich damit gut bewältigen, weil sie bei diesem Prozeß schrittweise deutlich werden und in wohlbeschriebener Weise gelöst werden können. Wir werden bald sehen, daß wir mit den bereits behandelten Elementen bei größeren Programmen aus an Grenzen stoßen. Deshalb benötigen wir neue Mittel zur Entwicklung von Algorithmen und führen sie in den nächsten Kapiteln ein.

69

9. Prozeduren als Gliederungsmittel - Kioskzentrale

Als erste etwas größere Aufgabe wollen wir das Problem der Kioskzentrale betrachten: Man stelle sich eine erst kürzlich erbaute Stadt mit schachbrettartigem Grundriß vor:

(, LJ

f1

I..J

fL 1 .J

~~

LfJ

ffll

q.l ,!~

LfJ

Horizontal wie vertikal gibt es je n Straßen. Alle Kioske in der Stadt liegen an Kreuzungen. Sie sollen von einem zentralen Auslieferungslager genau einmal täglich beliefert werden. Die vorhandenen Lieferwagen fassen leider nur Material für einen Kiosk, so daß jedesmal vom Depot zum Kiosk und wieder zurück gefahren werden muß. Die Kioske sind schon gebaut; gesucht wird ein Standort für das Depot, der die Summe der Fahrtwege minimal läßt. Wir können das Problem lösen, indem wir alle n

*n

möglichen Positionen für das Depot

ausprobieren und die beste suchen (Minimum!Maximumsuche). Das Programm zerfällt offensichtlich in zwei Teile: 1.

Eingabe der Kiosk-Koordinaten

2. Ermittlung eines optimalen Depotstandortes Zuerst wollen wir uns auf den zweiten Teil konzentrieren, weil er der eigentlich inter-

70 essante ist und wir erst bei der Konstruktion dieses Teilalgorithmus feststellen können, welche Daten (respektive Variablen im Programm) zur Lösung beIiitigt werden. Für die Minimumsuche können wir uns am Vorbild des Programms in Kap. 4 orientieren: nimm erste kreuzung ; nimm diese als als bisher besten standort WHILE noch kreuzungen zu untersuchen REP nimm naechste kreuzung ; IF diese ist besser THEN nimm diese als bisher besten standort FI ENDREP ; nimm bisher besten als wirklich besten standort •

Unser Algorithmus setzt "erstes" und "nächstes" voraus. Von Natur aus hat der Stadtplan aber keine sequentielle Struktur wie die "Folge" in Kap. 4. Wir müssen ihm deshalb eine ]{eihenfolge aufprägen, beispielsweise indem wir ihn Zeile für Zeile durchlaufen:

+

+ +

+ +~ +

+~

Wenn wir das in unseren ursprünglichen Ansatz einbauen, gibt es beim Auswählen der "nächsten" Kreuzung Probleme. (Man muß u.a. auf das ZeilenencIe achten.) Einfacher geht es, wenn wir uns etwas von der Vorstellung einer "Folge" lösen und direkt den Durchlauf durch das Schachbrett formulieren: durchlaufe den ganzen stadtplan INT VAR Y ; FOR Y FROM 1 UPTO n REP durchlaufe aktuelle zeile ENDREP • durchlaufe aktuelle zeile : INT VAR x ; FOR x FROM 1 UPTO n REP untersuche aktuelle kreuzung ENDREP •

71

Unser Verfahren vergleicht jeden neuen Standort mit dem bisher besten. d.h. bei jedem Schritt - auch dem ersten - muß ein solcher "bisher bester" zum Vergleich zur Verfügung stehen. Ursprünglich haben wir das erreicht, indem wir zu Anfang den ersten als ''bisher besten" angenommen und dann vom zweiten an gesucht haben. Jetzt hat die erste Kreuzung aber keine Sonderstellung mehr. Woher bekommen wir dann zu Anfang unseren "bisher besten" Standort? (W ählen wir trotz allem wieder die erste Kreuzung, wird sie überflüssigerweise doppelt behandelt.) Zur Lösung dieser Aufgabe und ähnlicher Probleme gibt es einen Standardtrick: Man fügt der betrachteten Grundgesamtheit (in diesem Fall dem Stadtplan) ein ~ perschlechtes Pseudoelement hinzu, das in Bezug auf die untersuchte Eigenschaft schlechter als alle Elemente der Grundgesamtheit ist. Natürlich muß man genau untersuchen, ob es ein solches superschlechtes Element glbt. Darauf werden wir spä ter eingehen. Wenn wir zu Beginn das Superschlechte als das "bisher beste" der leeren Menge festsetzen, arbeitet unser Algorithmus korrekt, da schon das erste betrachtete Element besser ist. Damit erhalten wir: nimm superschlechten als bisher besten standort untersuche den ganzen stadtplan ; nimm bisher besten als wirklich besten standort • untersuche den ganzen stadtplan INT VAR x, Y ; FOR Y FROM I UPTO n REP FOR x FROM 1 UPTO n REP untersuche diese kreuzung ENDREP ENDREP • untersuche diese kreuzung : berechne neue wegesumme IF neue wegesumme < bisher kleinste wegesumme TREN nimm diese kreuzung als bisher besten standort

FI. Um die Summe aller Fahrtwege zu berechnen benötigen wir die x- und y-Koordinaten aller Kioske, die wir am einfachsten in zwei ROWs ('x kiosk' und 'y kiosk') abspeichern können. Ohne Beschränkung der Allgemeinheit setzen wir den Abstand zwischen zwei benachbarten Straßen als 1 fest. Die kürzeste Entfernung zwischen zwei Kreuzungen berechnet sich dann als die Summe der x- und y-Abstände:

72 berechne neue wegesumme : INT VAR neue wegesumme :: 0 , kiosk nr FOR kiosk nr FROM 1 UPTO anzahl kioske REP neue wegesumme INCR weg zu diesem kiosk ENDREP • weg zu diesem kiosk : ABS (x - x kiosk [kiosk nr]) + ABS (y - y kiosk [kiosk nr]) •

Nun werden wir uns mit dem Problem der 'nimm ••• '-Refinements und in diesem Zusammenhang mit dem Superschlechten befassen. Für die superschlechte Wegesumme suchen wir einen Wert, der alle sonst möglichen Wegesummen echt übersteigt. Bei einem n Grundriß kann die Entfernung zwischen zwei Kreuzungen nie größer als 2

*n

*

n

sein, so

daß sich die größtmögliche Wegesumme leicht durch Multiplikation mit der Anzahl der Kioske abschä tzen läß t. nimm superschlechten als bisher besten standort

INT VAR x bisher bester standort , y bisher bester standort , bisher kleinste wegesumme

..

2

*

n

*

anzahl kioske + 1

nimm diese kreuzung als bisher besten standort x bisher bester standort := x ; y bisher bester standort := y ; bisher kleinste wegesumme := neue wegesumme

.

Man beachte, daß wir bei 'nimm superschlechten••• ' auf Zuweisungen an die Koordinatenvariablen des bisher besten Standortes nur deshalb verzichten können, weil diese sicher ( !) nicht gelesen werden, bevor sie im Laufe der folgenden Untersuchung definierte Werte erhalten!

Die bisherige Entwicklung hat uns gezeigt, welche Daten wir benötigen, und wie wir sie im Programm organisieren können, so daß wir die oberste Ebene jetzt beschreiben können: LET n = 20 anzahl kioske

= 100 ;

ROW anzahl kioske INT VAR x kiosk, Y kiosk ermittle kiosk koordinaten ; ermittle optimalen depot standort

73 Die Konstruktion von 'ermittle optimalen depot standort' hat schon ein größeres Gebilde aus mehreren ltefinements ergeben. Nun wollen wir die Eingabe der Kiosk-Koordinaten aber komfortabel gestalten und dem Bediener jedesmal den Stadtplan mit allen schon bekannten Kiosken auf dem Bildschirm zeigen. Um diesen Teil mit den benötigten Refinements von der Ermittlung des Depotstandorts abzugrenzen und eine übergeordnete Gliederung zu erhalten, verwenden wir als neues Sprachmittel die Prozedur. Damit ergibt sich als komplettes Programm: LET n = 20 anzahl kioske

=

100 ;

ROW anzahl kioske INT VAR x kiosk, Y kiosk ermittle kiosk koordinaten ; ermittle optimalen depot standort PROC ermittle kiosk koordinaten INT VAR kiosk nr ; FOR kiosk nr FROM 1 UPTO anzahl kioske REP erfrage koordinaten dieses kiosks zeichne stadtplan mit allen schon bekannten kiosken ENDREP • erfrage koordinaten dieses kiosks : REP put ("Gib x- und y-Koordinate des Kiosks Nr.") put (kiosk nr) ; put ("ein:") get (x kiosk [kiosk nr] get (y kiosk [kiosk nr] line ; IF koordinaten sind gueltig THEN LEAVE erfrage koordinaten dieses kiosks ELSE put ("Bitte nur Werte zwischen 1 und") ; put (n) ENDREP koordinaten sind gueltig (x kiosk [kiosk nr] AND (y kiosk [kiosk nr]

)= )=

1 AND x kiosk [kiosk nr] 1 AND Y kiosk [kiosk nr]

zeichne stadtplan mit allen schon bekannten kiosken erzeuge plan als gittermuster ; trage alle bekannten kioske ein gib plan aus •

depot



81

Prozedur mit Parametern: Prozeduren mit Parametern ermöglichen es, allgemein verwendbare (parametrisierte) Algorithmen zu entwerfen. Form der Deklaration:

PkOC Prozedurname ( formale Parameterliste ) Abschnitt · Refinements ENDPROC Prozedurname Form des Aufrufs: Prozedurname

aktuelle Parameterliste )

"Liste" bedeutet hier ein Ubjekt oder mehrere durch Komma getrermte Ubjekte. Man karm ähnlich wie bei Datendeklarationen abkürzen, werm Typ und Accessattribut aufeinanderfolgender formaler Parameter gleich sind: PRoe a (INT eUNST k, 1, m, ltEAL VAR x, y) Besonders wichtig ist der Unterschied zwischen formalen und aktuellen Parametern. Bei jedem Aufruf werden die aktuellen auf die formalen Parameter abgebildet: Prozeduraufruf x

aktuelle Parameter

y z

Prozedurdeklaration

-----> -----> ----->

a b

formale Parameter

c

Diese Abbildung nermt man auch Parameterübergabe. Jedesmal, werm die aufgerufene Prozedur einen formalen Parameter anspricht, wird das aktuell übergebene Ubjekt benutzt. Das ist mehr als eine Wertekopie, da auch Schreibzugriffe direkt auf dem übergebenen Datenobjekt ausgeführt werden (z.B. 'x optimum' - 'depot'). Formale Parameter werden mit Angabe ihres Datentyps und Accessrechts im Prozedurkopf spezifiziert. Wie üblich bedeutet dabei VAl{ Schreib-und Leserecht, eUNST nur Leserecht. Allerdings bezieht sich das Zugriffsrecht nur auf den formalen Parameter und gilt nur irmerhalb der Prozedur. Übergibt man aktuell

VAR

----->

CONST

formal

schränkt man das Accessrecht ein. (Eine Ausweitung CUNST nach VAR ist nicht erlaubt!) Man karm CUNST-Parameter als reine Eingangsparameter, VAR-Parameter als Ein- und Ausgangsparameter ansehen. Mit der Spezifikation eines formalen Parameters als eUNST verspricht die Prozedur dem Aufrufenden, diesen Parameter nicht zu verändern. (Das prüft der ELAN-Übersetzer auch nach!) Aus Sicherheitsgründen sollte man deshalb alle Parameter, die die Prozedur nicht verändern muß, auch als eUNST spezifizieren. VARs sind nur notwendig, werm dem Aufrufer etwas mitgeteilt werden muß - wie in unserem Beispiel der Depotstandort.

82 In den folgenden Kapiteln werden wir uns immer mehr auf parametrisierte Prozeduren konzentrieren und umgebende Rahmenprogramme häufig nur andeuten.

Zurück zum inhaltlichen Problem der Kioskzentrale. Können wir das Verfahren noch weiter beschleunigen? Betrachten wir die Prozedur 'ermittle optimum', so fällt auf, daß die Abstimmung für jeden Depotstandort neu durchgeführt wird. Genau an dieser Stelle haben wir den Algorithmus aber etwas überstürzt entwickelt und "geschludert". Deshalb wollen wir die Prozedur noch einmal langsam und sorgfältig entwickeln. Der erste Teil ist ohne Zweifel eine gute und richtige Beschreibung des parlamentarischen Spiels: beginne ganz links ; WHILE die rechten haben die absolute mehrheit REP gehe einen schritt nach rechts ENDREP •

Bei Schleifen sind in der Hegel Invarianzen wichtig (s. Kap. 7). Es kommt darauf an, das Modell in sich konsistent zu halten. Unser Modell wird durch die 'anzahl kioske', das 'depot' und die 'anzahl der rechten' beschrieben. 'anzahl kioske' ändert sich während des Algorithmus' nicht, wohl aber 'depot' und aufgrund dieser Verschiebungen auch 'anzahl der rechten'. Wir müssen also auf die Konsistenz dieser beiden Größen achten. Dabei mti. jederzeit gelten: 'anzahl der rechten' ist die Anzahl aller Kioske rechts vom aktuellen 'depot'. Diese grundlegende Beziehung müssen wir im Refinement 'beginne ganz links' herstellen. Die einfachste Aussage über die Hechten können wir machen, wenn das Depot nicht auf der ersten, sondern links von der ersten Straße steht; dann nämlich sind alle Kioskbesitz er "rechts". Nun läßt uns "ganz links" durchaus die Freiheit, bei der Stelle 0 (links außerhalb der Stadt) zu beginnen: beginne ganz links : depot := 0 ; INT VAR anzahl der rechten :: anzahl kioske.

Das eigentliche Problem, unser Modell in sich konsistent zu erhalten, liegt bei 'gehe einen schritt nach rechts'. Die Kioskbesitzer entscheiden sich nun einzig aufgrund der geometrischen Lage; d.h. alle bisherigen Rechten bleiben rechts, bis auf diejenigen, die vorher genau 1 rechts vom Depot standen - die "Anrainer" des neuen Depotstandorts :

83

gehe einen schritt nach rechts : depot INCR 1 ; anzahl der rechten DECR anzahl der neuen depot anrainer Wir benötigen also für jeden möglichen 'standort' (l ...n) die Anzahl der Anrainer. Bei der x-Untersuchung ist das die Zahl aller Kioske mit 'xkiosk(i)

= standort';

entsprechendes

gilt bei der y-Untersuchung. Diese Anrainerzahlen könnten wir jedesmal ermitteln, indem wir alle Kioske durchgehen. Dann wäre aber nichts an Geschwindigkeit gewonnen. Viel günstiger ist es, zu Anfang alle Anrainerzahlen zu bestimmen: bestimme alle anrainerzahlen : setze alle anrainerzahlen auf null ; verteile alle kioske und zaehle anrainer setze alle anrainerzahlen auf null ROW n INT VAR anzahl anrainer

INT VAR i ; FOR i FROM I UPTO

n

REP

anzahl anrainer [i] := 0

ENDREP • verteile alle kioske und zaehle anrainer FOR i FROM 1 UPTO anzahl kioske REP anzahl anrainer [kiosk [i]] INCR 1

ENDREP • Damit haben wir mit sehr geringem Aufwand (n+k Durchläufe) alle Anrainerzahlen bestimmt. Wichtig: Hier sind wir das erste Mal auf das "Jojo" der Programmierung gestoßen. Unsere ursprüngliche Entwurfsmethode war, strikt von der höchsten Ebene nach unten zu entwickeln und zu verfeinern ("Top-Down"). Jetzt haben wir mittendrin festgestellt, daß wir ein neues Hilfsmittel (Anrainerzahlen) brauchen und den Algorithmus dafür auch auf der oberen Ebene leicht abändern müssen. Ähnliches Auf und Ab (daher "Jojo") tritt bei der Programmentwicklung häufig auf. Erkenntnisse oder Ideen, die man auf einer fortgeschrittenen Entwicklungsstufe bekommt, legen Änderungen oder gar Neuentwicklungen auf anderen Stufen nahe. Wichtig ist dabei, daß man die neuen Ideen auf keinen Fall nur "hineinflickt", sondern die Konsequenzen vollständig zieht und jede betroffene Ebene entsprechend umstrukturiert - notfalls sogar vollkommen neu entwirft. Auf lange Sicht macht sich sauberes Arbeiten immer bezahlt.

84 Unsere Prozedur sieht nach den notwendigen Änderungen für die Anrainerzahlen jetzt so aus: PROC ermittle optimum (ROW anzahl kioske INT CONST kiosk, INT VAR depot) bestimme alle anrainerzahlen ; beginne ganz links ; WHILE die rechten haben die absolute mehrheit REP gehe einen schritt nach rechts ENDREP • alle anrainerzahlen : n INT VAR anzahl anrainer VAR i ; i FROM 1 UPTO n REP anzahl anrainer [i] := 0 ENDREP i FROM 1 UPTO anzahl kioske REP anzahl anrainer [kiosk [i]] INCR 1 ENDREP

bestimme ROW INT FOR FOR

beginne ganz links depot := 0 ; INT VAR anzahl der rechten •• anzahl kioske. gehe einen schritt nach rechts : depot INCR 1 ; anzahl der rechten DECR anzahl anrainer [depot] • die rechten haben die absolute mehrheit anzahl der rechten > anzahl kioske DIV 2 • ENDPROC ermittle optimum

Dieses Verfahren braucht im Sclmitt nur 2*(n+k+nJ2) Durchläufe. In unaerem Beispiel sind wir also von ursprünglich 40 000 Durchläufen erst auf 4 000 und dann auf 260 gekommen. *)

C.H.A. Koster et al.: "Es gibt stets einen besseren Algorithmus!"

85 Übungen 35) Wenden Sie Übungen 32-34 auf die neuen Algorithmen an. 36) Parametrisieren Sie den in Kapitel 5 vorgestellten Algorithmus 'Gerade zeiclmen' zu einer Prozedur, die benutzt wird, um eine Schar von Zufallsgeraden zu erzeugen. 37) a) Schreiben Sie nach dem besten in Kapitel 6 entwickelten Algorithmus zur Berechnung der Quadratwurzel eine Prozedur ltEAL PIWC wurzel (ltEAL CONST zahl) Die Prozedur soll "narrensicher sein", d.h. sie soll bei jeder Parameterversorgung sinnvoll reagieren, also auch bei ungültigen Werten wie negativen REALs. Benutzen Sie für Fehlermeldungen die Standardprozedur PltOC errorstop (TEXT CONST fehlermeldung) Diese bricht das laufende Programm mit Ausgabe der übergebenen Meldung ab. b) Mit der Standardprozedur 'random' werden gleichverteilte REAL-Zufallszahlen im Intervall zwischen 0 und 1 erzeugt. Wenn man die Prozedur 'wurzel' auf diese Zufallszahlen anwendet, resultiert eine neue Verteilung, die statistisch untersucht werden soll.

11. Zwischenspiel: Das Prozedurkonzept

Prozeduren sind in der Regel abgeschlossenere Objekte als Refinements. Trotzdem erscheint es auf den ersten Blick etwas verwirrend, zwei so ähnliche Konzepte zu haben. Deshalb wollen wir die wichtigsten Unterschiede kurz gegenüberstellen: Refinements: - Sie werden nur durch ihren Namen beschrieben. - Sie werden oft für stark spezialisierte Operationen eingesetzt, die häufig nur einmal benötigt werden. -

Ihre Wirkung ist in der Regel nur im Zusammenhang (mit anderen Refinements) zu verstehen.

86 - Jedes Refinement kann auf alle Daten der umgebenden prozedur zugreüen. Es gibt keine Möglichkeit, "eigene" Daten eines Refinements vor dem Zugruf aus anderen Refinements zu schü tzen. Alle diese Eigenschaften sind bei der Top-Down-Programmierung innerhalb einer kleinen, überschaubaren Umgebung sehr wünschenswert. Dort braucht man ein Mittel zur Strukturierung und Dokumentierung, das möglichst wenig Zwängen unterliegt. Bei der Programmierung "im Größeren" wird die Lage jedoch zu unübersichtlich, wenn man ein Programm nicht in geschlossene Teile (Prozeduren) aufgliedert, die dann in sich wieder überschaubare Einheiten sind. Prozeduren: - Sie werden nicht nur durch ihre Namen, sondern auch durch ihre Parameter beschrieben. - Sie werden häufig eingesetzt, um allgemeiner verwendbare Operationen zu implementieren, da sie durch Parameterübergabe auf verschiedenen Datenobjekten arbeiten können. - Ihre Wirkung kann oft allgemein beschrieben werden. - Da Daten und Refinements lokale Objekte sind (nur innerhalb der Prozedur sichtbar), bilden Prozeduren vor Zugriffen von außen geschützte Einheiten. Aufgrund ihres geschlossenen Innenlebens gibt es auch keine Namenskollisionen mit Objekten anderer Prozeduren. Prozeduren bilden die nächstgrößere Stufe der Programmierung. Innerhalb von Prozeduren programmiert man top-down mit Hilfe von Refinements. Betrachten wir als Beispiel nochmals die beiden Prozeduren unseres Kioskprogramms. Zuerst: PROC ermittle kioskkoordinaten INT VAR kiosk nr ; FOR kiosk nr FROM 1 UPTO anzahl kioske REP erfrage koordinaten dieses kiosks zeichne stadtplan mit allen schon bekannten kiosken ENDREP • ENDPROC ermittle kioskkoordinaten

87

- Die Wirkung der Prozedur (auf die globalen Programmdaten) läßt sich einfach beschreiben: die Felder 'x kiosk' und 'y kiosk' werden mit gültigen (D Koordinaten gefüllt.

- Die Operation ist von der Programmierung umfangreich und stellt einen vom restlichen Programm unabhängigen Teil dar. Ihre Realisierung hat - solange sie den richtigen Effekt liefert - keine Seitenwirkungen auf andere Programm teile. - Die beiden ltefinements 'erfrage...' und 'zeichne...' sind kleine Operationen, die nur von dieser Prozedur benutzt werden. Sie hängen implizit vom lokalen Zustand der Prozedur ab: i)

'erfrage koordinaten dieses kiosks' identifiziert den Kiosk über die lokale Variable 'kiosk nr'.

i i) 'zeichne stadtplan mit allen schon bekannten kiosken' benutzt ebenfalls 'kiosk nr', um festzustellen, welche Kioske schon definiert sind. *) Betrachten wir nun die zweite Prozedur: PROG ermittle optimum (ROW anzahl kioske INT GONST kiosk, INT VAR optimum): bestimme alle anrainerzahlen ; beginne ganz links ; WHILE die rechten haben die absolute mehrheit REP gehe einen schritt nach rechts ENDREP • ENDPROG ermittle optimum ;

Aufgrund der Parametrisierung haben wir hier eine etwas allgemeiner einsetz bare Prozedur: - Die Wirkung erstreckt sich nur auf ihre Parameter und ist auch nur von den Parametern abhängig. Die Schnittstelle zum restlichen Programm umfaßt also nur die Parameterliste (da keine globalen Variablen von der Prozedur angesprochen werden). - Die Operation ist von der Programmierung her umfangreich. Wie wir in Kap. 10 gezeigt haben, sind verschiedene ltealisierungen möglich, die die Logik des restlichen Programms nicht beeinflussen.

Falls aus 'zeichne...' eine Prozedur gemacht werden soll, damit sie im ganzen Programm verfügbar ist, muß daraus eine allgemeine Operation werden, die mindestens mit der Anzahl der gültigen Kioske parametrisiert wird.

*)

88

- Die Refinements bilden ein lokales Modell mit ganz speziellen Operationen. Dazu gehört auch die lokale ROW 'anzahl anrainer'. Alle diese lokalen Objekte sind voneinander abhängig und gehören somit zusammen. Die gegenseitigen Beziehungen sind aufgrund der ltefinementnamen und der kleinen Umgebung (von PROC bis ENDPROC) überschaubar und verständlich. Interessanterweise sind Prozedurnamen oft kürzer als ltefinementnamen. Das liegt zum einen daran, daß in der Parameterliste die benutzten Datenobjekte aufgeführt werden. (Bei ltefinements schmuggeln sich diese manchmal in den Namen ein.) Zum anderen lassen sich für allgemeinere und geschlossenere Operationen in vielen Fällen recht kurze und prägnante

~amen

finden.

Einige Merkregeln zu Prozeduren: 1. Halte mögliChst viele Daten prozedurlokal! Alle Variablen, die nur von einer Prozedur benutzt werden, sollte man auch dort (als lokale Objekte) deklarieren, um sie vor unbefugtem Zugriff zu schützen.*) 2. Prozeduren müssen überschaubar bleiben. Bei zu großen Prozeduren verliert man - trotz Refinements - leicht den Überblick. Resultat: Programmierfehler! 3. Definiere Prozeduren so, daß ihre Anwendung zu übersichtlichen Programmen führt! Dafür sollte man genau das zur Prozedur machen, was sich gut als geschlossene Operation beschreiben und verwenden läßt. 4 • Parainetrisiere deine Prozeduren! (Versteckte) Kommunikation zwischen Prozeduren über globale Variablen führt leichter zu Programmierfehlern als (sichtbare) Kommunikation über Parameter. Im Zweifelsfall sollte man Prozeduren deshalb parametrisieren. Das heißt aber nicht "Parameter um jeden Preis". Zu viele Parameter führen zur Unübersichtlichkeit. Außerdem gibt es Fälle, in denen offensichtlich allen Prozeduren dieselben Datenobjekte zugrundeliegen. Dann ist es meist siruwoll, diese direkt als globale Da ten anzusprechen.

*) Insbesondere sollte man nie ein- und dieselbe Variable in verschiedenen Prozeduren für unterschiedliche Aufgaben verwenden, um Speicher zu sparen!

89 Zum Abschluß dieses Kapitels noch weiteres ELANesisch zu Prozeduren:

Wertliefernde Prozeduren:

Ähnlich wie Refinements können auch Prozeduren Werte liefern. Dabei muß der Datentyp allerdings explizit angegeben werden: Resultattyp PHOC Prozedurname Aufgrund des (Soll-)ltesultattyps kann der Compiler prüfen, ob auch tatsächlich ein Objekt des richtigen Typs geliefert wird. Unstimmigkeiten im Programm werden so leichter festgestellt. Eine Prozedur liefert einen Wert, indem der Prozedurrumpf mit einem entsprechenden Ausdruck schließt. Beispiele: INT PROC min (INT CONST a, b) IF a < b TREN a ELSE b FI

ENDPROC min ; TEXT PROC gespreizt (TEXT CONST wort, INT CONST spreizung) TEXT VAR spreizwort :: wort SUB 1 ; fuege das restwort buchstabenweise mit spreizung an spreizwort • fuege das restwort buchstabenweise mit spreizung an INT VAR i ; FOR i FROM 2 UPTO LENGTR wort REP spreizwort CAT (spreizung * " " + (wort SUB i» ENDREP • ENDPROC gespreizt

90

Parameterübergabe: Die Datentypen der aktuellen und der zugeordneten formalen Parameter müssen immer übereinstimmen. Bei den Zugriffsrechten ist aktuell

formal

VAli~~----------~)VAli

CONST

~

) CONST

erlaubt.*) Als aktuelle Parameter kann man auch Ausdrücke verwenden. Diese werden jeweils direkt vor dem Prozeduraufruf ausgewertet, dann wird das Resultat an die Prozedur übergeben. Die meisten Ausdrücke liefern nur Werte (z.B. 'i+1'), d.h. Objekte mit dem Zugriffsattribut CONST. Es gibt aber auch VAR-liefernde Ausdrücke (z.B. 'xkiosk (i)'), die dann auch an formale VAli-Parameter übergeben werden können. In keinem Fall kann das einmal übergebene Datenobjekt innerhalb der gerufenen Prozedur gewechselt werden. Bei a (x+l);

PROG a (INT GONST y) : x INGR 1; •••

führt 'x INC1{ l' in der Prozedur 'a' nicht dazu, daß 'y' gleichzeitig mit ansteigt. Ebenso bewirkt bei a ( liste[i] );

PROG a (INT VAR x) : x INGR 1; •••

die Anweisung 'i INCR l' innerhalb der Prozedur 'a' nicht, daß die schon übergebene Variable gewechselt wird und 'x' danach ein anderes Element der ROW 'liste' bezeichnet. Achtung Falle: Bei a (x)

PROG a (INT GONST

y)

: x

INGR 1 ;

bewirkt die Veränderung der globalen Variablen 'x' gleichzeitig die Veränderung des Parameters 'y' (trotz CONST)! Da sich die Einschränkung des Zugriffs rechts nur auf den Namen 'y' bezieht, 'y' und 'x' aktuell aber dasselbe Objekt bezeichnen, sind Schreibzugriffe darauf über den Namen 'x' möglich. Auch zwei aktuell gleiche Parameter, von denen mindestens einer an einen formalen VAR-Parameter übergeben wird, a (x,x) ;

PROG a (INT GONST y, INT VAR z) : •••

sind genauso fehlerträchtig. Solches "Alias" ist nur ungefährlich, wenn ausschließlich CONSTs benutzt werden, wie bei a (0,0) ;

PROG a (INT GONST y, z)

*) Die erforderlichen Überprüfungen kann der ELAN-Compiler stets zur Übersetzungszeit durchführen; das Programm wird dadurch also nicht langsamer, sondern nur sicherer.

91

Standardprozeduren: Bis jetzt als "vordefinierte Standardoperationen" bezeiclmete Objekte wie 'put' sind in Wirklichkeit auch Prozeduren, die dem ELAN-System allerdings schon standardmäßig bekannt sind und die genauso wie neu definierte Prozeduren aufgerufen werden. Rückblickend kann man die unterschiedliche Parameterversorgung von beispielsweise 'put' und 'get' besser fassen: PROC put (TEXT CONST wort) : ... PROC get (TEXT VAR wort) : ... Die Zugriffsattribute der Parameter ergeben sich zwangsläufig: Bei 'put' muß 'wort' als 'CONST' spezifiziert werden, damit auch TEXT CONSTs wie Denoter ("Hallo") oder Ausdrücke (t SUB 2) ausgegeben werden können. Eine Veränderung des übergebenen Parameters ist in keinem Fall notwendig. Bei 'get' muß 'wort' als 'V AR' spezifiziert sein, weil die Prozedur schreibend darauf zugreift; sie gibt der Variablen aufgrund einer Benutzereingabe einen neuen Wert.

Generische Prozeduren: Es gibt viele unterschiedliche Standardprozeduren mit gleichen Namen, beispielsweise: PltOC PROC PltOC PROC PltOC PROC

put put put put put put

(TEXT CONST wort) (INT CONST wert) (REAL CONST wert) (FILE VAR f, TEXT CONST wort) (FILE VAlt f, INT CONST wert) (FILE VAlt f, REAL CONST wert)

Prozeduren sind generische Objekte. Im Gegensatz zu Datenobjekten muß ihr Name sie nicht eindeutig identifizieren. Die genaue Bestimmung der gerufenen Prozedur erfolgt vielmehr zusä tzlich durch die Anzahl und Typen der übergebenen aktuellen Parameter. Die Auswahl der Prozedur erfolgt in folgenden Schritten: 1.

Es wird die Menge aller bekannten Prozeduren mit dem entsprechenden Namen betrachtet.

2.

Von den aktuellen Parametern des Aufrufs werden Typ und Zugriffs attribut festgestellt.

3.

Innerhalb der Menge der Prozeduren mit dem richtigen Namen wird diejenige ausgewählt, deren formale Parameterspezifikation zu den festgestellten aktuellen Parametern paßt. (Die Typen müssen übereinstimmen.)

92 4.

Die Zugriffsattribute der aktuellen und der formalen Parameter werden überprüft. Eine unverträglichkeit (aktuell CONST, formal VAR) wird als Fehler gewertet.*)

5.

Der Resultattyp der gewählten Prozedur wird übernommen. **)

Beispiele der generischen Identifikation:

subtext (wort,l,S)

-----> -----> -----> -----> ----->

subtext (wort,6)

----->

min (1,2) min (1.0, 2.0)

put ("Hallo") put (min(1, 2»

INT PROG min (INT GONST a,b) REAL PROG min (REAL GONST a, b)

PROG put (TEXT GONST wort) PROG put (INT GONST wert) TEXT PROG subtext (TEXT GONST text, INT GONST von, bis) TEXT PROG subtext (TEXT GONST text, INT GONST von)

Für den Programmierer ist dieses generische Prinzip sehr angenehm, denn er muß sich nicht für "im wesentliche gleiche" Operationen verschiedene Namen merken. Anmerkung: Bei Operatoren ist die Lage ähnlich. So gibt es INTs, REALs und TEXTe.

'+'

(glücklicherweise) für

LEAVE-Anweisung: Entsprechend zum Refinement kann die Abarbeitung einer Prozedur durch LEAVB

Prozedurname

oder (bei wertliefernden Prozeduren) durch LEA VE

Prozedurname

WITH

ausdruck

abgebrochen werden.

CONST und VAR ebenso wie die Typen zur generischen Identifizierung zu benutzen, würde erheblich mehr Verwirrung als Nutzen stiften. **) Der Resultattyp kann nicht mit zur generischen Identifikation benutzt werden. Man betrachte z.B.

*)

INT PltOC x : 1 ENDPROC x ; REAL PROC x : 2.0 ENDPROC x ; put (x) Wird jetzt '1' oder '2.0' ausgegeben?

93

~

Gierige Algorithmen - Bergsteigen

Eine Klasse unternimmt eine Bergwanderung. Im Gelände sind alle Wege möglich; aber der Lehrer, dem das Steigen schon einige Mühe bereitet, achtet sehr darauf, keine schon erreichte Höhe zu verschenken. Das Bergprofil bietet ihm dazu ideale Möglichkeiten, denn es besitzt weder Plateaus noch Nebengipfel oder Mulden sondern nur einen Gipfel.*) Unser Ziel ist, einen Algorithmus zu entwickeln, der die Klasse in jedem Einzelschritt näher an das Ziel bringt. Wir geben uns ein ltaster auf der Karte vor, über dem sich der eingipflige Berg erhebt. Von einem Startort an versuchen wir, so von ltasterpunkt zu ltasterpunkt zu wandern, daß jeder erreichte neue möglichst viel höher liegt als der vorige. Dazu prüfen wir jedesmal die in den vier Himmelsrichtungen benachbarten ltasterpunkte. Wegen der Monotonievoraussetzung (keine Plateaus und keine Mulden) muß es

*) Der Mathematiker spricht von einer unimodalen Funktion z

Monotonie.

f(x,y) mit strenger

94

darm genau einen Punkt mit größter Höhe unter den vier Punkten geben. Ihn wählen wir als Zielort für unseren nächsten Schritt. Wenn keiner der vier Nachbarpunkte die schon erreichte Höhe übersteigt, haben wir das Gipfelquadrat erreicht: ein höherer Ort in einem Nachbarquadrat würde die Monotoniebedingung verletzen, weil darm ein bereits geprüfter Ort eine Mulde darstellen würde. Dann verfeinern wir das Raster und setzen die Klettertour mit kleinerer "Schrittweite" fort: beginne die tour am startort mit vorgegebener schrittweite REP klettere bis es nicht mehr ho eher geht ; verkleinere die schrittweite UNTIL schrittweite ist zu klein geworden ENDREP nimm aktuellen ort als gipfel • klettere bis es nicht mehr hoeher geht : REP nimm aktuellen ort als bisher hoechsten untersuche osten ; untersuche westen untersuche norden ; untersuche sueden ; IF kein hoeherer ort gefunden THEN LEAVE klettere bis es nicht mehr hoeher geht

FI; nimm bisher hoechsten als neuen ort ENDREP •

Der Algorithmus "giert" nach Höhe und verbessert die aktuelle Höhe ständig, bis das Gipfelquadrat erreicht ist. Solange die 'schrittweite' noch groß genug bleibt, um Fortschritte zu garantieren (abhängig von der REAL-Arithmetik), wird das Verfahren wiederholt mit verkleinerter Schrittweite durchgeführt, um den Standort des Gipfels im Rahmen der Rechengenauigkeit möglichst genau einzuengen. Wollen wir den Algorithmus als parametrisierte Prozedur schreiben, möchten wir nicht nur Werte wie Startort und Anfangsschrittweite als Parameter übergeben, sondern auch die von 'x' und 'y' abhängige Funktion 'hoehe' • Glücklicherweise karm man auch Prozeduren als Parameter übergeben:

95

PROG bergsteigen (REAL PROG (REAL GONST, REAL GONST) hoehe, REAL GONST start x, start y, start schritt) beginne die tour am startort mit vorgegebener schrittweite

REP klettere bis es nicht mehr hoeher geht ; verkleinere die schrittweite UNTIL schrittweite ist zu klein geworden ENDREP nimm aktuellen ort als gipfel • beginne die tour am startort mit vorgegebener schrittweite : REAL VAR x :: x start, y :: y start, schrittweite :: startschritt verkleinere die schrittweite :

schrittweite := schrittweite / 2.0 •

schrittweite ist zu klein geworden : (x + schrittweite = x) OR (y + schrittweite

y).

(*1*)

nimm aktuellen ort als gipfel : put ("Gipfel bei: x =") ; put (x) put (" y =") ; put (y) ; line • klettere bis es nicht mehr hoeher geht :

REP nimm aktuellen ort als bisher hoechsten untersuche osten ; untersuche westen untersuche norden ; untersuche sueden ;

IF kein hoeherer ort gefunden THEN LEAVE klettere bis es nicht mehr hoeher geht FI ; nimm bisher hoechsten als neuen ort

(*2*)

ENDREP •

(*1 *) Als Abbruchkriterium wird benutzt, daß eine weitere Halbierung der Schrittweite zu keiner Veränderung des x-oder y-Wertes mehr führt, weil die Grenze der Genauigkeit der REAL-Verarbeitung erreicht ist. (*2*) Die Entscheidung 'kein hoeherer ort gefunden' muß vor C!) 'nimm bisher hoechsten als neuen ort' fallen, da man sonst nicht ohne weiteres entscheiden kann, ob überhaupt ein neuer höherer Ort gefunden wurde.

96 nimm aktuellen ort als bisher hoechsten ~~VM

bisher bestes x .. x , bisher bestes y :: y , bisher beste hoehe .. hoehe (x,y) nimm bisher hoechsten als x := bisher bestes x y := bisher bestes y kein hoeherer ort gefunden (x = bisher bestes x)

neuen

~D

ort

(y

bisher bestes y)

.

untersuche osten : IF hoehe (x+schrittweite, y) > bisher beste hoehe TREN bisher bestes x := X+schrittweite ; bisher beste hoehe := hoehe (x+schrittweite, y) FI • untersuche westen : IF hoehe (x-schrittweite, y) > bisher beste hoehe TREN bisher bestes x := x-schrittweite ; bisher beste hoehe := hoehe (x-schrittweite, y) FI • untersuche norden : IF hoehe (x, y+schrittweite) > bisher beste hoehe TREN bisher bestes y := y+schrittweite ; bisher beste hoehe := hoehe (x, y+schrittweite) FI • untersuche sueden : IF ho ehe (x, y-schrittweite) > bisher beste hoehe TREN bisher bestes y := y-schrittweite ; bisher beste hoehe := hoehe (x, y-schrittweite) FI • ENDPROC bergsteigen

97

Prozeduren als Parameter: Werm eine Prozedur statt eines Wertes (V AR oder CONST) als Parameter erwartet wird, muß sie in der formalen Parameterliste in der Form Resultattyp PROC ( virtuelle Parameterliste ) Procname aufgeführt werden. Die Angabe des Resultattyp entfällt, falls die erwartete Prozedur kein Resultat liefert. Die virtuelle parameterliste beschreibt die Parameter der erwarteten Prozedur. Jeder wird dort mit Typ und Zugriffs recht aufgeführt - allerdings ohne Namen (s. obiges Beispiel). Dieser Teil Cinclusive der Klammern) entfällt, falls die erwartete Prozedur keine Parameter hat. Soll beim Aufruf eine Prozedur als Parameter übergeben werden, karm das in der Kurzform PHOC Procname

geschehen, werm der Name die Prozedur eindeutig identifiziert. Existieren mehrere (generische) Prozeduren gleichen Namens, muß die Langform Resultattyp PROC ( virtuelle Parameterliste bzw. PROC ( virtuelle Parameterliste )

gewählt werden, die die gewünschte Prozedur durch Angabe aller Parameter vollständig indentifiziert.

Im Programm wird das Gelände durch eine Funktion 'hoehe(x,y)' beschrieben, die noch deklariert werden muß. Zum Testen mag man eine Halbkugel mit ungewöhnlichem Mittelpunkt oder eine Glockenfläche nehmen: REAL PROC halbkugel (REAL CONST x, y) sqrt( 25.0 - (x-pi)**2 - (y-e)**2 )

(*1*)

ENDPROC halbkugel REAL PROC glocke (REAL CONSTx,y) exp ( -(x-e)**2 - (y-pi)**2 ENDPROC glocke ; bergsteigen (PROC halbkugel, 0.0, 0.0, 1.0) bergsteigen (PROC glocke, 0.0, 0.0, 1.0) ;

(*1*) 'pi' und 'e' sind im ELAN-Standard enthaltene Konstanten.

98 Das dargestellte Verfahren stellt einen Algorithmus dar, der in jedem Einzelschritt versucht, ein Kriterium (hier: die erreichte Höhe) zu verbessern. Umwege, die evtl. das Kriterium nur vorübergehend verschlechtern, werden nicht zugelassen. Solche Algorithmen nennt man 'gierige Algorithmen'. Sie lassen sich für viele Fragestellungen einsetzen. Einige studieren wir in den öbungsaufgaben.

übungen 38) Anstatt beim Bergsteigen nur in den Haupthimmelsrichtungen zu suchen, kann man die Nebenrichtungen Nordosten, Nordwesten usw. hinzunehmen und in einer AchtPunkte-Umgebung untersuchen. Verändern Sie den obigen Algorithmus so und studieren sie "Kletterrouten" und Laufzeiten im Vergleich! 39) Gegeben sind eine Anzahl von n Objekten und ein ltucksack, der höchstens m kg fassen kann. Jedes Objekt i hat ein Gewicht g(i) und einen Wert w(i). Es kann in kontinuierlichen Portionen x(i) 0 = x(i) = 1 in den Rucksack gefüllt werden, d.h. höchstens kann das betreffende Objekt als Ganzes mit x(i) = 1.0 genommen werden. Der Gesamtwert des ltucksacks soll optimiert werden. Kürzer:

Maximiere Summe( xCi) 10

ENDPROC ist im lexikon

Konzentrieren wir uns auf die wesentlichen Operationen "Suchen" und "Eintragen", ergIbt sich als gröbste Formulierung: INT PROC lexikonindex (TEXT CONST wort) berechne hashklasse ; INT VAR index ; durchsuche die hashklasse index

PROC erweitere lexikon um (TEXT CONST wort) berechne hashklasse ; fuege wort als juengsten eintrag in die hashklasse ein •

Die Hashklassen wird man als Folgen von Wörtern organisieren, die linear durchsucht werden können. Im Prinzip spielt es dabei keine Rolle, ob man beim jüngsten oder ältesten Eintrag anfängt zu suchen. Aufgrund der Vermutung, daß ein neues Wort in der untersuchten Umgebung oft häufiger wieder auftritt als alte schon im Wörterbuch vorhandene (mit zufälligerweise gleichem Hashwert), wollen wir die Suche immer beim jüngsten Eintrag beginnen lassen: durchsuche die hashklasse setze index auf juengsten eintrag der klasse WEILE suche muss noch fortgesetzt werden REP setze index auf vorlaeufereintrag ENDREP •

155 suche muss noch fortgesetzt werden : noch in der klasse CAND untersuchter eintrag

wort •

Nun müssen wir uns entscheiden, wie die Hashklassen im Programm verwirklicht werden sollen. Da sie sehr unterschiedliche Umfänge annehmen können, scheidet eine Realisierung als ROW 300 ROW 5 TEXT aus. Es ist theoretisch möglich, daß, auch bei guter Hashfunktion, alle untersuchten Wörter zufällig in die gleiche Klasse fallen. Deshalb muß eine Klasse bis zur Gesamtgröße des Lexikons wachsen können. Eine Lösung ist die Verwendung von Zeigern: ROW 1000 STRUCT (TEXT eintrag, INT vorlaeufer) VAR lexikon

Jede Lexikonzelle besteht aus dem eigentlichen Texteintrag und einem INT-Feld, das angibt, welche Lexikonzelle der Vorläufer des aktuellen Eintrags innerhalb der Hashklasse ist. Das Klassenende ("Erdung" der Kette) wird durch 0 als 'vorlaeufer' markiert. Für die Klassenanfänge braucht man eine zusä tzliche Tabelle: ROW 300 INT VAR hashtabelle

Der Name "Hashtabelle" hat sich eingebürgert, weil diese Tabelle die Unterteilung in Hashklassen repräsentiert. Sie enthält für jede Klasse einen Zeiger auf den Klassenanfang im Lexikon. Wie bei den Zeigern innerhalb des Lexikons wird eine leere Klasse durch 0 in der Hashtabelle markiert.

Nach diesen Vorüberlegungen können wir das ganze Paket entwickeln: PACKET lexikon behandlung DEFINES lade lexikon aus , sichere lexikon in , erweitere lexikon um , lexikonindex , ist im lexikon :

156 LET maximale lexikongroesse anzahl der hashklassen

1000 300

erdung = 0 INT VAR aktuelle lexikongroesse ; ROW anzahl der hashklassen INT VAR hashtabelle ROW maximale lexikongroesse STRUCT (TEXT eintrag, INT vorlaeufer) VAR lexikon PROC lade lexikon aus (TEXT CONST dateiname) loesche hashtabelle ; mache lexikon leer fuelle lexikon aus datei loesche hashtabelle : INT VAR i ; FOR i FROM 1 UPTO anzahl der hashklassen REP hashtabelle [i] := erdung ENDREP • mache lexikon leer : aktuelle lexikongroesse := 0 • fuelle lexikon aus datei : FILE VAR lexikondatei:= sequential file (input, dateiname) TEXT VAR wort ; WHILE NOT eof (lexikondatei) REP getline (lexikondatei, wort) erweitere lexikon um (wort) ENDREP • ENDPROC lade lexikon aus ; PROC sichere lexikon in (TEXT CONST dateiname) : FILE VAR lexikondatei:= sequential file (output, dateiname) INT VAR i ; FOR i FROM 1 UPTO aktuelle lexikongroesse REP putline (lexikondatei, lexikon [i].eintrag) ENDREP • ENDPROC sichere lexikon

157 INT PROC hash (TEXT CONST wort) : INT VAR quersumme :: 0, i ; FOR i FROM 1 UPTO LENGTH wort REP quersumme INCR code (wort SUB i) ENDREP ; (quersumme MOD anzahl der hashklassen) + 1 • ENDPROC hash ; PROC erweitere lexikon um (TEXT CONST wort) INT CONST hashklasse :: hash (wort) ; IF lexikon ist schon voll TREN errorstop ("Uberlauf des Lexikons") ELSE fuege wort als juengsten eintrag in die hashklasse ein • FI • lexikon ist schon voll : aktuelle lexikongroesse

=

maximale lexikongroesse

fuege wort als juengsten eintrag in die hashklasse ein belege neue lexikonzelle neue zelle. eintrag := wort neue zelle.vorlaeufer := anfang der hashklasse anfang der hashklasse := index der neuen zelle belege neue lexikonzelle

aktuelle lexikongroesse INCR 1

index der neuen zelle :

aktuelle lexikongroesse

neue zelle

lexikon [index der aktuellen zelle] •

anfang der hashklasse

hashtabelle [hashklasse] •

ENDPROC erweitere lexikon um INT PROC lexikonindex (TEXT CONST wort) INT CONST hashklasse :: hash (wort) INT VAR index ; durchsuche die hashklasse index • durchsuche die hashklasse : setze index auf juengsten eintrag der klasse WHILE suche muB noch fortgesetzt werden REP setze index auf vorlaeufereintrag ENDREP •

158 setze index auf juengsten eintrag der klasse index := hashtabe1le [hashk1asse] setze index auf vor1aeufereintrag : index := aktuelle zelle.vorlaeufer suche muss noch fortgesetzt werden : index erdung GAND aktuelle zelle. eintrag aktuelle zelle:

wort.

(*2*)

lexikon [index] •

ENDPROG lexikonindex ; BOOL PROG ist im lexikon (TEXT GONST wort) lexikonindex (wort)

>0

ENDPROG ist im lexikon ; ENDPAGKET lexikon behandlung

lIashing ist ein Standardverfahren der Programmierung. Eine Abart des hier vorgestellten Hash mit Verkettung ist das Verfahren Hash mit Rehash. Hierbei wird die strikte Trennung der Hashklassen aufgegeben. Das ist möglich, weil das Suchverfahren auch dann funktioniert, wenn mehr als nur die eigene Hashklasse durchsucht wird. Die Grundidee von Hash mit Rehash ist folgende: Man nimmt die Grundgesamtheit selbst (hier das Lexikon) als Hashtabelle. Das Suchen läuft nach folgendem Algorithmus ab: bestimme aktuelle zelle durch hashfunktion WHILE aktuelle zelle wort AND aktuelle zelle waehle neue zelle vermittels rehash UNTIL alle zellen der tabelle inspiziert ENDREP

""

REP

"Rehash" ist eine weitere Funktion, die, vom aktuellen Index ausgehend, einen neuen Index auswählt. Wichtig ist dabei, daß durch fortgesetztes Rehash auch alle Tabelleneinträge erreicht werden können. Eine sehr einfache Rehash-Funktion ist index := «index+l) MOD tabellengroesse) + 1 Dieser "Hash mit Rehash" ist etwas einfacher als der von uns programmierte Hashalgorithmus (mit Verkettung). Sobald aber die Hashtabelle stärker gefüllt wird, weist er

(*2*) CAND ist wichtig, weil der rechte Teil der Abfrage bei 'index = erdung' nicht ausgewertet werden darf, da die aktuelle Zelle undefiniert ist. Ansonsten würde 'lexikon(O), angesprochen und das Programm mit Fehler abgebrochen.

159 deutliche Effizienznachteile auf. Zum einen steigen die Such- und Eintragezeiten (Eintragen ist Suchen nach leerem Eintrag!) dann stark an, zum anderen können Verlangsamungen durch "Klumpungen" auftreten, wenn verschiedene Hashklassen über Kehash kollidieren.

übungen 71) Konstruieren Sie verschiedene Hashfunktionen und vergleichen Sie ihre Güte. Testen Sie u.a ( (erster buchstabe + letzter buchstabe)

* laenge) MOD

300 + 1

hash := 0 ; FOR i FROM 1 UPTO LENGTH wort REP shifte hash zyklisch addiere buchstabe ENDREP ; hash + 1 • shifte hash zyklisch : hash INCR hash ; IF hash > 300 THEN hash DECR 299 FI addiere buchstabe : hash INCR code (wort SUB i) ; IF hash > 300 THEN hash DECR 299 FI •

72) Implementieren Sie das Lexikonpaket mit Hash/Rehash und vergleichen Sie. 73) Ersetzen Sie in den in Übung 69-70 entwickelten Programmen das Lexikonpaket durch das neue Lexikon mit Hash.

160

20. Abstrakte Datentypen und Operatoren - Bruchrechnung

Die Mächtigkeit des Sprachmittels "Paket" wird noch dadurch gesteigert. daß in ELAN auch neue Datentypen (abstrakte

~)

und Operatoren deklariert werden können.

Keicht man sie in einer DEFINES-Liste aus dem Paket hinaus. kann man den Satz der Standardtypen (INT. KEAL. TEXT ....) beliebig erweitern. Wir wollen das hier an einem kleinen Beispiel demonstrieren. Um die üblichen REAL-Problemen bei der Behandlung von Werten wie 1/3 zu vermeiden, soll ein Paket Bruchrechnung geschrieben werden, das neben dem Datentyp BKUCH als Basisoperationen die Grundrechenarten

+.-.*./.

die Zuweisung := und

die EinlAusgabeprozeduren 'put' und 'get' zur Verfügung stellt. Für die Kealisierung eines BRUCHs bieten sich zwei INTs als Zähler und Nenner an. Dabei sollten Brüche immer in vollständig gekürzter Form auftreten. Diese Kürzung durch den ggT erzielt man leicht mit Hilfe des Euklidischen Algorithmus. Bei den vorgegebenen Datentypen INT. KEAL. TEXT. BOOL können Werte direkt als Denoter im Programm angeben werden (z.B. 123. "otto"). Für den Datentyp BRUCH wäre etwas ähnliches wünschenswert. Nun kann man leider keine neuen Denoterformen definieren, aber den gleichen Effekt durch Prozeduren oder Operatoren erreichen. Man könnte eine BRUCH PKOC bruch (INT CONST zaehler, nenner) verwenden, die aus Zähler und Nenner einen gekürzten Bruch macht. Noch besser als die Notation 'bruch (1,4)' wäre aber die Form '1/4'. Da der I-Operator für INTs noch nicht definiert ist - die ganzzahlige Division heißt DIV -, können wir anstelle der Prozedur 'bruch' auch den Operator BKUCH OP I (INT CONST zaehler, nenner) neu definieren, der nicht dividiert sondern einen BKUCH bildet.

161 PACKET bruchrechnung DEFINES BRUCH, :=, +, -,

*, I, put, get

TYPE BRUCH = STRUCT (INT zaehler, nenner) BRUCH OP I (INT CONST zaehler, nenner) IF nenner = 0 TREN errorstop ("undefinierter Bruch: " + int(zaehler) + "/0") ELSE optimal gekuerzter bruch FI. optimal gekuerzter bruch : INT CONST kuerzung := ggt (zaehler, nenner) ; BRUCH: (zaehler DIV kuerzung, nenner DIV kuerzung) ENDOP I ; INT PROC ggt (INT CONST a, b) IF a MOD b = 0 TREN b ELSE ggt (b, a MOD b) FI ENDPROC ggt BRUCH OP + (BRUCH CONST 1, r) : (l.zaehler * r.nenner + r.zaehler ENDOP + ;

*

l.nenner) I (1. nenner

*

r.nenner)

BRUCH OP - (BRUCH CONST 1, r) : (l.zaehler * r.nenner - r.zaehler ENDOP - ;

*

l.nenner) I (1. nenner

*

r.nenner)

BRUCH OP * (BRUCH CONST 1, r) : (l.zaehler * r.zaehler) I (1. nenner ENDOP * ; BRUCH OP I (BRUCH CONST 1, r) (l.zaehler * r.nenner) I (1. nenner ENDOP I ;

*

*

OP := (BRUCH VAR ziel, BRUCH CONST quelle) CONCR (ziel) := CONCR (quelle) ENDOP := ;

r.nenner)

r.zaehler)

162 PROC put (BRUCH CONST bruch) IF bruch. nenner = 1 THEN put (bruch.zaehler) ELSE put ( text (bruch.zaehler) + li/li + text (bruch. nenner) ) FI ENDPROC put ; PROC get (BRUCH VAR bruch) TEXT VAR wort ; get (wort) ; suche bruchstrich im wort IF bruchstrich gefunden THEN uebernimm bruch ELSE uebernimm ganze zahl FI • suche bruchstrich im wort : INT CONST bruchstrich position := pos (wort, li/li) • bruchstrich gefunden : bruchstrich position

>0



uebernimm bruch : bruch := int (zaehlerteil) / int (nennerteil) •

(*1*)

uebernimm ganze zahl : bruch.zaehler := int (wort) bruch. nenner := 1 • zaehlerteil

subtext (wort, 1, bruchstrich position - 1) •

nennerteil

subtext (wort, bruchstrich position + 1)

ENDPROC get ; ENDPACKET bruchrechnung

Mit Hilfe des Typs BRUCH und seiner Operationen kann man z.B. ganz einfach eine geometrische Reihe mit den Gliedern a

n

:= a * qn 0

berechnen:

(*1*) Das führt bei Eingabe einer 0 als Nenner zum Fehlerabbruch "undefinierter Bruch••• " durch den eigenen I-Operator.

163 erfrage anfangsglied und quotient berechne soviel folgenglieder und summen wie moeglich • erfrage anfangsglied und quotient BRUCH VAR a, q; put ("Anfangsglied:") ; get (a) put ("Quotient:"); get (q) • berechne soviel folgenglieder und summen wie moeglich BRUCH VAR s :: a ; REP a := a

*

q

;

s := s + a ;

put (a); put (s) line ENDREP

Da Zähler lUld Nermer bald sehr hohe Werte armehmen, wird das Programm bald mit einem INT-Überlauf abbrechen. Auf dieses Problem wird in den Übungen näher eingegangen.

Operatoren: Operatoren körmen wie Prozeduren deklariert werden: Kesultattyp OP Opname Opera torrumpf ENDOP Opname

(. ein oder zwei Parameter

bzw. bei Operatoren, die kein Kesultat liefern: Opname ein oder zwei Parameter ) Operator rumpf ENDOP Opname

OP

Operatoren lUlterscheiden sich nur in drei Punkten von Prozeduren: a)

Operatornamen bestehen nicht aus Kleinbuchstaben. Zulässig sind nur die Sonderzeichen: + - * / = < > @ $ % • & ? ! - die Doppelzeichen: := = ** - aus GroBbuchstaben gebildete Namen (ohne Blanks)

b)

Operatoren haben entweder einen (monadische Operatoren) oder zwei Parameter (dyadische Operatoren).

c)

Der Aufruf erfolgt nach den inzwischen wohlbekarmten Regeln (s. Kap.3). Bei monadischen Operatoren ist der rechte (einzige) Operand der Parameter, bei dyadischen ist der linke Operand der erste lUld der rechte Operand der zweite Parameter. Die Prioritä t der Operatoren ergibt sich aus ihrem Namen (s. Anhang).

164 In allen anderen Punkten gleichen sich Prozeduren und Operatoren. (Der Operatorrumpf kann wie der Prozedurrumpf Deklarationen, Anweisungen und Refinements enthalten; Operatoren können wie Prozeduren auch rekursiv sein usw.) Man beachte, daß die Zuweisung := ebenfalls ein Operator ist! Operatoren können nicht nur im Zusammenhang mit neuen Datentypen benutzt werden, sondern auch dann, wenn die Operatorschreibweise beim Aufruf günstiger erscheint:

REAL OP $ (REAL GONST dollarbetrag) tageskurs * dollarbetrag ENDOP $ ; put ("DM"); put (3.5 + $ 27.8)

Abstrakte Datentypen:

Neue Datentypen, die zusammen mit ihren Basisoperationen durch Pakete implementiert werden, heißen abstrakte Datentypen. Form der Typdeklaration: TYPE Typname = Feinstruktur Der Typname darf dabei nur aus Großbuchstaben (ohne Blanks) bestehen. Die Feinstruktur kann - ein STlWer-Typ - ein RO W-Typ - ein anderer Datentyp sein. Beispiele: TYPE COMPLEX TYPE LEXIKON TYPE ZUSTAND

= STRUer (REAL re, = ROW 1000 TEXT; = INT

im) ;

Neu deklarierte Typen können genauso wie die Standardtypen benutzt werden, d.h. man kann damit Variablen und Konstanten deklarieren, ROWs und STRUers konstruieren usw. Wichtig: Außerhalb des definierenden Pakets ist die Feinstruktur des deklarierten Datentyps unsichtbar. Die Manipulation ist also nur unter Kontrolle des Pakets mit den Operationen möglich, die es für diesen neuen Datentyp zur Verfügung stellt. (Auch die Zuweisung := muß explizit aufgeführt werden, wenn sie für den neuen Typ verfügbar sein soll.) So kann man die Konsistenz eines BltUCHs nicht aU!.erhalb des Pakets Bruchrechnung stören, indem man durch

bruch.zaehler INGR bruch.zaehler ; bruch.nenner INGR bruch. nenner einen unvollständig gekürzten Bruch abspeichert.

165

KmJStruktor:

Mit dem Konstruktor können in dem Paket, das den neuen Datentyp implementiert, Werte dieses Typs aus ihren Komponenten "konstruiert" werden. Form: Typ

(

Wertliste

)

In der wertliste wird - durch Komma getrennt - für jedes Feld des Datentyps ein Wert aufgeführt. Dabei entspricht die Folge der einzelnen Werte der Folge der Felder in der Typdeklaration. Im Beispiel 'bruchrechnung' liefert BRUCH:(1,4) einen BRUCH-Wert mit dem 'zaehler' 1 und dem 'nenner' 4. Weitere Beispiele (zusammen mit Typdeklara tionen) : TYPE PUNKT = STRUCT (REAL X. y) ; PUNKT CONST nullpunkt:: PUNKT:(O.O, 0.0) ;

Konkretisierer:

Mit dem Konkretisierer kann man in dem Paket, das den neuen Datentyp definiert, auf die Feinstruktur des Typs zugreifen. Form: CONCR

Ausdruck

)

Im Paket Bruchrechnung wird der Konkretisierer benutzt, um die Zuweisung von Brüchen auf die standardmäßig vorgegebene Zuweisung auf Strukturen zurückzuführen: OP := (BRUCH VAR ziel, BRUCH CONST quelle) CONCR (ziel) := CONCR (quelle) ENDOP : =

Dabei haben 'CONCR (ziel)' und 'CONCR (quelle)' nicht mehr den Typ BRUCH, sondern den Typ 'STRUCT (INT zaehler, nenner)'. Darauf ist := aber definiert. Ohne CONCR müßten die Komponenten einzeln kopiert werden: OP := (BRUCH VAR ziel, BRUCH CONST quelle) ziel.zaehler := quelle.zaehler ziel. nenner := quelle. nenner

(**)

Bei einfachen Typen wie TYPE ZAHL

= INT

kommt man überhaupt nicht ohne CONClt aus. Weil die Feinstruktur weder ein STRUCT-

(**) Strikt genommen müßte es 'CONClt(ziel).zaehler := CONCR(quelle).zaehler' heißen,

da die Selektion von Feldern eigentlich nur auf einem STRUCT-Typ möglich ist. Um die Handhabung für den Programmierer zu vereinfachen, darf man aber bei Selektion und Subskription auf das CONCR verzichten.

166 noch ein ltOW-Typ ist, kann man auf das INT nur über den Konkretisierer zugreüen:

PROC put (ZAHL VAR wert) put (CONCR (wert» ENDPROC put Ohne CONClt wäre die Rückführung auf das INT-put unmöglich (Rekursion!).

übungen 74) Erweitern Sie das Paket 'bruchrechnung' um a) die Booleschen Operatoren =, , =, ,=, ; b) "Mixed Mode"-Operatoren, die bei der Zuweisung und den Rechenoperationen die Verknüpfung von INT- und BRUCH-Werten gestatten. 75) Verbesser:n Sie die ltechenoperationen so, daß es nur dann zu INT-Überläufen kommt, wenn das Endresultat in Zähler oder Nenner 'maxint' übersteigt. (Augenblicklich können auch Zwischenergebnisse vor dem Kürzen zum Überlauf führen.) 76) Erweitern Sie den Wertebereich des Datentyps BltUCH, indem Sie auf TYPE BltUCH

= STRUCT

(REAL zaehler, nenner)

übergehen. 77) Programmieren Sie die Algorithmen von Kap. 6 mit Hilfe des erweitern BRUCH-Pakets. (BRUCH statt REAL) 78) Entwickeln Sie ein Paket, das komplexe Zahlen mit allen notwendigen Operationen zur Verfügung stellt. 79) Entwickeln Sie ein Paket, das doppeltgenaue ganze Zahlen (DINT) mit den beootigten Grundrechenarten implementiert. Realisieren Sie BltUCH als STRUCT (DINT zaehler, nenner).

167

21. Das abschließende Projekt - Polynome

Für viele Aufgabenstellungen haben die Nullstellen von ganzen rationalen Funktionen besondere Bedeutung. So führt die Frage nach der Lage der Linse zwischen Gegenstand und Leinwand, so daß es reelle Bilder gibt, auf eine quadratische Gleichung, die Frage

nach der Eintauchtiefe einer schwimmenden Hohlkugel auf eine kubische, die Frage nach dem Abhebwinkel der Schaukeln eines Kettenkarussels auf die Nullstelle eines Polynoms vierten Grades. Die Untersuchung der Extrem- und Wendestellen von Funktionen höheren Grades wird über Nullstellen ihrer Ableitungspolynome beantwortet. Wir setzen uns deshalb das Ziel, Nullstellen von Polynomen höheren Grades zu ermitteln. Freilich beschränken sich die schulischen Möglichkeiten ohne Computer auf Polynome höchstens vierten oder fünften Grades. Diese werden aU1.erdem so speziell gewählt, daß sich ihre Nullstellen teilweise erraten (und danach abspalten), teilweise mit dem einzigen Algorithmus, der aus der Sekundarstufe I zur Verfügung steht, der Lösung der quadratischen Gleichung, bearbeiten lassen. Mit dem Computer könnte man sich zunächst Polynomen genau 5.Grades ·zuwenden: sie haben mindestens eine reelle Nullstelle mit Zeichenwechsel (warum?), die man durch Bisektion näherungsweise finden kann. Wenn man durch den entsprechenden Linearfaktor dividiert, verbleibt höchstens ein Polynom vierten Grades, für das es ebenso wie für Polynome dritten Grades analytisch geschlossene Lösungsformeln gibt, die sich auf der Schule erarbeiten lassen, wenn auch nicht ganz ohne einigen Aufwand. Für die Berechnung der Lage der Extrempunkte und Wendepunkte gelangt man an Polynome entsprechend niedrigeren Grades. Solche analytischen Verfahren sind aber nach einem bekannten Ergebnis von N.H.Abel nicht auf Polynome fünften oder höheren Grades ausdelmbar. Bevor wir uns den Nullstellenverfahren mit allen ihren Problemen widmen, überlegen wir, welche grundlegenden Objekte und Operationen beim Arbeiten mit Polynomen nützlich sein könnten. Polynome sind gute Kandidaten für einen Datentyp, den wir mit seinen Basisoperationen durch ein Paket 'polynome' realisieren können. In der Mathematik sind Polynome Funktionen der Form: p

an

* xn

+

a n- 1

* x n-1

+ •••• + a 1

*x

+ aO

Da Funktionen dieses Typs vollständig durch ihren Grad (höchste auftretende Potenz von x) und ihre Koeffizienten bestimmt werden, wählen wir als Feinstruktur des Daten-

168 typs: TYPE POLYNOM = STRUCT (INT grad, ROW anzahl koeffizienten REAL a) LET anzahl koeffizienten

=

10 ;

Hier beschränken wir uns auf Polynome maximal 9. Grades. Man kann den maximalen Grad höher ansetzen, muß sich aber über die numerischen Probleme im klaren sein. Betrachten wir als Beispiel ein Polynom 20. Grades an der Stelle 'x = 10.0'. Bei der Auswertung treten durch die verschiedenen Potenzen von 'x' leicht Werte im Bereich von 1.0 bis 1.0e20 auf, die, insbesondere bei Summenbildungen, schon zu beträchtlichen Rundungsfehlern führen können. Die numerische Stabilität hängt neben der Struktur des Polynoms auch von der REAL-Arithmetik des verwendeten Rechners ab (Mantissenlänge, Rundungsverfahren). Bei einer Mantissenlänge von 12 oder mehr Stellen und geeigneter Rundung werden viele Polynome 9. Grades noch unkritisch zu behandeln sein, wenn Operationen wie "Auswertung" unter Berücksichtigung der numerischen Stabilität konstruiert werden. Die oben erwähnte Auswertung eines Polynoms an der Stelle 'x' ist sicherlich eine häufig gebrauchte Basisoperation. Sie sollte deshalb schnell sein und möglichst wenig Rundungsfehler machen. Wir verwenden hier das bekannte Horner-Verfahren: p I x

Dabei treten relativ wenig Multiplikationen auf, und die Glieder der einzelnen Summen haben in der Regel einigermaßen gleiche Größenordnungen, da sie sich immer nur in einer Potenz von 'x' unterscheiden. Den Operator I verwenden wir, um "Auswertung an der Stelle x" auszudrücken: REAL OP I (POLYNOM CONST p, REAL CONST x) wert von p an der stelle x wert von p an der stelle x REAL VAR wert :: koeffizient [p.grad+l] INT VAR i ; FOR i FROM p.grad DOWNTO 1 REP wert := wert * x + koeffizient [i) PER; wert • koeffizient ENDOP I

p.a •

169 Weiterhin benötigen wir sicherlich Operationen für die Zuweisung, die Denotation (in Programmen) von Polynomen einfachen Grades (Einspolynom 'p=1', Genratorpolynom

'p=x') und schließlich die Ein-/Ausgabe von Polynomen. Außerdem mtf, man sich über Grad und Koeffizienten von Polynomen informieren können. Aus Gründen der Systematik hat das Nullpolynom 'p=O' für uns den Grad -1. Als Transformationen von Polynomen bieten sich "Ableitung" (anband der bekannten Potenz regel der Differentialrechnung), Stammfunktion und schließlich affine Streckung und Stauchung durch Multiplikation bzw. Division aller Koeffizienten mit einen REAL-

Wert:

OP := (POLYNOM VAR ziel. POLYNOM GONST quelle) POLYNOM PROG einspolynom POLYNOM PROG generatorpolynom PROG get (POLYNOM VAR p) PROG put (POLYNOM GONST p) INT PROG grad (POLYNOM GONST p) REAL PROG koeff (POLYNOM GONST p. INT GONST potenz) POLYNOM PROG ableitung (POLYNOM GONST p) POLYNOM PROG stammfunktion (POLYNOM GONST p) POLYNOM OP * (POLYNOM GONST p. REAL GONST faktor) POLYNOM OP / (POLYNOM GONST p. REAL GONST divisor) Die Mathematik kennt auf Polynomen ähnliche "Rechen"-operationen wie auf ganzen Zahlen:

POLYNOM POLYNOM POLYNOM POLYNOM POLYNOM POLYNOM

OP OP OP OP OP OP

+ (POLYNOM GONST p. q) - (POLYNOM GONST p. q) * (POLYNOM GONST p, q) DIV (POLYNOM GONST p. q) MOD (POLYNOM GONST p. q) - (POLYNOM GONST p)

Die Ähnlichkeit mit INTs zeigt sich bei der Division, die "mit Rest" arbeitet. (Deshalb nennen wir sie auch DIV und nicht I .) Addition und Subtraktion geschehen koeffizientenweise:

Bei der Multiplikation muB nach dem Distributivgesetz jeder Koeffizient des ersten mit jedem Koeffizient des zweiten multipliziert werden. Dabei ergibt die Summe der Koeffizientenstellen die Stelle. wo die multiplizierten Koeffizienten zum Ergebniskoeffizienten kumuliert werden:

170

Für die Polynom-Division sei an das schriftliche Grundschulverfahren der Division von ganzen Zahlen erinnert, die sich im Zehner-Stellenwertsystem auch als Summen von Zehnerpotenzen darstellen lassen: Kurzform: 143

11

Langform: 100 + 40 + 3 )

13

( 10 + 1 )

10 + 3

100 + 30

11

33

10 + 3

33

10 + 3

o

o

Aus der Langform entlehnen wir das in der Mittelstufe des Gymnasiums gelehrte Verfahren über die Division von Polynomen: 2 2 x + 5 ) 2 x - 1. 5 x

(x

- 0.5 x + 5

( 2 x - 3 )

0.5 x - 0.25 Rest:

4.25

- 0.5 x + 0.75 4.25

Da der Divisionsalgorithmus wegen der gleichzeitigen Berechnung von Quotient und Rest für DIV und MOD bemtigt wird, entwickeln wir ihn als Hilfsprozedur: PROC dividiere (POLYNOM CONST zaehler, nenner, POLYNOM VAR quotient, rest): IF nenner. grad < 0 THEN errorstop ("Division durch Nullpolynom") ELSE rest :; zaehler; bilde sukzessive quotient und verringere rest entsprechend reduziere grad des restes soweit moeglich FI •

bilde sukzessive quotient und verringere rest entsprechend quotient.grad :; zaehler.grad - nenner. grad; INT VAR quotientensteile ; FOR quotientensteile FROM quotient.grad+1 DOWNTO 1 REPEAT dividiere hoechste koeffizienten; verringere rest um teilergebnis mal divisor ENDREP •

171 dividiere hoechste koeffizienten: INT CONST zaehlerstelle :: nenner.grad + quotientenstelle quotient.a [quotientenstelle] := rest.a [zaehlerstelle] / nenner.a [nenner.grad+l] verringere rest um teilergebnis mal divisor : INT VAR i ; FOR i FROM zaehlerstelle DOWNTO quotientenstelle REPEAT rest.a [i] DECR quotient.a [quotientenstelle] * nenner.a [i-quotientenstelle+l] ENDREP • reduziere grad des restes soweit moeglich : rest.grad := nenner. grad - 1 WHILE rest.grad)= 0 CAND rest.a [rest.grad+l] rest.grad DECR 1 ENDREP •

0.0

REP

ENDPROC dividiere;

Der Grad des Restpolynoms muß geringer als der des Nennerpolynoms sein. Leider kann man den genauen Grad nur durch Vergleich der Koeffizienten mit 0.0 ermitteln. und das ist wegen der Ungenauigkeiten bei REALs eine sehr riskante Operation. I.a. muß man bei der MOD-Operation also damit rechnen, daß das Ergebnis wegen der Rundungsfehler einen zu hohen Grad aufweist! Nach diesen Überlegungen ergibt sich als komplettes Polynompaket: PACKET polynome DEFlNES

POLYNOM, koeff , grad , := , einspolynom , generatorpolynom , + , - , * , DIV , MOD , / , ableitung , stammfunktion , I ,

get, put :

TYPE POLYNOM = STRUCT (INT grad, ROW anzahl koeffizienten REAL a) LET anzahl koeffizienten = 10 ; OP := (POLYNOM VAR ziel, POLYNOM CONST quelle) CONCR (ziel) := CONCR (quelle) ENDOP := ;

172 INT PROG grad (POLYNOM GONST p) p.grad ENDPROG grad REAL PROG koeff (POLYNOM GONST p, INT GONST potenz) IF potenz =

0

ENDPROG koeff POLYNOM PROG einspolynom POLYNOM VAR resultat ; resultat. grad := 0 ; resultat. a [l] : = 1. 0 ; resultat ENDPROG einspolynom POLYNOM PROG generatorpolynom POLYNOM VAR resultat ; resultat.grad := 1 ; resultat.a [1] := 1.0 resultat.a [2] := 1.0 resultat ENDPROG generatorpolynom

REAL OP I (POLYNOM GONST p, REAL GONST x) wert von p an der stelle x • wert von p an der stelle x : REAL VAR wert :: koeffizient [p.grad+1] INT VAR i ; FOR i FROM p.grad DOWNTO 1 REP wert := wert * x + koeffizient [i] PER wert • koeffizient ENDOP I ;

p.a •

173 POLYNOM OP + (POLYNOM GONST p, q) lF p.grad < q.grad THEN q + P ELSE berechne koeffizientenweise die summe reduziere grad soweit moeglich ; summe Fl • berechne koeffizientenweise die summe POLYNOM VAR summe :: p lNT VAR i ; FOR i FROM 1 UPTO q.grad+l REP summe.a [i] lNGR q.a [i] ENDREP • reduziere grad" soweit moeglich : WHlLE summe.a [summe.grad+l] summe.grad DEGR 1 ENDREP •

0.0 AND summe.grad

ENDOP + POLYNOM OP - (POLYNOM GONST p, q) p

+

(-q)

ENDOP POLYNOM OP - (POLYNOM GONST p) POLYNOM VAR resultat ; resultat.grad := p.grad lNT VAR i ; FOR i FROM 1 UPTO p.grad+l REP resultat.a [1] := - p.a [i] ENDREP ; resultat ENDOP POLYNOM OP

*

(POLYNOM GONST p, q):

POLYNOM VAR produkt ; lF p.grad + q.grad < anzahl koeffizienten THEN berechne produktgrad setze produktkoeffizienten auf null kumuliere koeffizientenprodukte auf ELSE errorstop ("Grad des Produktes zu gross")

FI; produkt •

>0

REP

174

berechne produkt grad : IF p.grad < 0 OR q.grad < 0 THEN produkt.grad := -1 ELSE produkt.grad := p.grad + q.grad FI • setze produktkoeffizienten auf null : INT VA!!.. i ; FOR i FROM 1 UPTO produkt.grad +1 REP produkt.a [i] := 0.0 ENDREP. kumuliere koeffizientenprodukte auf : INT VAR j ; FOR i FROM p.grad+1 DOWNTO 1 REP FOR j FROM q.grad+l DOWNTO 1 REP produkt.a [i+j-l] INGR p.a [i] ENDREP ENDREP • ENDOP

*

q.a [j]

*

POLYNOM OP DIV (POLYNOM GONST p, q) POLYNOM VAR quotient, rest dividiere (p, q, quotient, rest) quotient ENDOP DIV ; POLYNOM OP MOD (POLYNOM GONST p, q) POLYNOM VAR quotient, rest dividiere (p, q, quotient, rest) rest ENDOP MOD PROG dividiere (POLYNOM GONST zaeh1er, nenner, POLYNOM VA!!.. quotient, rest): IF nenner. grad < 0 THEN errorstop ("Division durch Nullpolynom") ELSE rest := zaeh1er; bilde sukzessive quotient und verringere rest entsprechend reduziere grad des restes soweit moeg1ich FI •

175 bilde sukzessive quotient und verringere rest entsprechend quotient.grad := zaehler.grad - nenner. grad; INT VAR quotientenstelle ; FOR quotientenstelle FROM quotient.grad+l DOWNTO 1 REPEAT dividiere hoechste koeffizienten; verringere rest um teilergebnis mal divisor ENDREP • dividiere hoechste koeffizienten: INT GONST zaehlerstelle :: nenner.grad + quotientenstelle ; quotient.a [quotientenstelle] := rest.a [zaehlerstelle] / nenner.a [nenner.grad+l] verringere rest um teilergebnis mal divisor : INT VAR i ; FOR i FROM zaehlerstelle DOWNTO quotientensteile REPEAT rest.a [i] DEGR quotient.a [quotientensteile] * nenner.a [i-quotientenstelle+l] ENDREP • reduziere grad des restes soweit moeglich : rest.grad := nenner. grad - 1 WHILE rest.grad >= 0 GAND rest.a [rest.grad+l] rest.grad DEGR 1 ENDREP • ENDPROG dividiere; POLYNOM OP

*

(POLYNOM GONST p, REAL GONST faktor)

POLYNOM VAR resultat ; resultat. grad := p.grad INT VAR i ; FOR i FROM 1 UPTO p.grad+l REP resultat.a [i] := p.a [i] ENDREP ; resultat ENDOP

* ;

*

faktor

0.0

REP

176

POLYNOM OP / (POLYNOM CONST p, REAL CONST divisor) POLYNOM VAR resultat ; resultat.grad := p.grad INT VAR i ; FOR i FROM 1 UPTO p.grad+l REP resultat.a [i] := p.a [i] / divisor ENDREP ; resultat ENDOP / ; POLYNOM PROC ableitung (POLYNOM CONST p): POLYNOM VAR resultat ; INT VAR i; resultat.grad := p.grad - 1; FOR i FROM p.grad+l DOWNTO 2 REPEAT resultat.a [i-I] := real{i-l) * p.a [i] ENDREP ; resultat

(*1*)

ENDPROC ableitung; POLYNOM PROC stammfunktion (POLYNOM CONST p) POLYNOM VAR resultat IF p.grad < 0 THEN resultat := p ELIF p.grad < anzahl koeffizienten THEN berechne stammfunktion ELSE errorstop ("Grad des Polynoms zu gross") FI ; resultat berechne stammfunktion INT VAR i ; resultat.grad := p.grad + 1 ; FOR i FROM p.grad+l DOWNTO 1 REP resultat.a [i+l] := p.a [i] / real{i+l) ENDREP ; resultat.a [1] := 0.0 ENDPROC stammfunktion ;

(*1*) 'REAL PROC real (INT CONST wert)' konvertiert den übergebenen INT-Wert und liefert ihn als kesultat.

177 PROG get (POLYNOM VAR p): erfrage polynom ; reduziere grad soweit moeglich • erfrage polynom : put (" Grad des Polynoms ?") get (p.grad); lF p.grad < -1 THEN p.grad := -1 FI ; INT VAR stelle ; FOR stelle FROM p.grad+1 DOWNTO 1 REP put(" Koeffizient a(" ); put(stelle-1); put(") =" ); get (p.a [stelle) ENDREP • reduziere grad soweit moeglich WHILE p.grad >= 0 GAND p.a [p.grad+1) p.grad DEGR 1 ENDREP •

0.0

REP

ENDPROG get; PROG put (POLYNOM GONST p): lF p.grad < 0 THEN put ("0.0") ELSE gib nichttriviales polynom aus

FI; line •

gib nichttriviales polynom aus : INT VAR stelle := p.grad+1 ; put polynomteil (p.a [stellel, stelle-1) ; REP uebergehe folgende nullkoeffizienten ; lF stelle = 0 THEN LEAVE gib nicht triviales polynom aus

FI; gib plus minus und aktuellen koeffizienten aus ENDREP • uebergehe folgende nullkoeffizienten REP stelle DEGR UNTIL stelle = 0 GOR p.a [stelle]

0.0 ENDREP

178

gib plus minus und aktuellen koeffizienten aus IF p.a [stelleI >= 0.0 TREN put ("+") put polynomteil ( p.a [stelleI. stelle-I) ELSE put ("-") ; put polynomteil (- p.a [stelleI. stelle-I) FI • ENDPROC pu t ; PROC put polynomteil (REAL CONST koeffizient. INT CONST potenz) IF potenz > I TREN put ( text (koeffizient) + " x**" + text (potenz) ) ELIF potenz = I TREN put ( text (koeffizient) + " x" ) ELSE put (koeffizient) FI ENDPROC put polynomteil ENDPACKET polynome

übungen 80) Schreiben Sie das Paket von lWW-REAL auf den Datentyp VECfOR um. 81) Führen Sie einen effizienten Algorithmus für eine Potenzierung von Polynomen aus! 82) Schreiben Sie unter Verwendung des Standard-Datentyps COMPLEX ein Paket über ganze Gaußsche Zahlen (Gitterpunkte der Gaußschen Zahlenebene) mit ähnlichen Prozeduren!

Offenbar liegt die zentrale Schwierigkeit für eine allgemeingültige Diskussion einer ganzen rationalen Funktion in einer sicher funktionierenden Nullstellen-Prozedur, die auf die Funktion selbst und ihre beiden ersten Ableitungen angewendet werden kann. (Das bekannte und effiziente Newton-Verfahren konvergiert leider nicht immer.) Wir setzen voraus, daß zwei verschiedene Nullstellen eines Polynoms einen bekannten Mindestabstand nicht unterschreiten. (Solche Mindestabstände werden im allgemeinen in allen Anwendungsfällen aus der Problemlage bekannt sein.) Dann kann man ein Suchverfahren mit kleinerer Schrittweite einsetzen. Dieselbe Voraussetzung soll auch für andere ausgezeichnete Punkte gelten, also z.B. für Extremwerte.

179

Zur Ermittlung einer einfachen Nullstelle setzen wir ein Bisektionsverfahren ein; eine einfache Nullstelle führt immer zu einem Zeichenwechsel der Funktion. Für die stetigen Polynome sagt der Zwischenwertsatz, daß eine reelle Nullstelle existiert. Eine Ermittlung über kEAL-Größen wird sie mit begrenzten Ansprüchen an die Genauigkeit des kesultates ebenso wie bei der Berechnung der Quadratwurzel ( Seite??

) näherungs-

weise ermitteln. Was aber soll mit mehrfachen Nullstellen geschehen, die keinen Zeichenwechsel aufweisen? Schon der Scheitel von y =

würde so nicht gefunden werden. Dazu gibt es einen sehr einfachen Lösungsgedanken. Das Polynom

( x-I )2* ( x - 3 ) hat z.B. die einfache Nullstelle x(Ol)

=

x 3 - 5* x 2 + 7 * x - 3

= 3 und die

"berührende" doppelte Nullstelle x(02)

= 1. Das zugehörige Ableitungspolynom 2*( x - 1)*( x - 3 ) + ( x - I )2

=

3* x 2- 10* x + 7

hat mit ihm ersichtlich einen gemeinsamen Faktor, den Linearfaktor (x - 1). Wenn man also den größten gemeinsamen Teiler eggT) des Polynoms mit seinem Ableitungspolynom ermittelt und das Polynom durch diesen ggT dividiert, bleibt ein "Ersatzpolynom" übrig. Dieses besitzt die gleichen Nullstellen wie das ursprüngliche Polynom, nur sind alle Nullstellen einfach, die sich wie oben gezeigt approximieren lassen. Den ggT berechnet der Euklidische Algorithmus. Er läßt sich in der Mathematik ebenso auf Polynome wie auf ganze Zahlen anwenden. Diese Freiheit läßt sich auf das ELANProgramm übertragen. Wir benützen fast unverändert die für INT-Konstanten entwikkelte rekursive Prozedur, nehmen aber POL YN OMe als Parameter. Allerdings müssen wir für den Abbruch der Rekursion zwischen der starken Bedingung 'ist nullpolynom' der der Grad ist negativ (alle Koeffizienten sind Null) - und der schwächeren Bedin-

gung, daß der Grad Null ist, unterscheiden. Im letzten Fall muß das Einspolynom geliefert werden, denn die Polynome sind in jedem Falle teilepfremd, wenn ein absolutes Glied als Rest übrigbleibt.

POLYNOM PROC ggt (POLYNOM CONST p, q) IF grad (q) ELIF grad (q) FI ENDPROC ggt