Grundlagen der Informatik I “Programmierung”

Grundlagen der Informatik I “Programmierung” Grundlagen der Informatik I “Programmierung”

22.08.2013 Aufrufe

Entwurfsprinzip 4.5.13 (Invarianten als Abschwächung der Nachbedingung) Eine Invariante kann durch Abschwächung der Nachbedingung post auf folgende Arten erzeugt werden. Entfernen eines Konjunktionsgliedes: Wenn post die Form A ∧B ∧C hat, dann kann B entfernt werden und A ∧B als Invariante gewählt werden. B ist dann in Kandidat für die Abbruchbedingung. Ersetzen einer Konstanten durch eine Variable: In der Nachbedingung sum= q k=p ak kann zum Beispiel die Konstante q durch eine neue Variable i ersetzt werden, deren Wertebereich natürlich beschränkt werden muß. sum= q k=p ak ≡ sum= i k=p ak ∧ i=q Erweiterung des Wertebereichs einer Variablen: In der obigen Bedingung kann i=q erweitert werden zu p≤i≤q. Erweiterung durch Disjunktion: Als Invariante kann post ∨A gewählt werden, wobei A eine beliebige Bedingung ist. Die ersten drei Methoden sind ziemlich sinnvoll, während die letzte nur relativ selten eingesetzt wird. Meist tritt eine Kombination der Methoden auf. In Beispiel 4.5.11 haben wir z.B. erst eine Konstante durch eine Variable ersetzt und dann den Wertebereich dieser Variablen ausgedehnt. Eine Reihe weiterer Beispiele und ergänzender Hinweise findet man in [Gries, 1981, Kapitel 16]. Die Variante einer Schleife erfüllt zwei Aufgaben. Einerseits soll sie sicherstellen, daß die Schleife überhaupt terminiert. Zum anderen liefert sie auch eine obere Schranke für die Anzahl der Schritte, die bis zum Abbruch durchgeführt werden. Für ein und dasselbe Problem gibt es daher sehr verschiedene Varianten – je nachdem ob das Interesse eines Programmierers nur in der Terminierung oder auch in einer effizienten Lösung liegt. Auch wenn die Variante formal nur ein Integer-Ausdruck ist, beschreibt sie dennoch eine Eigenschaft des zu erzeugenden Programms. So ist z.B. bei unserem Programm zur Summierung von Folgenelementen die Variante die Anzahl der noch nicht aufsummierten Elemente, die in jedem Schritt geringer werden soll. Die Tatsache, daß diese Anzahl stets positiv ist, ergibt sich aus der Invarianten. Es lohnt sich daher, diese Eigenschaft zunächst informal zu beschreiben und dann mithilfe der bisher formulierten Eigenschaften in einen Integer-Ausdruck umzuwandeln. Entwurfsprinzip 4.5.14 (Strategie zum Erzeugen der Variante) Beschreibe die Variante zunächst informal als eine Eigenschaft, die sich aus der Invariante und der Spezifikation ergibt. Formalisiere sie dann als Integer-Ausdruck. 4.5.4 Sinnvoller Einsatz von Rekursion Wir haben im Abschnitt 4.3.10 gesehen, daß der Effekt von Schleifen genauso gut durch die Deklaration rekursiver Routinen erreicht werden kann. Rekursion ist ein nützliches Hilfsmittel, das unbedingt zum Repertoire eines guten Programmierers gehören sollte, da manche Probleme sich auf diese Art sehr viel eleganter lösen lassen als durch Schleifen. Es gibt zwei wichtige Programmierstrategien, bei denen Rekursion besonders häufig auftritt. Man kann eine Aufgabenstellung dadurch lösen, daß man einen Teil davon auf ein bekanntes Problem zurückführt und den Rest als einfachere Variante der ursprünglichen Aufgabenstellung ansieht. Man kann aber auch das Problem so aufteilen, daß beide Teile einfachere Varianten der ursprünglichen Aufgabenstellung sind. Diese Strategie ist unter dem Namen Divide&Conquer (Teilen und Erobern) bekannt geworden. Wir wollen beide kurz anhand von Beispielen erläutern.

