Entwickler-Ecke

Algorithmen, Optimierung und Assembler - Mergesort ohne extra Speicher


Heiko - Mi 01.03.06 20:56
Titel: Mergesort ohne extra Speicher
Hi @all,

wir sollen momentan eine MergeSort von der Schule aus programmieren, aber mit einer kleinen Optimierung, wozu ich bei Google etc. bisher nichts gefunden habe :(. Und zwar sollen wir kein Hilfsarray beim "mergen" (mischen) verwenden. Nur irgendwie haut das bei mir nicht hin (kommt immer irgendein Schrott raus, wo Zahlen überschreiben werden :arrow: einige Zahlen sind dementsprechend zu oft da ;) ). Seht ihr mein Denkfehler bzw. einen allgemeienen Fehler?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
var
  SortArray: array of integer;

procedure MergeSort(Left, Right: Integer);
var
  Middle: Integer;

  procedure Merge;
  var
    x, y: Integer;
    IntBackUp: Integer;
    Mid: Integer;
  begin
    x:=Left;
    Mid:=Middle;
    y:=Mid;
    while x<Mid do
    begin
      if SortArray[y]<SortArray[x] then
      begin
        IntBackUp:=SortArray[y];
        Move(SortArray[x], SortArray[x+1], (y-x)*4);
        SortArray[x]:=IntBackUp;
        inc(y);
        inc(Mid)
      end;
      inc(x)
    end;
  end;
begin
  if not (Left=Right) then
  begin
    Middle:=(Left+Right) div 2;
    MergeSort(Left, Middle);
    MergeSort(Middle+1, Right);
    Merge
  end
end;


Anmerkung zum Quelltext: Irgendwo komme ich momentan auch außerhalöb des Speicherbereiches :(. (Das problem löst sich sicherlich von alleine, wenn der Denkfehler raus ist ;) ).


Gausi - Mi 01.03.06 21:04

Schlag mich, wenn ich jetzt Müll erzähle, aber so wie ich Mergesort kenne, ist das nicht ohne erhebliche Laufzeitverlängerung möglich (der Mischvorgang würde dann nicht mehr 2N Schritte brauchen (N zum Mischen, und weitere N zum Umkopieren), sondern bis maximal N^2 viele, da man ständig das Array komplett umbauen muss).


Heiko - Mi 01.03.06 21:17

Ich weiß nicht wie Move arbeitet, aber ich glaube das er nur N braucht (kopieren und dann das in das Zeil reinschreiben). Wenn beim kopieren bissl RAM gebraucht wird stört es ja nicht, aber alle Verfahren die ich finde kopieren alles neu in RAM :(. Wir dürfen den Algo auch auf Zeigern umbasteln (wozu ich aber irgendwie nicht so richtig Lust habe), da doppelter Speicher (4*n Bytes für Daten + 4*n Bytes für die Zeiger) ;).


Gausi - Do 02.03.06 16:09

Es ist schon richtig, dass einmal Move nur N Schritte braucht. Aber das muss man bei einem Merge-Vorgang ggf. N mal wiederholen (ich rede im O-Kalkül, konstante Faktoren spielen da keine Rolle).
Von daher fällt dieses Verfahren für eine "Optimierung" von Mergesort flach - da kann man dann direkt Bubblesort nehmen.

Ich habe das Problem "Merge ohne Hilfsarray" heute mal beim Mittagessen mit einer sehr netten Doktorandin an der Uni ausdiskutiert (die hat grade die Vorlesung Info3 hinter sich, wo auch Mergesort drankam). Wir sind dann zu dem Schluss gekommen, dass Mergesort ohne Hilfsarray auch ohne signifikante Steigerung der Laufzeit möglich ist (d.h. einmal Merge bleibt in O(N)). Allerdings ist der Verwaltungsaufwand der einzelnen Indizes (man braucht ein paar mehr als beim normalen mergen, wahrscheinlich 4 oder 5), immmens hoch und erfordert auch einige zusätzliche Vergleiche. Hinzu kommt, dass häufig nicht nur 2 Werte geswappt werden müssen, sondern 3, und ab und zu die Indizes selbst vertauscht werden müssen (also das i wird zu k und umgekehrt).

Das ganze ist dann unglaublich fehleranfällig für Indizierungsfehler, unübersichtlich und praktisch unmöglich nachvollziehbar. Selbst beim durchrechnen eines kleinen Beispiels an der Tafel (bzw. auf der Tür *g*) kommt man durcheinander, weil man diverse Fallunterscheidungen hat und sich diverse Markierungen merken muss.

Trotzdem bin ich mir nach anfänglichem Zweifel zu 90% sicher, das das möglich ist. Aber der Aufwand lohnt IMHO nicht. Wenn man ein schnelles In-Situ-Verfahren haben will, nimmt man Quick- oder Heapsort, die allerdings nicht so gut eine Vorsortierung ausnutzen können.

