Algorithmen und Datenstrukturen Übung 17: Externes Sortieren1 Natürliches Zwei-Wege Mischsortieren Einführung. 2 geordnete Teilsequenzen mit den Längen M und N werden direkt zu einer Sequenz mit M + N Elementen gemischt. Das natürliche Mischen mischt immer längste möglichen Teilfolgen. Vereinbarung. Eine Teilsequenz X[I] .. X[J] heißt (maximaler) Lauf, wenn folgende Bedingungen erfüllt sind: 1. X[K] <= X[K + 1] 2. X[I - 1] > X[I] 3. X[J] > X[J + 1] (für K = I .. J - 1) Die natürliche Mischsortierung mischt (maximale) Läufe statt fester Sequenzen mit vorbestimmter Länge. Jede Folge von natürlichen Zahlen zerfällt in eine solche Folge, so z.B.: 5 3 2 7 10 4 1 7 3 6 8 2 geordnete Sequenzen sind dann jeweils zu einer einzigen, geordneten Sequenz zu vereinigen. Verfahrensaufwand. Er wird danach gemessen, wie oft Läufe in die Betrachtung eingehen. Läufe haben die Eigenschaft, daß beim Mischen von 2 Sequenzen mit L Läufen eine einzige Sequenz mit genau L Läufen entsteht. So ergibt sich für die folgenden beiden Sequenzen 8 7 6 5 4 3 2 1 mit jeweils 4 Läufen die Sequenz 7 8 5 6 3 4 1 2 , die genau 4 Läufe besitzt. Die Zahl der Läufe wird in jedem Durchlauf halbiert. Im schlimmsten Fall ergibt sich dann die Anzahl der Bewegungen zu: L ld(L). Der Algorithmus. Ablauf: Die zu sortierenden Daten liegen im File F vor und sollen am Schluß in sortierter Form unter demselben Namen zurückgegeben werden. Die beiden Hilfsfiles sind G1 und G2. Jeder Durchlauf umfaßt eine Verteilungsphase (distribution), die Läufe gleichmäßig von F auf G1 und G2 verteilt, und eine Mischphase, die Läufe von G1 und G2 auf F mischt. 1 vgl. Skriptum, 3.1.1.2.2 1 Algorithmen und Datenstrukturen G1 F G1 G1 F F F F ....... G2 G2 G2 Mischphase Verteilungsphase Abb.: Das Sortieren ist beendet sobald F nur noch ein Lauf ist. Für die Definition des momentanen Zustands eines Files stellt man sich am Positionszeiger vor. Er wird beim Schreiben um je eine Einheit vorwärts geschoben. Beschreibung: Sie erfolgt nach der Methode „stepwise refinement“. Grobstruktur des Prozesses: Wiederhole Setze die Zeiger aller 3 Files auf den Anfang; Verteile; Setze die Zeiger aller 3 Files auf den Anfang; Mische; bis L = 1; (* L ist die Anzahl der Läufe auf dem File F *) Verfeinerungsschritt: Verteile Wiederhole Kopiere ein Lauf F auf G1 Falls noch nicht eof(F), kopiere einen Lauf von F auf G2; bis Ende von F erreicht; Mische 2 besten einen Algorithmen und Datenstrukturen Setze L = 0; Solange weder eof(G1) noch eof(G2) fuehre aus: Mische je einen Lauf von G1 und G2 auf F; Erhoehe L um 1; Solange eof(G1) noch nicht erreicht, fuehre aus: Kopiere einen Lauf von G1 auf F; Erhöhe L um 1; Solange eof(G2) noch nicht erreicht, fuehre aus: Kopiere einen Lauf von G2 auf F; Erhöhe L um 1; Bsp.: Gegeben ist das File F, das die folgenden 17 Zahlen enthält F: 13 44 7 3 3 9 99 37 61 71 6 8 11 14 15 1 F wird gemäß bereits sortiert vorliegender Teilfolgen zerlegt: F: [13 44] [7] [3 3 9 99] [37 61 71] [2 6 8 11 14 15] [1] Die Teilfolgen (Läufe) werden verteilt auf G1 und G2: G1: [13 44] [3 3 9 99] [37 61 71][2 6 8 11 14 15] G2: [7] [37 61 71][1] Die ersten beiden Teilfolgen von G2 können als eine Teilfolge betrachtet werden. F: [13 44] [7] [3 3 9 99] [37 61 71] [2 6 8 11 14 15][1] G1: [13 44] [3 3 9 99] [2 6 8 11 14 15] G2: [7 37 61 71][1] Zusammenmischen von G1 und G2 zu F: F: [7 13 37 44 61 71] [1 3 3 9 99] [2 6 8 11 14 15] Weiteres Aufteilen (Split) und Mischen führt zu: G1: [7 13 37 44 61 71] [2 6 8 11 14 15] G2: [1 3 3 9 99] F: [1 3 3 7 9 13 37 44 61 71 99] [2 6 8 11 14 15] G1: [1 3 3 7 9 13 37 44 61 71 99] G2: [2 6 8 11 14 15] F: [1 2 3 3 6 7 8 9 11 13 14 15 37 44 61 71 99] 3 Algorithmen und Datenstrukturen Implementierung in Java2. ExternalSort SortFileStream Die Klasse ExternalSort3 implementiert das natürliche 2-Wege-Mischen. Die Klasse SortFileStream implementiert eine Klasse zur Verarbeitung einer binären Datei mit „Comparable“-Objekten. Das natürliche ExternalSort: Mischsortieren übernimmt die Methode naturalMerge() der Klasse /** * naturalMerge – implementiert den Algorithmus fuer natuerliches Mischen: * - verteilt Laeufe * - mischt * - wiederholt das bis nur noch ein einziger Lauf existiert */ public static void naturalMerge(SortFileStream inFile) throws IOException { SortFileStream fileA, fileB, sortFile; int numRuns; fileA = new SortFileStream("outfile1"); fileB = new SortFileStream("outfile2"); sortFile = new SortFileStream("outfile3"); // Initialisieren Verteilungsphase. System.out.println("\nDistribute:"); distribute(inFile, fileA, fileB); System.out.print(" File A: "); fileA.list(true); System.out.print(" File B: "); fileB.list(true); // Alternativ mische und verteile zurueck. do { System.out.print("\nMerge: "); numRuns = merge(fileA, fileB, sortFile); sortFile.list(true); if (numRuns > 1) { System.out.println( "--------------------------------------------------------------"); System.out.println("Distribute:"); distribute(sortFile, fileA, fileB); System.out.print(" File A: "); fileA.list(true); System.out.print(" File B: "); fileB.list(true); } } while (numRuns > 1); } naturalMerge() führt das natürliche Mischsortieren aus und verteilt bzw. mischt solange bis die Zahl der Läufe größer als 1 ist mit den beiden Methoden merge() und distribute(): /* * merge - mischt alle Laeufe aus fileA und fileB auf destFile, 2 Nach einer Vorlage der Harvard University, vgl.: http://www.fas.harvard.edu/~libs111/unit7/assign/ExternalSort.java 3 vgl. pr31122 4 Algorithmen und Datenstrukturen * beruecksichtigt dabei, dass die Zahl der Laeufe in den beiden * Dateien ungleich sein kann. Zaehlt und gibt die Zahl der Laeufe * zurueck in destFile. */ private static int merge(SortFileStream fileA, SortFileStream fileB, SortFileStream destFile) throws IOException { int numRuns = 0; fileA.openRead(); fileB.openRead(); destFile.openWrite(); // Mische Laeufe. while (!(fileA.endOfFile() || fileB.endOfFile())) { mergeRun(fileA, fileB, destFile); numRuns++; } // Copy tail of fileA if necessary. while (!fileA.endOfFile()) { copyRun(fileA, destFile); numRuns++; } // Copy tail of fileB if necessary. while (!fileB.endOfFile()) { copyRun(fileB, destFile); numRuns++; } destFile.close(); return numRuns; } /* * distribute – verteile alle Laeufe von der sourceFile alternately * alternierend auf fileA und fileB */ private static void distribute(SortFileStream sourceFile, SortFileStream fileA, SortFileStream fileB) throws IOException { sourceFile.openRead(); fileA.openWrite(); fileB.openWrite(); do { copyRun(sourceFile, fileA); if (!sourceFile.endOfFile()) copyRun(sourceFile, fileB); } while (!sourceFile.endOfFile()); fileA.close(); fileB.close(); } merge() und distribute() werden ergänzt durch die Methoden mergeRun(), copy() und copyRun() private static void mergeRun(SortFileStream fileA, SortFileStream fileB, SortFileStream destFile) throws IOException { // // Bis beide Dateien das Ende eines Laufs erreichen, erfolgt die . // Bearbeitung in einer Schleife. Nur wenn beide Dateien das endOfRun// Flag gesetzt haben, ist das Mischen beendet do { 5 Algorithmen und Datenstrukturen Comparable nextA = fileA.peekNextObject(); Comparable nextB = fileB.peekNextObject(); if (nextA.compareTo(nextB) < 0) { copy(fileA, destFile); if (fileA.endOfRun()) copyRun(fileB, destFile); } else { copy(fileB, destFile); if (fileB.endOfRun()) copyRun(fileA, destFile); } } while (!(fileA.endOfRun() && fileB.endOfRun())); } private static void copy(SortFileStream fRead, SortFileStream fWrite) throws IOException { fWrite.writeObject(fRead.readObject()); } private static void copyRun(SortFileStream fRead, SortFileStream fWrite) throws IOException { do { copy(fRead, fWrite); } while (! fRead.endOfRun()); } Test: 6 Algorithmen und Datenstrukturen Lösungen: /* * ExternalSort.java * * Computer Science S-111, Harvard University */ import java.io.*; // import utils.SavitchIn; /** * ExternalSort - a class containing implementations of two external * sorting algorithms--i.e., methods for sorting sequential files * without reading them into an array. */ public class ExternalSort { private static void copy(SortFileStream fRead, SortFileStream fWrite) throws IOException { fWrite.writeObject(fRead.readObject()); } private static void copyRun(SortFileStream fRead, SortFileStream fWrite) throws IOException { do { copy(fRead, fWrite); } while (! fRead.endOfRun()); } private static void mergeRun(SortFileStream fileA, SortFileStream fileB, SortFileStream destFile) throws IOException { // // Keep looping until *both* files reach the end of a run. // At the end of the first iteration, the file from which we don't // read may "falsely" have its endOfRun flag set, but we will only // find both files' flags set if the merge is actually done. // do { Comparable nextA = fileA.peekNextObject(); Comparable nextB = fileB.peekNextObject(); if (nextA.compareTo(nextB) < 0) { copy(fileA, destFile); if (fileA.endOfRun()) copyRun(fileB, destFile); } else { copy(fileB, destFile); if (fileB.endOfRun()) copyRun(fileA, destFile); } } while (!(fileA.endOfRun() && fileB.endOfRun())); } /* * distribute - distribute all runs from sourceFile alternately * into fileA and fileB */ private static void distribute(SortFileStream sourceFile, SortFileStream fileA, SortFileStream fileB) throws IOException { 7 Algorithmen und Datenstrukturen sourceFile.openRead(); fileA.openWrite(); fileB.openWrite(); do { copyRun(sourceFile, fileA); if (!sourceFile.endOfFile()) copyRun(sourceFile, fileB); } while (!sourceFile.endOfFile()); fileA.close(); fileB.close(); } /* * merge - merge all runs from fileA and fileB onto destFile, * remembering that there might be unequal numbers of runs in the * two files. Count and return the number of runs in the * resulting destFile. */ private static int merge(SortFileStream fileA, SortFileStream fileB, SortFileStream destFile) throws IOException { int numRuns = 0; fileA.openRead(); fileB.openRead(); destFile.openWrite(); // Merge runs. while (!(fileA.endOfFile() || fileB.endOfFile())) { mergeRun(fileA, fileB, destFile); numRuns++; } // Copy tail of fileA if necessary. while (!fileA.endOfFile()) { copyRun(fileA, destFile); numRuns++; } // Copy tail of fileB if necessary. while (!fileB.endOfFile()) { copyRun(fileB, destFile); numRuns++; } destFile.close(); return numRuns; } /** * naturalMerge - perform the natural merge algorithm: * - distribute runs as evenly as possible * - merge * - repeat until there's only one run */ public static void naturalMerge(SortFileStream inFile) throws IOException { SortFileStream fileA, fileB, sortFile; int numRuns; fileA = new SortFileStream("outfile1"); fileB = new SortFileStream("outfile2"); sortFile = new SortFileStream("outfile3"); // Initial distribution phase. System.out.println("\nDistribute:"); distribute(inFile, fileA, fileB); System.out.print(" File A: "); 8 Algorithmen und Datenstrukturen fileA.list(true); System.out.print(" File B: "); fileB.list(true); // Alternately merge and redistribute. do { System.out.print("\nMerge: "); numRuns = merge(fileA, fileB, sortFile); sortFile.list(true); if (numRuns > 1) { System.out.println( "--------------------------------------------------------------"); System.out.println("Distribute:"); distribute(sortFile, fileA, fileB); System.out.print(" File A: "); fileA.list(true); System.out.print(" File B: "); fileB.list(true); } } while (numRuns > 1); } /* * fibDistrib - do the initial distribution of runs according to * the Fibonacci merge algorithm */ private static void fibDistrib(SortFileStream file0, SortFileStream file1, SortFileStream file2) throws IOException { Comparable nextObj0, lastWrite1, lastWrite2; int lastFib, nextLastFib; file0.openRead(); file1.openWrite(); file2.openWrite(); // // Start with targets of 1 run for each file. // At this point, they're all dummy runs. // file1.runs = 1; file2.runs = 1; file1.dummyRuns = 1; file2.dummyRuns = 1; // // Initialize the variables used to update the targets/totals // according to the Fibonacci sequence. // nextLastFib = 0; lastFib = 1; // Distribute the runs while (!file0.endOfFile()) { // // If the current targets have both been met, increase // them by adding new dummy runs to both files. // if ((file1.dummyRuns == 0) && (file2.dummyRuns == 0)) { System.out.println("Adding " + lastFib + " dummy runs to file 1, " + nextLastFib + " to file 2"); file1.dummyRuns = lastFib; file2.dummyRuns = nextLastFib; 9 Algorithmen und Datenstrukturen file1.runs += lastFib; file2.runs += nextLastFib; // Generate next set of Fibonacci numbers. lastFib = lastFib + nextLastFib; nextLastFib = lastFib - nextLastFib; } // // Add a run to a file that hasn't met its target (i.e., // that still has dummy runs), giving preference to file1. // nextObj0 = file0.peekNextObject(); lastWrite1 = file1.getLastWritten(); lastWrite2 = file0.getLastWritten(); if (file1.dummyRuns == 0) { // Copy a run to file2. if (lastWrite2 == null || nextObj0.compareTo(lastWrite2) < 0) { // // NOT a contination - a new real run, so // we need one fewer dummy run. // file2.dummyRuns -= 1; } copyRun(file0, file2); } else { // Copy a run to file1. if (lastWrite1 == null || nextObj0.compareTo(lastWrite1) < 0) { // a new real run file1.dummyRuns -= 1; } copyRun(file0, file1); } } } /* * fibMerge - merge as many runs as there are in infile2 */ private static void fibMerge(SortFileStream infile1, SortFileStream infile2, SortFileStream outfile) throws IOException { // // infile2 should already be open for reading--because // we were reading from it during the last phase of the // algorithm, or because we opened it in fibonacci(). // infile1.openRead(); outfile.openWrite(); outfile.runs = 0; outfile.dummyRuns = 0; // Merge enough runs to use up infile2. for (int run = 1; run <= infile2.runs; run++) { outfile.runs++; infile1.runs--; if ((infile1.dummyRuns > 0) && (infile2.dummyRuns > 0)) { // "Merge" a pair of dummy runs, adding one to outfile. infile1.dummyRuns--; infile2.dummyRuns--; 10 Algorithmen und Datenstrukturen outfile.dummyRuns++; } else if (infile1.dummyRuns > 0) { // // "Merge" a dummy run with a real run from infile2, // copying the real run. // copyRun(infile2, outfile); infile1.dummyRuns--; } else if (infile2.dummyRuns > 0) { // same as above, but we copy a run from infile1 copyRun(infile1, outfile); infile2.dummyRuns--; } else { // no dummies left on either file mergeRun(infile1, infile2, outfile); } } infile2.runs = 0; outfile.close(); } /** * fibonacci - perform the Fibonacci merge algorithm: * - distribute runs so that there are consecutive fibonacci * numbers of runs in two files; * - keep merging the files, reusing a file for output as soon as * it becomes empty */ public static void fibonacci(SortFileStream infile) throws IOException { SortFileStream[] file = new SortFileStream[3]; int bigIn, smallIn, out; file[0] = new SortFileStream("outfile0"); file[1] = new SortFileStream("outfile1"); file[2] = new SortFileStream("outfile2"); // Empty this out for the purposes of the listing below. file[2].openWrite(); file[2].runs = file[2].dummyRuns = 0; // Do the initial distribution of runs and display the results. fibDistrib(infile, file[0], file[1]); System.out.println("\nfname (runs, dummy): contents"); for (int i = 0; i < 3; i++) { System.out.print("f" + i + " (" + file[i].runs + ", " + file[i].dummyRuns + "): "); file[i].list(true); } // Initialize index numbers. bigIn = 0; smallIn = 1; out = 2; file[1].openRead(); // file0 will be opened in fibMerge() do { fibMerge(file[bigIn], file[smallIn], file[out]); // Display files after the merge. System.out.println( "--------------------------------------------------------------"); for (int i = 0; i < 3; i++) { 11 Algorithmen und Datenstrukturen System.out.print("f" + i + " (" + file[i].runs + ", " + file[i].dummyRuns + "): "); if (i == out) file[i].list(true); else file[i].list(false); // list from current point to end } // // Rotate files: // - old output becomes new big input file. // - old big input becomes small input file. // - old small input, now empty, becomes output file. // bigIn = (bigIn + 2) % 3; smallIn = (smallIn + 2) % 3; out = (out + 2) % 3; } while (file[smallIn].runs > 0); } public static void main(String[] args) throws IOException { SortFileStream infile; char reply; // SortFileStream file; BufferedReader ein = new BufferedReader(new InputStreamReader(System.in)); String name; int numInt; System.out.print("file name: "); name = ein.readLine(); infile = new SortFileStream(name); if (! infile.exists()) { System.out.print("How many integers? "); numInt = /* SavitchIn.readLineInt(); */ Integer.parseInt(ein.readLine()); infile.openWrite(); for (int i = 0; i < numInt; i++) { Integer val = new Integer((int)(100 * Math.random())); infile.writeObject(val); } } else { infile.openRead(); // infile.list(true); } // infile.close(); // System.out.print("Name of input file to sort: "); // infile = new SortFileStream(SavitchIn.readLine()); System.out.println("Contents: "); infile.list(true); System.out.println(); /* System.out.print("Fibonacci merge (instead of natural merge)? [Y,N] "); reply = SavitchIn.readLineNonwhiteChar(); if (reply == 'Y' || reply == 'y') fibonacci(infile); else */ naturalMerge(infile); System.out.println(); } 12 Algorithmen und Datenstrukturen } /* * SortFileStream.java * * Computer Science S-111, Harvard University */ import java.io.*; // import utils.SavitchIn; /** * Implementation of a class for a binary sequential file * containing Comparable objects. */ public class SortFileStream { private String fname; private File file; private ObjectInputStream in; private ObjectOutputStream out; private boolean endOfRun; private boolean endOfFile; private Comparable nextObject; private Comparable lastWritten; private int objectNum; // index of the object to be returned next /** * These are used by the Fibonacci merge algorithm, so we * make them public so it can easily access them. */ public int runs; // the total/target number of runs public int dummyRuns; // the number of "dummy" runs /** constructor */ public SortFileStream(String name) { fname = name; file = new File(fname); in = null; out = null; endOfRun = false; endOfFile = false; nextObject = null; lastWritten = null; runs = dummyRuns = objectNum = 0; } public boolean exists() { return (file.exists()); } /** * openRead - open a sequential file for reading, starting * at the beginning of the file. */ public void openRead() throws IOException { // Close any existing streams attached to the file. close(); in = new ObjectInputStream(new FileInputStream(fname)); endOfFile = false; objectNum = 0; // 13 Algorithmen und Datenstrukturen // We set nextObject to null to ensure that endOfRun will // not be set in the call to readObject(). // nextObject = null; readObject(); // nextObject now contains the first value in the file. } /** * openWrite - open a sequential file for writing, truncating any * existing contents */ public void openWrite() throws IOException { close(); out = new ObjectOutputStream(new FileOutputStream(fname)); lastWritten = null; endOfFile = false; endOfRun = false; } public void close() throws IOException { if (in != null) { in.close(); in = null; } if (out != null) { out.close(); out = null; } } /** * readObject - read a value from the file--storing it in nextObject * and testing for end-of-file/end-of-run--and return the * previously read value. We stay one read ahead so that we are * able to detect the ends of runs. */ public Comparable readObject() throws IOException { Comparable obj = nextObject; try { nextObject = (Comparable)in.readObject(); endOfRun = (obj == null ? false : (nextObject.compareTo(obj) < 0)); } catch (EOFException e) { // end of file, and thus end of run endOfFile = true; endOfRun = true; } catch (ClassNotFoundException e) { System.out.println("Object read from " + fname + "is not Comparable."); System.exit(-1); } objectNum++; return obj; } /** 14 Algorithmen und Datenstrukturen * writeObject - write the specified value to the file and keep track of * the last value written */ public void writeObject(Comparable obj) throws IOException { out.writeObject(obj); lastWritten = obj; } /** * list - lists a sequential file, breaking it into runs */ public void list(boolean fromStart) throws IOException { int count = 0; int origObjectNum = objectNum; if (!fromStart && in == null) throw new IllegalStateException("fromStart = false and file isn't open"); // // Only reopen the stream if we're listing // the entire file. // if (fromStart) openRead(); // Do the actual output. while(!endOfFile) { System.out.print(readObject()); count = count + 3; // Print appropriate separator characters. if (endOfRun && !endOfFile) { System.out.print(" | "); count = count + 3; } else { System.out.print(" "); count = count + 1; } // Try to avoid wrap-around. if (count > 100) { System.out.print("\n\t"); count = 0; } } System.out.println(); // // // // // // if If we're listing the remainder of a file that's being read, we need to return it to the point in the file from which we began. We're assuming that there are no repeated values. (!fromStart) { openRead(); while (objectNum < origObjectNum) readObject(); } } public boolean endOfRun() { 15 Algorithmen und Datenstrukturen return endOfRun; } public boolean endOfFile() { return endOfFile; } public Comparable peekNextObject() { return nextObject; } public Comparable getLastWritten() { return lastWritten; } public String toString() { return fname; } /** * main - test the class */ /* public static void main(String[] args) throws IOException { SortFileStream file; BufferedReader ein = new BufferedReader(new InputStreamReader(System.in)); String name; int numInt; System.out.print("file name: "); name = ein.readLine(); file = new SortFileStream(name); if (! file.exists()) { System.out.print("How many integers? "); numInt = /* SavitchIn.readLineInt(); Integer.parseInt(ein.readLine()); file.openWrite(); for (int i = 0; i < numInt; i++) { Integer val = new Integer((int)(100 * Math.random())); file.writeObject(val); } } else { file.openRead(); file.list(true); } file.close(); } */ } 16