Effiziente Algorithmen Aufgabe 5 Gruppe E Martin Schliefnig, 0160919 Christoph Holper, 9927191 Ulrike Ritzinger, 0125779
1. Problemstellung Gegeben ist eine Datei, die eine Million reelle Zahlen enthält. Von dieser Datei kann nur sequentiell gelesen werden. Des weiteren ist zu beachten, dass man möglichst wenig Platz im Hauptspeicher benutzt; man muss mit weniger als sieben Zahlen auskommen. Die Aufgabe besteht darin, einen möglichst effizienten Algorithmus zu entwerfen, welcher unter Verwendung von möglichst wenig Platz im Hauptspeicher den Median dieser Datei findet. Die gegebene Datei befindet sich im unsortiertem Zustand, und darf nicht verändert werden. Es ist auch nicht erlaubt weitere Dateien anzulegen. Hinweis: Es gibt einen Algorithmus, der dieses Problem in Laufzeit O(n log n) löst. Der zweite Teil der Aufgabe besteht darin, einen möglichst effizienten Algorithmus zu entwerfen, der das oben genannte Problem löst, jedoch dürfen hier auch weitere (temporäre) Dateien angelegt werden. Hinweis: Hier existiert eine Lösung mit Laufzeit O(n). 2. Teil I 2.1 Herangehensweise Die Aufgabe erscheint auf den ersten Blick sehr schwierig, da man sofort ans Sortieren der Liste denkt und das mit dem kleinen Hauptspeicher bzw. dem strengen Komplexitätskriterium nicht machbar ist. Der Trick liegt dann schlussendlich aber darin, sich die Eigenschaften des Medians genauer anzusehen und alle anderen Bedingungen für eine Sortierte Liste auszulassen. Die wichtigste Eigenschaft dabei ist die Definition, dass der Median die Liste in genau zwei Teile teilt: Einen linken Teil, in dem alle Zahlen kleiner (oder gleich) dem Median und einen rechten Teil, in dem alle Zahlen größer (oder gleich) dem Median sind. Denkt man ein wenig darüber nach, so bietet sich das Arithmetische Mittel der Zahlen als eventuell ganz gute Näherung für den Median an. Zumindest aber erfüllt es folgende Eigenschaft: Es teilt die Zahlen ebenfalls in zwei Mengen, die zwar unterschiedlich groß sein können, was aber bei iterativem Vorgehen keinen wesentlichen Nachteil bringt, wie wir später sehen werden. Die Idee nutzt im Prinzip diese Annäherung und den Vorteil aus, dass die Zahlen in den Teilmengen nicht sortiert, sondern nur erkannt werden müssen. Dazu ist es ausreichend, sich eine untere und eine oberer Schranke für die jeweilige Teilmenge zu merken. 2.2 Algorithmus Der von uns ausgedachte Algorithmus funktioniert folgendermaßen: Am Beginn wird das arithmetische Mittel über alle Zahlen in der Datei gebildet. Durch dieses Mittel werden zwei Mengen gebildet: eine linke Teilmenge, welche die Zahlen kleiner, gleich dem arithmetischen Mittel beinhaltet, und eine rechte Teilmenge, in welcher sich die Zahlen befinden, die größer als das arithmetische Mittel sind. Danach wird ein Delta (= maximaler Abstand eines Schrankenwertes vom Median) bestimmt, welches zu Beginn n/2 beträgt (abgerundet). Danach wird eine dieser beider Teilmengen gewählt. Dies erfolgt nach folgendem Kriterium: Wenn Delta kleiner ist als die Anzahl der Elemente der linken Teilmenge, dann wird die linke Teilmenge zur weiteren Bearbeitung herangezogen, ansonsten wird die rechte Teilmenge gewählt. Wenn die rechte Teilmenge gewählt wird, wird zusätzlich Delta neu berechnet. Das neue Delta berechnet sich aus dem alten Delta minus der Anzahl der Elemente, die sich in der linken Teilmenge befinden. Befindet man sich in der linken Teilmenge, so wird die obere Schranke dieser Menge bestimmt und der Median ist Delta Elemente von der oberen Schranke entfernt. Ist man in der rechten Teilmenge, so wird die untere Schranke bestimmt, und der Media ist Delta Elemente von der unteren Schranke entfernt. Beide Schrankenwerte werden jeweils aktualisiert und gespeichert, um so immer die aktuelle Teilmenge bestimmen zu können. In den folgenden Iterationen ist es das Ziel, Delta zu minimieren und sich so dem Median zu nähern. Es wird immer von der neu bestimmten Teilmenge das arithmetische Mittel neu berechnet, wieder geteilt, und so weiter. Wenn Delta den Wert 0 erreicht, dann ist der Median die obere bzw. die untere Schranke (je nachdem in welcher Teilmenge man sich befindet). Zum besseren Verständnis ist ein Beispiel unter Punkt 4 gegeben.
2.3 Beispiel An dieser Stelle wird ein kleines Beispiel demonstriert, um den oben beschriebenen Algorithmus zu veranschaulichen. gegebene Datei: 7.6, 1.1, 8.0, 9.2, 1.1, 1.4, 1.2, 1.0, 7.7, 9.4, 5.8, 6.2, 7.8, 6.2, 1.3, 8.0, 8.1 Median dieser Werte: 6.2 Arithmetisches Mittel: 5.3 Delta: n/2 = 8 Linke TM: 1.1, 1.1, 1.4, 1.2, 1.0, 1.3 Rechte TM: 7.6, 8.0, 9.2, 7.7, 9.4, 5.8, 6.2, 7.8, 6.2, 8.0, 8.1 Schritt1: -> rechte Teilmenge wird betrachtet, da Delta (8) > Anzahl der Elemente der linken TM (6) neues Delta: altes Delta (8) Anzahl der Elemente der linken TM (6) = 2 Untere Schranke: 5.8 Arithmetisches Mittel der rechten Teilmenge wird berechnet: 7.64 Linke TM: 7.6, 5.8, 6.2, 6.2 Rechte TM: 8.0, 9.2, 7.7, 9.4, 7.8, 8.0, 8.1 Schritt2: -> linke Teilmenge wird betrachtet, Delta bleibt gleich (2) Obere Schranke: 7.6 Arithmetisches Mittel neu berechnen: 6.45 Linke TM: 5.8, 6.2, 6.2 Rechte TM: 7.6 Schritt3: -> linke Teilmenge wird betrachtet, Delta bleibt gleich (2) Obere Schranke: 6.2 Arithmetisches Mittel neu berechnen: 6.1 Linke TM: 5.8; Rechte TM: 6.2, 6.2 Schritt4: -> rechte Teilmenge wird betrachtet neues Delta: altes Delta(2) Anzahl der Elemente der linken TM (1) = 1 Untere Schranke: 6.2 Arithmetisches Mittel der rechten Teilmenge wird berechnet: 6.2 weil AM gleich US, Median ist 6.2 2.4 Pseudocode MEDIANBESTIMMUNG(M) { Input: unsortierte Datei mit reellen Zahlen Output: Median der Datei os = größtes Element der Teilmenge; // obere Schranke us = kleinstes Element der Teilmenge; // untere Schranke // arithmetisches Mittel li = n; // Anzahl Elemente linke Teilmenge delta = n/2; //abgerundet foo() { if (delta < li ) {
foolinks(); else { foolinks() { os = obereschranke(us, am); if (am == os) { Median = os; if (delta < li) { foolinks(); else { foorechts() { delta = delta - li; us = untereschranke(os, am); if (delta == 0 or am == us) { Median = us; else { if (delta >= li) { else { foolinks(); 3. Teil II 3.1 Herangehensweise Der 2. Teil befasst sich mit einem Algorithmus mit Laufzeit O(n), wobei man nun Dateien anlegen darf. Der vorgeschlagene Algorithmus findet das k-te Element in einer Menge und benutzt dabei auch Zufallszahlen und rekursive Aufrufe. Dabei verkleinert jeder erneute Aufruf die Problemgröße. Die dargestellte Implementierung für AWK versucht ehest nahe an die Aufgabenstellung heranzukommen, indem wenn auch aufwendig für die Partitionierungsmengen tatsächlich Files benutzt werden. Dabei wird der Pseudocode für eine Implementierung mit durchschnittlicher Laufzeit O (n) von n bzw. einer maximalen Laufzeit O (n). Hierbei kommt es vor allem auf die Auswahl des Pivot-Elements an. Wird dieses nicht zufällig bestimmt, sondern ist dieses der Median einer Teilmenge der ursprünglichen Eingabedatenmenge, so ist der Algorithmus sehr effizient. Theoretische Arbeiten über die Wahl des Pivot-Elements wurden von Floyd, Rivest, Cunto, Munro im Journal of the ACM 1989 verfasst.
3.2 Pseudocode Input: Output: A = {a1,, an Menge der Eingabezahlen k k-te Element, Median ist k/2 bzw. roundup (k/2) + 1 k-te Element FindKth (M, K) { m = zufällige Auswahl eines Elements aus M; // Durchschnittliche Laufzeit O (n) /********** m = FindKth (M, K ) Maximale Laufzeit O (n): M Teilmenge von M, K Position Median in M Auswahl der Parameter M, K entsprechend Problemstellung **********/ Partitionierung von M in S und B (S: alle Elemente < m, B: alle Elemente > m); if ( S = K 1) { return m; // Median gefunden if ( S < K 1) { FindKth (B, K - S - 1); // Median irgendwo in B else { FindKth (S, K); 3.3 AWK # awk -f effn.awk -v ITEMS=n -v K=k datfile # n items in file, k k-th element, datfile file with numbers BEGIN { system ("rm -f smlr bigr"); if (ARGC!= 2) { print "need one data file"; exit; if (ITEMS == "") { # awk -f effn.awk -v ITEMS=n -v K=k datfile print "set variable ITEMS"; exit; if (K == "") { print "set variable K"; exit; # arguments file = ARGV [1]; items = ITEMS; k = K; # init randomizer srand (); # randomly pick one element pivotnum = 1 + int (rand () * items); // random index for (i = 1; getline pivotel < file > 0 && i <= pivotnum - 1; i ++); # pivot element in pivotel { END { if ($1 < pivotel) { print $1 >> "smlr"; // file output smlr ++; else if ($1 > pivotel) { print $1 >> "bigr"; // file output bigr ++; if (smlr == k - 1) { print "k-th element = ", pivotel; else if (smlr < k - 1) { system ("mv bigr dat"); system ("rm -f smlr"); cmd = "awk -f effn.awk -v ITEMS=" bigr " -v K=" k - smlr - 1 " dat"; system (cmd); else { system ("mv smlr dat"); system ("rm -f bigr"); cmd = "awk -f effn.awk -v ITEMS=" smlr " -v K=" k " dat"; system (cmd);