Frag mal deinen Lehrer, ob er weiß, was er da von dir verlangt. Wenn das dein Thema für deine Facharbeit ist und du dir ne 1++ verdienen willst, dann ist das ok - ansonsten nicht :roll:.

Die Variante mit den Zeigern (d.h. anstelle der Arrays Listen zu nehmen [aber Vorsicht: Was in Delphi List heißt, ist oft eigentlich auch ein Array] ), ist natürlich eine Möglichkeit. Das Gegenargument hast du schon geliefert: Wenn man nur Integers sortieren möchte, ändert das am Speicherbedarf gar nichts - ob man nun jede Zahl doppelt speichert, oder für jede Zahl doppelten Platz benötigt - das ist Jacke wie Hose.


Heiko - Do 02.03.06 19:05

Danke Gausie für die Zusatzinformationen (deinen Beitrag werde ich mal meinem Infolehrer zeigen, denn ich stimme dir voll zu, dass diese Optimierung ohne/mit Zeigern eigentlich sinnlos ist :mrgreen: )

Heute im Mittagsband habe ichs dann hinbekommen :) .


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
procedure MergeSort(Left, Right: Integer);
var
  Middle: Integer;

  procedure Merge;
  var
    L: Integer;
    IntBackUp: Integer;
    Mid: Integer;
  begin
    Mid:=Middle+1;
    L:=Left;
    while (L<Mid) and (Mid<=Right) do
    begin
      if (SortArray[L]>SortArray[Mid]) then
      begin
        IntBackUp:=SortArray[Mid];
        Move(SortArray[L], SortArray[L+1], (Mid-L)*4);
        SortArray[L]:=IntBackUp;
        inc(Mid);
      end;
      inc(L)
    end
  end;
begin
  if Left<Right then
  begin
    Middle:=(Left+Right) div 2;
    MergeSort(Left, Middle);
    MergeSort(Middle+1, Right);
    Merge
  end
end;


Horst_H - Fr 03.03.06 09:08

Hallo,

hast Du mal einen Zaehler eingebaut, wieviele Byte da verschoben werden.
Also die Summe

Delphi-Quelltext
1:
2:
3:
Move(SortArray[L], SortArray[L+1], (Mid-L)*4);   
Summ := summ+(Mid-L)
SortArray[L]:=IntBackUp;

Wenn die Daten genau umgekehrt sortiert sind, ist Mid-L= konstant= halbe Abschnittbreite und das (Mid-L)-fach, also (Mid-l)^2 Einzelwert Kopierungen, das ist ja wohl nicht im Sinne des Erfinders.
Den Fall kann man einfach testen:

Delphi-Quelltext
1:
2:
If(SortArray[L]> SortArray[R]) then
  Daten sind umgekehrt.


Gruss Horst
P.S.
Ich mit mergesort unter Punkt c aus http://www.inf.fh-flensburg.de/lang/algorithmen/sortieren/merge/merge.htm sehr gute Erfahrungen gemacht, da hier nur die halbe Zeigerliste kopiert werden muss und weniger Fallunterscheidungen noetig sind.
Insbesondere bei Zeigerlisten auf groessere Strukturen(>10 Byte ;-) ) und Millionen von Datensaetzen ist es von Vorteil, das Mergesort zu Beginn immer im Speicher sehr nahe Elemente vergleicht, waehrend Quicksort die Daten schon sofort sehr durcheinanderwirbelt, sodass die Vergleiche entsprechend lange auf den Hauptspeicher warten muessen.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
program untitled;
{$APPTYPE CONSOLE}
uses
  sysutils;

type
  TPointerList = array of Pointer;
  TCompFunc = function(A,B:Pointer):integer;
  //Hier wird die Pointerliste direkt fuer Integer Werte missbraucht

function intComp(A,B:Pointer):integer;
begin
  result := (integer(A)-integer(B));
end;