Beispiel 4.5.15 (Vertauschen von Sektionen in Feldern) In einem Array a mit Grenzen lower und upper sollen zwei Sektionen Sl ≡ alower,alower+1,...,ap−1 und Sr ≡ ap,ap+1,...,aupper miteinander vertauscht werden. D.h. aus dem Feld soll das Feld alower,alower+1,...,ap−1 ap,ap+1,...,aupper ap,ap+1,...,aupper alower,alower+1,...,ap−1 entstehen. Da das ursprüngliche Feld sehr groß sein kann, kommt als zusätzliche Einschränkung hinzu, daß der zusätzlich verbrauchte Speicherplatz nur eine konstante Größe haben darf, also nicht etwa ein zweites Feld für Kopierzwecke zur Verfügung steht. Diese Aufgabe wäre sehr einfach zu lösen, wenn die beiden Sektionen gleich groß wären. In diesem Fall könnte man nämlich einfach alower mit ap tauschen, alower+1 mit ap+1, usw. Es ist also nicht sehr schwer, eine Routine swapequals zu schreiben, welche Sektionen gleicher Größe tauscht. Die Tatsache, daß ein Spezialfall unseres Problems leicht zu lösen ist, wollen wir uns bei der Konstruktion eines Algorithmus zunutze machen. Nehmen wir einmal an, die linke Sektion Sl 51 wäre größer als die rechte Sr (andernfalls läßt sich das Argument umkehren). Dann können wir sie aufspalten in zwei Teile Sl1 und Sl2, von denen der erste genauso groß ist wie Sr: Sl1 Sl2 Sr Wir tauschen nun Sl1 und Sr mithilfe der Routine swapequals und erhalten Sr Sl2 Sl1 . Da Sr nun an der richtigen Stelle steht, brauchen wir nur noch das Sl2 und Sl1 zu tauschen. Dies ist das selbe Problem wie das ursprüngliche, wobei jedoch die zu tauschenden Sektionen kleiner geworden sind. Wir können also das gleiche Verfahren erneut aufrufen und tun dies solange, bis die Sektionen gleich lang sind, was spätestens dann eintritt, wenn die Sektionen die Länge Eins erreicht haben. Damit ist die wesentliche Idee des Algorithmus – der übrigens eine sehr starke Verwandtschaft zu der Berechnung des größten gemeinsamen Teilers zweier Zahlen hat – klar. Beim vollständigen Ausprogrammieren muß man nur noch berücksichtigen, daß sich bei jedem Schritt die Feldgrenzen ändern und ebenso der Index, an dem die beiden Sektionen aneinanderstoßen. Das Beispiel illustriert die Grundidee der folgenden Strategie Entwurfsprinzip 4.5.16 (Entwurf durch Reduktion auf leichtere Probleme) Versuche, ein Problem so aufzuspalten, daß zunächst die Lösung eines bereits bekannten einfacheren Problems angewandt werden kann und der Rest eine (kleinere) Variante des ursprünglichen Problems beschreibt. Je nach Problemstellung kann “einfacher” dabei sehr verschiedene Bedeutungen haben. Im obigen Beispiel handelte es sich um einen Spezialfall, d.h. daß Problem wurde zusätzlich auf Sektionen gleicher Länge beschränkt. Im Beispiel 4.5.2 der maximalen Segmentsumme (Seite 179) hat es sich gelohnt, das Problem dahingehend zu erweitern, daß gleichzeitig die maximale Sume von Segmenten berechnet wurde, die bis zum rechten Ende des Feldes reichen – das Problem wurde dadurch leichter. Ein Problem mag einfacher werden, wenn man es zunächst verallgemeinert (einige Beschränkungen aufhebt) und aus der allgemeinen Lösung die Gesuchte als Spezialfall ansieht. Dies ist zum Beispiel der Fall, wenn man die Reihenfolge der Elemente eines Feldes a1,...,an Elementen umdrehen möchte: man schreibt zunächst eine Prozedur, welche beliebige Segmente mit Grenzen links und rechts umdreht, und wendet diese dann an auf die Grenzen 1 und n. 52 51 Bei kompliziert zu beschreibenden Sachverhalten lohnt es sich immer, Namen zur Abkürzung einzuführen und auf die genaue Definition erst dann zuzugreifen, wenn Detailfragen gelöst werden müssen. Die Beschreibung der Grundidee des Algorithmus wäre erheblich undurchsichtiger, wenn wir die Indizes die ganze Zeit mitschleppen würden. 52 Ein immer noch sehr empfehlenswertes Buch über allgemeine Methoden zum Lösen von Problemen ist [Polya, 1945].

Beispiel 4.5.15 (Vertauschen von Sektionen in Fel<strong>der</strong>n)<br />

In einem Array a mit Grenzen lower und upper sollen zwei Sektionen Sl ≡ alower,alower+1,...,ap−1<br />

und Sr ≡ ap,ap+1,...,aupper miteinan<strong>der</strong> vertauscht werden. D.h. aus dem Feld<br />

soll das Feld<br />

alower,alower+1,...,ap−1 ap,ap+1,...,aupper<br />

ap,ap+1,...,aupper alower,alower+1,...,ap−1<br />

entstehen. Da das ursprüngliche Feld sehr groß sein kann, kommt als zusätzliche Einschränkung hinzu,<br />

