Algorithmen und Datenstrukturen 1 Ausarbeitung zum Übungsblatt 1 (WS 02) Klaffenböck Patrick a.k.a. Yrucrem Freitag, 18. Oktober 2002 Version 0.6 Danke an franco für den Hinweis das eine Teilfrage bei Aufgabe 8 nicht behandelt wurde. Irish machte mich darauf aufmerksam, dass eine Teilfrage von Aufgabe 6 nicht behandelt wurde. Slater korrigierte eine Laufzeitabschätzung aus Aufgabe 3. Der LATEX-Source, sowie die neueste Version dieses Dokuments sind unter yrucrem.dr.ag/studium/algodat 1/uebung.html erhältlich. Zusammenfassung Ich veröffentliche meine Lösungen hauptsächlich, damit die Studenten eine Vergleichsmöglichkeit haben, bzw. um denjenigen - die sich mit einem Beispiel schwer tun - einen Denksanstoß zu geben. Ich versuche meine Ansätze möglichst genau zu erklären, weil ich denke, dass man dann besser darüber reden kann. Denn die Lösungen sollen eine Diskussionsbasis sein, bzw. eine Lernhilfe für diejenigen, denen der Stoff einfach nicht liegt. Deshalb freue ich mich auch, über Kritik, Hinweise auf Fehler und Fragen zu den Lösungen – kurz: wenn sich jemand mit dem Thema auseinandersetzt. Was mich weniger freut, ist wenn die Lösungen einfach abgeschrieben und die Erklärungen dazu benutzt werden, sich selbst keine Gedanken über die Fragen machen zu muessen. Aber erstens erkennen es die Tutoren trotzdem, wenn jemand nur abgeschrieben hat ohne das Thema zu verstehen und zweitens faellt einem das spätestens bei der Prüfung auf den Kopf. Aufgabe 1 Beweisen oder widerlegen Sie, dass f (n) = Θ(n4 ): ( f (n) = 0.5n4 falls n durch 3 teilbar n4 − n log n falls n nicht durch 3 teilbar Da es unendlich viele Zahlen gibt die durch drei Teilbar sind und auch unednlich viele Zahlen nicht durch drei teilbar sind muss für beide Formeln bewiesen werden, dass sie in Θ(n4 ) sind. f (n) = 12 n4 : 1 0 ≤ n4 c1 ≤ n4 ≤ n4 c2 | : n4 2 1 0 ≤ c1 ≤ ≤ c2 2 Wenn man zum Beispiel c1 = Ungleichung ∀n ≥ 1. 1 3 und c2 = 1 wählt, dann gilt die obige f (n) = n4 − n log n: 0 ≤ n4 c1 ≤ n4 − n log n ≤ n4 c2 | : n4 n4 n log n − ≤ c2 n4 n4 log n 0 ≤ c1 ≤ 1 − 3 ≤ c2 n 0 ≤ c1 ≤ n Der Ausdruck log ist konvergent gegen 0. Wenn wir also das n0 groß genug n3 wählen, lässt sich diese Funktion auch zwischen zwei Konstanten einsperren. Zum Beispiel mit: 1 2 c2 = 2 c1 = n0 = 106 1 Aufgabe 2 Beweisen oder widerlegen Sie die Behauptung f (n) = Θ(g(n)) ⇒ f (n) = O(2g(n)) Die Definitionen von Θ(n) und O(n): Θ(g(n)) = {f (n)|∃(c1 , c2 , n0 ) > 0 : 0 ≤ c1 g(n) ≤ f (n) ≤ c2 g(n)∀n ≥ n0 } O(g(n)) = {f (n)|∃(c, n0 ) > 0 : 0 ≤ f (n) ≤ cg(n)∀n ≥ n0 } Man sieht also schon aus der Definition, dass f (n) = Θ(g(n)) ⇒ f (n) = O(g(n)) gilt, denn wenn man den linken Teil der Definition von Θ(n) weglässt und in der Ungleichung von O(n) das c durch c2 ersetzt, erhält man die gleiche Ungleichung. Wenn man beide Seiten der Ungleichung durch g(n) dividiert (eigentlich reicht es, wenn man durch die höchste Potenz von n in g(n) dividiert), sieht (n) kleiner gleich einem Konstanten c sein muss für man, dass der Ausdruck fg(n) alle n ≥ n0 . Wenn man also die Behauptung auf diese Form bringt, f (n) f (n) ≤c⇒ ≤ 2c g(n) g(n) sieht man schon, dass die Behauptung stimmt – eine Konstante mal zwei ist ja wieder eine Konstante und nur das ist wichtig. Aufgabe 3 (a) Θ(n): die Schleife wird ca. 1 3 mal durchlaufen. (b) Θ(n): die innere Schleife wird beim ersten Mal n + 26 mal ausgeführt, dann n2 + 26 mal, + n4 + . . . das gibt insgesammt etwa 2n ist also in Θ(n). 2 Aufgabe 4 Wenn man die Definitionen der Notationen O, Ω und Θ (siehe oben) ins “Deutsche” ürbersetzt, erhäelt man sinngemäßfolgendes: (n) Fuer Θ: f (n) = Θ(g(n)) ⇔ man kann fg(n) zwischen zwei Konstanten c1 und c2 einschließen. Das muss nicht einmal für alle n gelten, sondern nur für alle n ≥ n0 . Wobei c1 , c2 und n0 Konstanten sind die nur größer als 0 sein müssen. Wenn man diese drei Konstanten finden kann, dann ist f (n) = Θ(g(n)). Analog sieht es bei O und Ω aus, nur dass man eben bei O die kleinere und bei Ω die größere Konstante nicht braucht. Daraus folgt auch, dass wenn eine Funkion in Θ(g(n)) ist, dann ist sie auch in O(g(n)) und in Ω(g(n)). √ Beweis fuer n3 + n = Θ(n3 ): √ n <= c2 n3 | : n3 √ n3 n 0 <= c1 <= 3 + 3 <= c2 n n 0 <= c1 n3 <= n3 + Diese Ungleichung gilt zum Beispiel fuer: c1 = 1, c2 = 2, n0 = 1 Analog kann man dann zeigen, dass n3 + √ n 6= O(n2 ). Gegeben sei nun die Funkion f wie folgt definiert: ( f (n) = 0.5n2 falls n < 1000 √ n3 + nfalls n ≥ 1000 Welche der folgenden Aussagen sind richtig und welche sind falsch? Da wir ja nur Aussagen für jene n machen müssen die größer gleich n0 sind und wenn wir n0 = 1000 setzten, müssen wir nur die Formel betrachten für n ≥ 1000. √ f = Ω(n3 ): stimmt, denn n3 +√ n = Ω(n3 ) f = O(n3 ): simmt, denn n3 + n = O(n3 ) 3 f = Θ(n3 ): stimmt auf jeden Fall wenn √ schon die ersten beiden stimmen f = O(n2 ): stimmt nicht, denn n3 − n wächst sicher schneller als als n2 . Aufgabe 5 Selection-Sort anhand dieser Folge veranschaulichen: 3, 1, 7, 6, 2, 4, 9, 5, 8. Durchgang Feld 1 3 1 7 6 2 2 1 3 7 6 2 3 1 2 7 6 3 4 1 2 3 6 7 5 1 2 3 4 7 6 1 2 3 4 5 7 1 2 3 4 5 8 1 2 3 4 5 1 2 3 4 5 4 9 5 8 4 9 5 8 4 9 5 8 4 9 5 8 6 9 5 8 6 9 7 8 6 9 7 8 6 7 9 8 6 7 8 9 Vergleiche 8 7 6 5 4 3 2 1 Vertauschungen 1 1 1 1 1 0 1 1 36 7 Die fettgeschriebenen Zahlen werden jeweils vertauscht. Bereits am Beginn des jeweiligen Durchgangs sind die unterstrichenen Zahlen schon an ihren endgültigen Positionen. Selection-Sort ist nicht stabil, weil gleich große Elemente aneinander vorbeigetauscht werden können. Zum Beispiel 3a , 5, 3b , 2. 2 ist das kleinste Element also wird es mit 3a vertauscht → 2, 5, 3b , 3a . Aufgabe 6 Welches Sortierverfahren ist bei bereits aufsteigend sortierten Daten effizienter, Insertion-Sort oder Merge-Sort? Insertion-Sort ist in diesem Fall linear (Θ(n)) und Merge-Sort ist in Θ(n log n), also ist Insertion-Sort effizienter. 4 Betrachten wir die Zahlen folge 1, 2, 3, . . . , n. Insertion-Sort vergleicht 2 mit 1, merkt dass 1 bereits kleiner ist und beginnt sofort den nächsten Schleifendurchlauf. Und das passiert bei allen n Zahlen, es gibt also n Vergleiche (keine Vertauschungen) → Θ(n). Merge-Sort teilt, auf jeden Fall das Array so lange in zwei Hälften bis es nur noch Teilarrays der länge 1 gibt → Θ(n log n). Die zweite Frage ist, welches der beiden Sortierverfahren im Average-Case effizienter ist. Im Average-Case liegt Insertion-Sort in Θ(n2 ) und Merge-Sort liegt immer in Θ(n log n), also ist Merge-Sort im Durchschnittsfall besser. Aufgabe 7 Der Algorithmus ist abhängig vom Inhalt der Eingabefolge, weil keine unnötigen Datenbewegungen vorkommen (ie. es werden keine Elemente auf sich selbst verschoben). Im Best-Case haben wir 0 Datenbewegungen und im Worst-Case Θ(n2 ) Bewegungen. Die Schlüsselvergleiche sind nicht vom Inhalt der Folge abhängig, sondern nur von der Anzahl der Elemente. Hier die Laufzeitfunktion: T (n) = nc1 + n−1 X (n − i + 1)c2 + n−1 X n2 −n 2 (n − i)c3 + i=1 i=1 X (ti ) c4 i=1 Das ergibt: n2 −n 2 X n2 + n − 2 n2 − n c2 + c3 + (ti ) c4 = Θ(n2 ) nc1 + 2 2 i=1 Es ist deswegen erlaubt die innere Schleife nur bis n − i laufen zu lassen, weil nach jedem Durchlauf der äußeren Schleife ein Element an die richtige Position gestellt wurde und da Bubble-Sort die Elemente immer vor sich herschiebt, bis es etwas größeres findet sind immer die letzten i Elemente 5 bereits sortiert. Deswegen macht es keinen Sinn mit der inneren Schleife über den Index n − i zu gehen, weil dort nichts mehr zu tun ist. Eigentlich dürfte alles bis auf die letzt Summe klar sein und die will ich jetzt kurz erklären. Nach jedem Durchlauf der Äusseren Schleife befindet sich das größte Elemente, der unsortierten Teilfolge, ganz an deren Ende (also an der richtigen Position). Das kommt davon, dass Bubblesort ein Element so lange “vor sich her schiebt” bis ein noch größeres Element gefunden wird (und dann schiebt er das vor sich her, ...). Das bedeutet, dass am Ende jedes Durchlaufes der äußeren Schleife (mit dem Schleifenzähler i) die letzten i Elemente bereits sortiert sind. Das wiederum bedeutet, dass in diesem Bereich sicher keine Verschiebungen mehr stattfinden. Sobald also die innere Schleife den Index (n − i) überschritten hat, kommen keine Verschiebungen mehr vor. Ich versuche, das kurz zu verdeutlichen. Ist die äußere Schleife in ihrem ersten Durchlauf ist i = 1, das würde bedeuten sobald die innere Schleife den Index (n − 1) passiert hat, kommen keine Verschiebungen mehr hinzu, aber (n − 1) ist sowieso das Maximum für die innere Schleife. Am Ende des Ersten Durchgangs ist das größte Element ganz am Ende des Feldes. i = 2, das heißt ab j = n − 2 gibt es für die inenre Schleife eigentlich nichts mehr zu tun, das letzte Element ist ja schon an der richtigen Stelle. Wir summieren jetzt also auf wie oft die Bedingung in Zeile 3 erfüllt sein könnte und lassen aber die Durchgänge der inneren Schleife weg, die sowieso nichts mehr bringen. Die Bedingung könnte bei jedem Durchlauf der äußeren Schleife (n − i)-mal erfüllt sein. Also beim ersten mal (n − 1)-mal, dann (n − 2)-mal, . . ., dann Pn−1 2 i = n 2−n -mal (n − (n − 2))-mal, dann (n − (n − 1))-mal. Es könnte also i=1 sein, dass wir eine Vertauschung haben. Ob wir vertauschen müssen, hängt natürlich vom Element ab, dass an der Stelle i steht. Wir müssen also an n2 −n Stellen schauen ob hier eine Vertauschung nötig ist. Das machen wir 2 2 −n Pn mit i=12 ti , wobei ti = 1 wenn an dieser Stelle getauscht werden muss und ti = 0, wenn nicht getauscht werden muss. 6 Aufgabe 8 Die Zahlenfolge 8, 2, 9, 4, 6, 0, 1, 5 mit Quicksort sortieren. Was macht Quicksort? Es wählt ein “zerteilendes” Element, das sogenannte Pivotelement und bringt alle Elemente die kleiner sind auf dessen linke Seite und alle Elemente die größer sind auf dessen rechte Seite. Dafür benutzt der Algorithmus zwei Zeigervariablen. i durchläuft das Feld von links nach rechts und bleibt bei jedem Element stehen, dass grösser oder gleich dem Pivotelement ist. j geht von rechts nach links und stopt bei jedem kleineren Element. Wenn beide Zeiger stehengeblieben sind (und i auch wirklich noch links von j ist) werden diese beiden Elemente ausgetauscht. Wenn i und j aneinander vorbeilaufen, heißt das, dass links von der Position an dem das i steht nur Elemente sind, die kleiner sind als das Pivotelement und rechts davon nur Elemente die größer oder gleich groß sind. Also tauschen wir das Pivotelement mit dem Element an der Stelle i. Nach jedem Durchgang steht ein Element genau an der Stelle an die es hingehört. Nun ruft sich Quicksort rekursiv auf um alle Elemente links beziehungsweise rechts des endgültig platzierten Elements zu sortieren. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 i 8 1 1 1 1 1 j 2 9 2 i9 2 0 2 0 2 0j 2 0 i 1 2 0 0 2 1 j i 2 1 1 2 0 1 Feld 4 6 0 4 6 0j 4j i 6 9 4 5 9 i 4 4 2 4 j i 5 6 9 6 1j 8 8 8 5 5 5 6 8 8 8j 8 8 6 9 i 9 9 9 Eine Zahlenfolge die von Quick-sort im Best-Case sortiert wird, wäre etwa: 7 1, 3, 2, 6, 5, 7, 4. Das Pivotelement halbiert jedes mal die Zahlenfolge. Dadurch tritt am öftesten der Fall ein, dass neben den Pivotelement nur noch einelementige Felder sind (die ja dann von Quick-Sort übersprungen werden. Aufgabe 9 Merge-Sort soll absteigend sortieren. Dafür muss man nur im Pseudocode von Algorithmus 4 (Merge(A, l, m, r), Seite 34 im Skriptum) das “ ≤00 ind Zeile 5 durch ein “ ≥00 ersetzten. Aufgabe 10 Warum sortiert der Algorithmus nicht jede Zahlenfolge korrekt? Wenn das größte Element an der Stelle A[l] steht, gibt es ein Problem. Zuerst wird nämlich das kleinste Element mit dem an der Stelle A[l] vertauscht, und dann wird dieses Element (weil der Algorithmus glaubt, dass dort immer noch das größte Element steht) mit A[r] vertauscht. Zum Beispiel: 5, 4, 1, 2, 3 wird zu 1, 4, 5, 2, 3 und das zu 3, 4, 5, 2, 1. Eigentlich sollte aber das kleinste ganz links und das größte ganz rechts stehen. Den Algorithmus so ändern, dass er funktioniert. Man darf nicht zuerst beide Positionen bestimmen und dann beide vertauschen, sondern man muss zuerst die Position des kleinsten Elementes bestimmen, es an den linken Rand tauschen, dann die Position des größten Elementes bestimmen und es an den rechten Rand tauschen. Man muss also nur die 4. Zeile im Pseudcode (max = ...) zwischen die beiden VertauscheZeilen schieben. 8