procedure mergesort(lks,rts:integer;a: TPointerList;CompFunc:TCompFunc);
var
  B : TPointerlist;
  procedure merge(links,rechts:integer);
  var
    i,j,k,mid :integer;

    procedure InsertSort;
    var
      i: integer;
      Pivot : Pointer;
    begin
      for i:=links+1 to rechts do
        begin
        j :=i;
        Pivot := A[j]; // elemente merken
        while (j>links) AND (CompFunc(A[j-1],Pivot)>0do
          begin  // pruefen, ob das gemerkte element kleiner als das betrachtete ist
          A[j] := A[j-1];// und gegebenenfalls austauschen
          dec(j);
          end;
        A[j] :=Pivot;// s.o.
        end;
     end;

  begin
    If rechts-links<=4 then
      //by kleinen Abschnitten Mengen insertion sort
      InsertSort
    else
      begin
      // Mittleres Element bestimmen und rekursiver Abstieg
      mid := (rechts+links) div 2;
      merge(links, mid);
      merge(mid+1, rechts);
      //schon sortiert?
      IF CompFunc(A[Mid],A[Mid+1])<0 then
        exit;
      // Mischen der sortierten Unterfelder
      // dabei wird ein Hilfsfeld b verwendet
      // Daten in Hilfsfeld kopieren
      move(A[links],B[0],(mid-links+1)*SizeOf(Pointer));
      i := 0;
      j := mid+1;
      k := links;
      while (k<j) AND (j<=Rechts) do
        begin
        IF CompFunc(B[i],A[j])<=0 then
          begin
          A[k] := B[i];
          inc(i);
          end
        else
          begin
          A[k]:= A[j];
          inc(j);
          end;
        inc(k);
        end;

      while (k<j) do
        begin
        A[k] := B[i];
        inc(i);
        inc(k);
        end;
    end;   // Ende von "if" (Abbruchbedinung)
  end;     // Ende von "procedure"
begin
  setlength(B,((rts-lks)+1)div 2);
  merge(lks,rts);
  setlength(B,0);
end;

Var
  T0,T1: TDateTime;
  A : TPointerlist;
  i,k : integer;
Begin
  randomize;
  k := 10000000;
  setlength(A,K);
  For i := 0 to K-1 do
    begin
    A[i]:= Pointer(random(k));
    end;

  T0:= time;
  mergesort(0,High(A),A,intComp);
  T1 := time;
  Writeln( FormatDateTime('hh:mm:ss.zzz',T1-t0));
  readln;
end.

EDIT: Eine hoffentlich funktioniernde Version.
Mit k = 10.000.000 dauert das natürlich ein paar Sekunden, auch schon wegen der Zufallszahlen


Gausi - Fr 03.03.06 17:26

Auch wenns vielleicht nicht mehr unbedingt interessiert: Das Verfahren, was ich gestern im Kopf hatte, funktioniert so nicht. Ich war heute nochmal Mittagessen, und da haben wir das weiter diskutiert.
Die Grundidee des Verfahrens ist, dass man den Platz, wo Elemente aus dem rechten Teilarray nach vorne verschoben werden, zur Speicherung von Elementen des linken Teilarrays nutzt. Damit die Vorsortierung nicht verloren geht, muss man dafür das rechte Teilarray splitten, sodass das Gesamt-Array sich im Verlauf so aufteilt:

Quelltext
1:
 |--X--|----L2----|----R1----|---L1---|---R2--|                    
Dabei ist X das bereits sortierte Teilstück, L1--L2 der Rest des linken sortierten Arrays (Achtung: verkehrte Reihenfolge!), und R1--R2 das rechte Teilarray. Die "Vergleichs-Zeiger" stehen dabei am Anfang von L1 bzw. R1.
Man bekommt nun ein Problem, wenn R1[1] nach vorne geschoben muss. Dann muss man entweder einen der Blöcke komplett verschieben (L2), oder aber je zwei der Blöcke rekursiv mergen, sodass man wieder bei der Ursprungssituation

Quelltext
1:
|--XX--|--L--|--R--|                    
ist, und man die Blöcke wieder neu aufteilen kann. Wir vermuten, dass man das mit Hilfe einer amortisierten Laufzeitabschätzung auf Linearzeit runterdiskutieren kann (zumindest im mittleren Fall, im Worst-Case habe ich noch leichte Zweifel). Es scheint sinnvoll zu sein, bei "kleinen Blöcken" eine Verschiebung vorzunehmen, und erst bei größeren Blöcken rekursiv zu mergen. Wo da die optimale Grenze liegt, dürfte aus der Laufzeitanalyse hervorgehen.
Das aber wiederrum war uns dann zu blöd, und haben uns lieber über das Wetter unterhalten :lol:. Amortisierte Laufzeitabschätzung ist aber auch irgendwie "bäh".

Und mir kam dabei ein Zitat aus StarTrek in den Kopf. Ich glaube, es kommt aus der Folge, in der die Sheliak-Corporation einen Planeten besiedeln will, auf der widerrechtlich eine Menschen-Kolonie ist, wo man aber nicht beamen kann.
Geordi LaForge meint dann gegen Ende:
Captain, wir können es schaffen. Wir brauchen nur drei Jahre Entwicklungszeit und ca. 150 Techniker und Ingenieure, aber wir können es schaffen...


uall@ogc - Fr 03.03.06 18:10


Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
1. einfache annahme:

1 3 4 5 | 6 7 8 9
z1      | z2

z1^ und z2^ vergleichen da z1^ < z2^ immer inc(z1)
bei z1 = z2 dann ende

Resultat:

1 3 4 5 | 6 7 8 9
          z1
          z2
_______________________

2. annahme (bsp):

1 3 4 8 | 2 2 2 2
z1        z2

1. schritt
z1^ < z2^ -> inc (z1)

1 3 4 8 | 2 2 2 2
  z1      z2

2. schritt
z1^ > z2^ -> mov z1^ <> z2^, inc(z1), inc(z2)

1 2 4 8 | 3 2 2 2
    z1      z2

wiederhole bis z1 = z2


(ich führ das mal vor)

1 2 2 8 | 3 4 2 2
      z1      z2

1 2 2 2 | 3 4 8 2
          z1    z2

1 2 2 2 | 2 4 8 3
            z1  z2

1 2 2 2 | 2 3 8 4
              z1z2

1 2 2 2 | 2 3 4 8
                z1
                z2 (ende)

wenn z2 = max dann z2 nicht mehr erhöhen!


O(n) da z1 maximal n schritte hat
aber wenn sortiert dann O(n/2) also auch o(n)

Einwände? Fehler? (Wenn ja mit Bsp wo es nicht funzt)


uall@ogc - Fr 03.03.06 18:28


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
procedure merge(list: array of integer; min, max: integer);
var i, j, a: integer;
begin
  i := min;
  j := max div 2;
  while i < j do
  begin
    if (list[i]) > list[j]) then
    begin
      a := list[i];
      list[i] := list[j];
      list[j] := a;
      if (j < max) then inc(j);
    end;
    inc(i);
  end;