daß <strong>der</strong> zusätzlich verbrauchte Speicherplatz nur eine konstante Größe haben darf, also nicht etwa ein<br />

zweites Feld für Kopierzwecke zur Verfügung steht.<br />

Diese Aufgabe wäre sehr einfach zu lösen, wenn die beiden Sektionen gleich groß wären. In diesem Fall<br />

könnte man nämlich einfach alower mit ap tauschen, alower+1 mit ap+1, usw. Es ist also nicht sehr schwer,<br />

eine Routine swapequals zu schreiben, welche Sektionen gleicher Größe tauscht.<br />

Die Tatsache, daß ein Spezialfall unseres Problems leicht zu lösen ist, wollen wir uns bei <strong>der</strong> Konstruktion<br />

eines Algorithmus zunutze machen. Nehmen wir einmal an, die linke Sektion Sl 51 wäre größer als die<br />

rechte Sr (an<strong>der</strong>nfalls läßt sich das Argument umkehren). Dann können wir sie aufspalten in zwei Teile<br />

Sl1 und Sl2, von denen <strong>der</strong> erste genauso groß ist wie Sr:<br />

Sl1 Sl2 Sr<br />

Wir tauschen nun Sl1 und Sr mithilfe <strong>der</strong> Routine swapequals und erhalten<br />

Sr Sl2 Sl1 .<br />

Da Sr nun an <strong>der</strong> richtigen Stelle steht, brauchen wir nur noch das Sl2 und Sl1 zu tauschen. Dies ist das<br />

selbe Problem wie das ursprüngliche, wobei jedoch die zu tauschenden Sektionen kleiner geworden sind.<br />

Wir können also das gleiche Verfahren erneut aufrufen und tun dies solange, bis die Sektionen gleich<br />

lang sind, was spätestens dann eintritt, wenn die Sektionen die Länge Eins erreicht haben.<br />

Damit ist die wesentliche Idee des Algorithmus – <strong>der</strong> übrigens eine sehr starke Verwandtschaft zu <strong>der</strong><br />

Berechnung des größten gemeinsamen Teilers zweier Zahlen hat – klar. Beim vollständigen Ausprogrammieren<br />

muß man nur noch berücksichtigen, daß sich bei jedem Schritt die Feldgrenzen än<strong>der</strong>n und<br />

ebenso <strong>der</strong> Index, an dem die beiden Sektionen aneinan<strong>der</strong>stoßen.<br />

Das Beispiel illustriert die Grundidee <strong>der</strong> folgenden Strategie<br />

Entwurfsprinzip 4.5.16 (Entwurf durch Reduktion auf leichtere Probleme)<br />

Versuche, ein Problem so aufzuspalten, daß zunächst die Lösung eines bereits bekannten einfacheren<br />

Problems angewandt werden kann und <strong>der</strong> Rest eine (kleinere) Variante des ursprünglichen Problems<br />

beschreibt.<br />

Je nach Problemstellung kann “einfacher” dabei sehr verschiedene Bedeutungen haben. Im obigen Beispiel<br />

handelte es sich um einen Spezialfall, d.h. daß Problem wurde zusätzlich auf Sektionen gleicher Länge beschränkt.<br />

Im Beispiel 4.5.2 <strong>der</strong> maximalen Segmentsumme (Seite 179) hat es sich gelohnt, das Problem dahingehend<br />

zu erweitern, daß gleichzeitig die maximale Sume von Segmenten berechnet wurde, die bis zum rechten<br />

Ende des Feldes reichen – das Problem wurde dadurch leichter. Ein Problem mag einfacher werden, wenn man<br />

es zunächst verallgemeinert (einige Beschränkungen aufhebt) und aus <strong>der</strong> allgemeinen Lösung die Gesuchte<br />

als Spezialfall ansieht. Dies ist zum Beispiel <strong>der</strong> Fall, wenn man die Reihenfolge <strong>der</strong> Elemente eines Feldes<br />

a1,...,an Elementen umdrehen möchte: man schreibt zunächst eine Prozedur, welche beliebige Segmente<br />

mit Grenzen links und rechts umdreht, und wendet diese dann an auf die Grenzen 1 und n. 52<br />

51 Bei kompliziert zu beschreibenden Sachverhalten lohnt es sich immer, Namen zur Abkürzung einzuführen und auf die genaue<br />

Definition erst dann zuzugreifen, wenn Detailfragen gelöst werden müssen. Die Beschreibung <strong>der</strong> Grundidee des Algorithmus<br />

wäre erheblich undurchsichtiger, wenn wir die Indizes die ganze Zeit mitschleppen würden.<br />

52 Ein immer noch sehr empfehlenswertes Buch über allgemeine Methoden zum Lösen von Problemen ist [Polya, 1945].

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!