end;


ungetestet

list ist z.b. ein array: 1 2 3 4 | 2 2 2 9 | 3 3 3 3 | 1 2 7 8
wobei 2 mal aufgerufen wird mit

merge(list,0,8);
merge(list,9,17);

wenn low(list) = 0

für den Mergesort dann in etwa so


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
procedure mergesort(list: array of integer);
var : integer;
begin
  i := 2;
  while i < high(list) do
  begin
    for j := 0 to high(list) div i do
      merge(list,j*i,j*i+i);
    i := i*2;
  end
end;



Ungetestet und würde im Moment auch nur so mit Listen der größe n^2 funktionieren.
Hab leider kein funktionierendes Delphi sonst würd ich des mal selbst testen.


Gausi - Fr 03.03.06 18:37


Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
     20 30 70 90 |   1    2   50   60 61 62 63
     --             --

-->   1 30 70 90 |  20 |  2   50   60 ...
                    --   --

-->   1  2 70 90 |  20   30 | 50   60 ..
                    --        --

-->   1  2 20 90 |  70   30 | 50   60  FEHLER
Abhilfe: Rechtes Array aufsplitten
-->   1  2 20 90 | 50 |  30   70 | 60 61...
                   --    --
Bis dahin ist das jede einzelne Zeile in konstanter Zeit durchführbar. Aber hier bekommt man ggf. später Probleme. Dafür Beispiele zu finden, ist aber etwas komplizierter, die werden dann etwas länger und unübersichtlich.
Das nur mit Code und Bildern zu erklären, ist zu umständlich, das muss man eher an der Tafel vorführen. Aber es geht nicht so ohne weiteres - das garantiere ich dir ;-)


uall@ogc - Fr 03.03.06 19:02

Hast recht habs nochmal eben getestet.

Kommt man auf ein Laufzeitverhalten im WorstCase von O(n^2 / 4) [O(n^2)]und BestCase O(n/2) [O(n)]


Gausi - Fr 03.03.06 19:13

user profile iconuall@ogc hat folgendes geschrieben:
Kommt man auf ein Laufzeitverhalten im WorstCase von O(n^2 / 4)
Die Schritte, die ich in meinem Beispiel durchgeführt habe, gehen insgesamt in O(N). Ab und an wird ein rekursiver Merge nötig, mit einem wesentlich kleinerem Array. Meine (und Antjes) Vermutung ist, dass man durch eine geeignete "Kontostandsfunktion" genug Zeit bei den einfachen Operationen "ansparen " kann, so dass dieses Merge dann davon "bezahlt" werden kann, um innerhalb von O(N) zu bleiben.
Das basiert auf keiner exakter Analyse, sondern auf Bauchgefühl, kann also auch gut verkehrt sein.

Aber das bewegt sich weit abseits des Schulniveaus...


uall@ogc - Fr 03.03.06 19:19

Fragt man sich nun ob im rechten Teil nicht noch kompliziertere Sachen auftreten können (d.h. mehrmals unterteilt)
für dne linkten Teil braucht man O(n/2), um den Rechten (ggf) wieder herzustellen, reicht ein simpler Bubblesort der in dem Fall immer einen bestCase von O(n/2) hat.
Das liegt daran, dass das Element im schlimmsten Fall weietr nach hinten verschoben werden muss.

Du musst bei derin Methode halt ziemlich viel merken. Und bei einer weiteren Splittung (die ja eventl auftreten kann, bei 4er Pärchen merkt man das ja nicht) funkioniert es nicht und du musst ebenfalls (wie shcon gesagt) einen Rekursiven Merge machen


Gausi - Fr 03.03.06 19:39

Doch, man kann merken, wann eine weitere Splittung des rechten Teils nötig wird: Wenn bereits gesplittet ist, und das kleinste Element des rechten Arrays kleiner ist als das Linke. Dann muss man entweder das linke Linke eins nach rechts verschieben (wenn da wenig Elemente drin sind), oder halt die 4 Teilblöcke zu 2 mergen.
Ich kann leider nicht auf Anhieb genau abschätzen, inwieweit durch das neue Mergen die Vorsortierung vor- oder nachteilhaft für den Gesamtmerge wird (ich investiere da auch keine Energie rein, da es nichts bringt ;-))

BubbleSort etc. braucht man imho nicht, da das Verfahren die Vorsortierung in den maximal 4 Teilblöcken erhält.


Heiko - Fr 03.03.06 20:25

user profile iconGausi hat folgendes geschrieben:
Aber das bewegt sich weit abseits des Schulniveaus...


Das stört micht nicht, im Gegenteil. Was wir in der Schule machen langweilt mich meistens eher :gaehn: (zu mindestens von der Praxis her).

@Bubbelsort: Eine Bubbelsort verschlechtert doch die Performance eigentlich in der Mergesort erheblich, da dadurch der Vorteil der vorsortierer wegfällt. Oder habe ich einen part bei euch gerade vergessen? :gruebel: (schnelleres als Mergesort habe ich eigentlich bisher noch nicht gemacht, wodurch ich nicht alles beurteilen kann ;) ).


Gausi - Fr 03.03.06 20:35

user profile iconHeiko hat folgendes geschrieben:
@Bubbelsort: Eine Bubbelsort verschlechtert doch die Performance eigentlich in der Mergesort erheblich, da dadurch der Vorteil der vorsortierer wegfällt. Oder habe ich einen part bei euch gerade vergessen? :gruebel: (schnelleres als Mergesort habe ich eigentlich bisher noch nicht gemacht, wodurch ich nicht alles beurteilen kann ;) ).


Man kann schnell sortieren, und langsam. In aller Regel langsam (d.h. im Mittel eine Laufzeit von N^2) sind Sortieren durch Auswahl, Sortieren durch Einfügen, Bubblesort, Shakersort. Schnelle Varianten (d.h. im Mittel log(N) * N Laufzeit) sind Mergesort in den unterschiedlichen Varianten, Heapsort und Quicksort. Letzterer ist besonders interssant, da er im schlechtesten Fall genauso lange wie Bubblesort braucht, aber in der Praxis sich als der schnellste herausgestellt hat.

Es gibt auch Verfahren, die in Linearzeit sortieren (Bucketsort), aber diese Verfahren arbeiten dann unter anderen Voraussetzungen (nicht nur "a < b", sondern greifen z.B. auf die einzelnen Ziffern der Zahl zu).

Von daher stimmt deine Aussage, dass es prinzipiell nichts (prinzipiell) schnelleres als Mergesort gibt.

Was ich hier mit diskutiert habe ist, ob es eine Möglichkeit gibt, den Nachteil des zusätzlichen Speicherbedarfs von Mergesort auszubügeln, ohne den Geschwindigkeitsvorteil einzubüßen, was bei deiner Lösung mit Move leider der Fall ist. Es scheint zu gehen, ist aber recht kompliziert und lohnt nicht wirklich (das StarTrek-Zitat trifft es ganz gut, finde ich).


Delphi-Laie - Di 08.09.09 14:55

user profile iconHeiko hat folgendes geschrieben Zum zitierten Posting springen:
MergeSort [...] mit einer kleinen Optimierung, wozu ich bei Google etc. bisher nichts gefunden habe : [...] kein Hilfsarray beim "mergen" (mischen) verwenden.


3,5 Jahre nach dem Originalbeitrag hat sich inzwischen etwas getan: Es findet sich im Internet ein Mergesort eines Österreichers Siegfried Sielaff, das er Swapsort nennt und diese Bedingung zu erfüllen scheint. Der Autor beschreibt einleitend über mehrere Absätze (s)ein Verfahren, was eigentlich im wesentlichen die Grundidee von Mergesort ist. Setzt man das Ockhamsche Rasiermesser an, dann bleibt das "in-place"-Vermischen als wirklich seine Leistung dazu übrig. Der Algorithmus ist allerdings nicht ganz trivial und fragil: Wehe, wenn nur eine Variable nicht richtig gesetzt/begrenzt wird. Die Grundidee habe ich verstanden, jedoch das, was er dazu als Algorithmus anbietet, in/mit Delphi bisher noch nicht zum Laufen gebracht. Vielleicht versuche ich es mit dem eigenen Schreiben. Gerade beim Mischen vieler, kleiner Teilarrays, was beim Mergesort ja weit überwiegend passiert, wird der Algorithmus ziemlich fummelig und rechenintensiv (Gausi führte dazu auch schon einiges aus, was ich anhand erster Erfahrungen dazu nur bestätigen kann), so daß ich - im Gegensatz zum Autor, der das natürlich im rosigen Licht verkauft - nicht so recht an eine Laufzeitgleichwertigkeit mit dem "echten" Mergesort glauben mag.


Gausi - Di 08.09.09 15:15

Dazu möchte ich auf eine Arbeit von 1988 verweisen, auf die ich vor ein paar Monaten gestoßen bin:

http://portal.acm.org/citation.cfm?id=42392.42403

Nennt sich "Practical In-Place Merging". Vorgestellt wird da schon ein Linearzeit-Mischen auf konstantem Platz, was durchaus praktikabel ist. Getestet habe ich das nicht, das Verfahren ist auch nicht "mal eben so runterprogrammiert", aber auch nicht furchtbar komplex. Wie die Arbeit von Herrn Sielaff dazu einzuordnen ist, weiß ich nicht. Aber: wie man effizient (d.h. Linearzeit) auf konstantem Platz mergen kann, ist jetzt nicht soo neu. (Die Arbeit kannte ich 2006 aber noch nicht. )

(Der Link ist normalerweise kostenpflichtig. Ob es die Arbeit woanders kostenfrei gibt, weiß ich nicht - aus universitären Netzen hat man in der Regel Zugriff darauf. ;-))


Delphi-Laie - So 27.09.09 17:24

Peter Weigel stellte auf (s)einer Internetseite http://www.sortieralgorithmen.de und, nachdem er sie vor wenigen Jahren abgab und diese seit Monaten nicht mehr erreichbar ist, dankenswerterweise nunmehr wieder unter http://home.arcor.de/peter-weigel/sortieralgorithmen/bitonicsort u.a. ein Verfahren vor, daß er Bitonic Sort nennt (nicht zu verwechseln mit Bitonic Mergesort, einem "echten", also "out-of-place"-Mergesort), das (rekursiv, und zwar "depth first") "in place" mischt. Großer Nachteil: Es funktioniert nur mit Elementeanzahlen, die gleich einer Zweierpotenz sind.


Gausi - So 27.09.09 17:36

Hm, Bitonicsort kenne ich eigentlich nur als paralleles Sortierverfahren in Sortiernetzen. Da wird der Algorithmus quasi in Hardware gegossen. Damit ist dann sortieren schneller als O(n) möglich. Wenn man die Dinger sequentiell nachbaut, kann evtl. auch was nettes bei rauskommen - hier wohl O(n log(n)^2). :nixweiss:

http://en.wikipedia.org/wiki/Sorting_network
http://en.wikipedia.org/wiki/Bitonic_sorter


Delphi-Laie - Mi 30.09.09 22:55

So, Gausi und alle anderen, ich habe es im zweiten Anlauf hinbekommen :D (eine Arbeit über mehrere Stunden, das meiste davon natürlich Fehlersuche): Verschmelzen ohne zusätzlichen Speicher, mit Ausnahme eines (einzigen!) Variablenspeicherplatzes für das Tauschen, das ja eigentlich ein zyklisches bzw. ringförmiges Verschieben bzw. Tauschen ist.

Verwandt habe ich letztlich die Idee des von ihm so getauften Swapsortes von Siegfried Sielaff. Entweder, ich kann C(++/#) zu schlecht lesen, oder das, was er anbietet, funktioniert so nicht (natürlich nehme ich ersteres an).

Und, Gausi, Du hast insofern völlig recht, es ist ein elend fragile Indizierungsfummelei, aber ein Computer kommt schließlich nicht durcheinander. :wink: Wenn ich an Smoothsort, AVL-Sort oder B-Sort denke, so ist ein Mergesort mit (rekursivem) "In-Place-Mergen" noch relativ überschaubar. Und es ist allein von der „gefühlten“ Laufzeit her keinesfalls elementar bzw. quadratisch, sondern eindeutig schneller (Bitonicsort schafft es ja auch asymptotisch optimal, auch die graphische Veranschaulichung läßt die Ähnlichkeit mit Bitonicsort erkennen).

Hier mein bei mir durchaus funktionierender Quelltext für das eigentliche Mergen eines Arrays, aus zwei miteinander zu verschmelzenden Teilarrays (a-b, c-d) bestehend:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
procedure swapmerge(a,b,c,d:word);
var lauf,z:word;

begin
z:=0;
while (b-z>=a) and (c+z<=d) and (Menge[b-z]>Menge[c+z]) do inc(z);//Ermittlung der Größe der zu swappenden Teil-Teilarrays
for lauf:=1 to z do tausche(b-(z-lauf),c+pred(lauf));           //Swap
{Erläuterung: Mit tausche wird eine externe Prozedur zum Zwecke des Tauschens (wer hätte das gedacht) aufgerufen,
die die Indizes der zu tauschenden Elemente der zu sortierenden Menge übergeben bekommt; es werden demnach
Menge[b-(z-lauf)] und Menge[c+pred(lauf)] miteinander ver-/ausgetauscht.}


if z>0 then
  begin
  if a<=b-z then swapmerge(a,b-z,succ(b-z),b);
  if c+z<=d then swapmerge(c,pred(c+z),c+z,d)
  end
end;


Mehr ist das eigentliche Swapmergen nicht und damit im Code verblüffend simpler als das, was ich auf Herrn Sielaffs Internetseite dazu fand.

Heiko, ich bin durchaus auch dafür, Abiturienten zu fordern, aber das, was Dein Lehrer dort erwartet, ja womöglich gefordert hatte, war m.E. eindeutig eine Überforderung. Sollte er tatsächlich von „einer kleinen Optimierung“ gesprochen haben, so finde ich das fast schon ein wenig zynisch. Vielleicht bringt das mal jemand ihm schonend bei....


Delphi-Laie - Fr 02.10.09 15:56

Und noch eins drauf: Mit der rekursiven Lösung gab ich mich nicht zufrieden. Die Rekursion (nebenbei bemerkt: sie ist eine Selbstähnlichkeit und ein Selbstbezug im Bereich der Algorithmen, der auf der internenen Ähnlichkeiten, auf Wiederholungen in deren Ablaufprozessen beruht) ist zwar elegant und führt zu kürzeren, oft überschaubareren Quelltexten, doch sie hat andere Nachteile (Stack(speicher)verbrauch inkl. Gefahr des Überlaufes, geringere Geschwindigkeit). Nur als intellektuell anspruchsvolle Herausforderung, Alternativen zu ersinnen, ist sie mir willkommen.

Nach der Idee Robert Sedgewicks (zumindest seine Veröffentlichung in seinem Hauptwerk) analog seinem Quicksort gestaltete ich die obige Prozedur iterativ um, wobei ein eigens dafür genutzter Vektor (Array) namens Stack letztlich den Stack emuliert und der deshalb als Namenspatron herhalten mußte, jedoch deutlich weniger (Haupt-)Speicher, als was vom Stack benutzt würde, verbrauchen und zudem laufzeitgünstiger sein dürfte:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
procedure swapmerge(a,b,c,d:word);
var lauf,n,z:word;
begin
n:=4;
  repeat
  z:=0;
  while (b-z>=a) and (c+z<=d) and (Menge[b-z]>Menge[c+z]) do inc(z);//Ermittlung der Größe der zu swappenden Teil-Teilarrays
  for lauf:=1 to z do tausche(b-(z-lauf),c+pred(lauf));//Swap
  if z>0 then
    begin
    {Die rekursive Variante dieser Funktion kommt ohne die Variable n, die äußere
    (repeat-until-)Schleife und den emulierten Stack aus, benutzt dafür aber an
    folgender Stelle die rekursiven Aufrufe:}

    //if a<=b-z then swapmerge(a,b-z,succ(b-z),b);
    //if c+z<=d then swapmerge(c,pred(c+z),c+z,d);
    if a<=b-z then
      begin
      stack[n]:=a;
      stack[succ(n)]:=b-z;
      stack[n+2]:=succ(b-z);
      stack[n+3]:=b;
      inc(n,4)
      end;
    if c+z<=d then
      begin
      stack[n]:=c;
      stack[succ(n)]:=pred(c+z);
      stack[n+2]:=c+z;
      stack[n+3]:=d;
      inc(n,4)
      end
    end
  else //z>0
    begin
    dec(n,4);
    a:=stack[n];
    b:=stack[succ(n)];
    c:=stack[n+2];
    d:=stack[n+3]
    end
  until n=0
end;


Diese Prozedur ermöglicht einen Sortieralgorithmus, der bei dem heutigen Softwareniveau (meine ich positiv!) als optimal angesehen werden kann:

- allgemeingültig i.d.S., daß alle Werte (und eben nicht nur integre) können sortiert werden,
- stabil (d.h., wertgleiche Elemente werden nicht vertauscht, deren ursprüngliche Reihenfolge bleibt also im Verlaufe der Sortierung enthalten),
- laufzeit- bzw. geschwindigkeitsoptimal (n*log(n)),
- "intelligent" genug, (vor-)sortierte Mengen zu "erkennen", indem er darauf mit einer Laufzeitverkürzung "reagiert",
- nur iterativ, keine Rekursion und
- (fast) kein zusätzlicher Speicher wird benötigt, insbesondere der etwas heikle Stack wird geschont.

All' diese Eigenschaften besitzt das Natural Mergesort mit dem In-Place-Mischen mit dieser Prozedur.


Horst_H - Sa 03.10.09 12:23

Hallo,

kannst Du mal ein minimales funktionierdes Beispielprogramm posten?
Zum Beispiel 100.000 Zufalsszahlen sortieren.Irgendwie habe ich heute ein Brett vorm Kopf. Bei mir wird nichts sortiert.

Das hier funktioniert überhaupt nicht:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
program test;
{$Apptype console}
 
uses  
  sysutils,math;
  
type
  tSorttype = integer;

var
  i:integer;
  tmp: tSorttype;
  Menge : array[1..16of tSorttype;
{
procedure tausche (wer,mitwem:word); inline;
begin
   tmp:= Menge[wer];
  Menge[wer]:= Menge[mitwem];
  Menge[mitwem] := tmp;
end;
}

procedure swapmerge(a,b,c,d:integer);
var lauf,n,z:integer;
    stack : array of integer;
begin
n:=4;
z :=round(ln((MAX(b-a,d-c))/ln(2))+1)+1;
setlength(stack,4*z);
writeln(#10#13,'StackSize= ',z);

repeat
  z:=0;
  writeln('Mische a-b: ',a,'..',b,' mit c-d ',c,'..',d);
  while (b-z>=a) and (c+z<=d) and (Menge[b-z]>Menge[c+z]) do 
    inc(z);//Ermittlung der Größe der zu swappenden Teil-Teilarrays
    writeln('z = ',z);
  for lauf:=1 to z do 
    begin
    //tausche(b-(z-lauf),c+pred(lauf));//Swap
    tmp:= Menge[b-(z-lauf)];
    Menge[b-(z-lauf)]:= Menge[c+pred(lauf)];
    Menge[c+pred(lauf)] := tmp;
    end;
    
  if z>0 then
    begin
    {Die rekursive Variante dieser Funktion kommt ohne die Variable n, die äußere
    (repeat-until-)Schleife und den emulierten Stack aus, benutzt dafür aber an
    folgender Stelle die rekursiven Aufrufe:}

    //if a<=b-z then swapmerge(a,b-z,succ(b-z),b);
    //if c+z<=d then swapmerge(c,pred(c+z),c+z,d);
    if a<=b-z then
      begin
      stack[n]:=a;
      stack[n+1]:=b-z;
      stack[n+2]:=succ(b-z);
      stack[n+3]:=b;
      inc(n,4)
      end;
    if c+z<=d then
      begin
      stack[n]:=c;
      stack[n+1]:=pred(c+z);
      stack[n+2]:=c+z;
      stack[n+3]:=d;
      inc(n,4)
      end
    end
  else //z>0
    begin
    dec(n,4);
    a:=stack[n];
    b:=stack[succ(n)];
    c:=stack[n+2];
    d:=stack[n+3]
    end;
  writeln('StackPos ',n div 4);
  until n=0;
setlength(stack,0);  
end;

Begin
//gleiche Startbedingungen
randseed:= 0;
writeln(Low(Menge):10,High(Menge));
For i := low(Menge) to High(Menge) do
  begin
  Menge[i]:= random(High(tSortType));
  write(Menge[i]:8);
  if i AND 7 = 0 then
    writeln;
  end;
swapmerge(1,8,9,16);
writeln;
For i := low(Menge) to High(Menge) do
  begin
  write(Menge[i]:8);
  if i AND 7 = 0 then
    writeln;
  end;
end.


Gruß Horst


Delphi-Laie - So 04.10.09 11:31

user profile iconHorst_H hat folgendes geschrieben Zum zitierten Posting springen:
Hallo,

kannst Du mal ein minimales funktionierdes Beispielprogramm posten?
Zum Beispiel 100.000 Zufalsszahlen sortieren.Irgendwie habe ich heute ein Brett vorm Kopf. Bei mir wird nichts sortiert.


Jetzt nicht, aber demnächst (irgendwann innerhalb der nächsten Woche), mein Sortierkinoprogramm, daran gibt es nur noch wenige Dinge zu erledigen. Wird in drei Varianten veröffentlicht werden, natürlich wie immer inkl. kompletter Quelltexte.

Ich bin noch nicht ganz mit den zwei Varianten ab Delphi 4 damit fertig, nur die mit statischer Arraydimensionieren, bei der die Bildschirmkoordinaten im Quelltext ziemlich weit oben entweder gelassen oder kleiner als 1600x1200 eingestellt werden können, bei noch größerer Bildschirmauflösung jedoch nach oben angepaßt werden sollten (oder kleinere Fenstergröße wählen).

Ganz davon abgesehen, ziehe ich Deine Arraydimensionierung mit Setlength nicht nach (womit ich jedoch nicht behaupte, daß sie falsch sei). Ich meinen Delphi4-Varienten werde ich - analog, wie ich es mit dem iterativen Quicksort tue - die Größe des Arrays prüfen (bei jeder Inkrementierung des Stack-Indixes "n" und ggf. anpassen. Mag nicht laufzeitoptimal sein, doch damit vermeide ich das Risiko der Unterdimensionierung.

Bist Du Dir sicher, daß Du immer auf tatsächlich existente Arrayelemente zugreifst? Die (Array-)Bereichsüberprüfung ist in den mir näher bekannten Delphi-Versionen (bis 7) im Standardfall ausgeschaltet, könnte darin schon der Fehler liegen?

Ich schicke Dir im Vertrauen die Delphi2++ - Variante schon mal vorab privat zu - einverstanden?

Gruß Delphi-Laie