Entwickler-Ecke

Algorithmen, Optimierung und Assembler - schnellstes Sortierverfahren für Strings


Heiko - Mi 05.10.05 15:21
Titel: schnellstes Sortierverfahren für Strings
Kann jemand vlt. mir sagen, welches Sortierverfahren am schnellsten ist für Strings, denn QuickSort ist ja bekanntlich nicht die schnellste Variante. Die Anzahl der Strings beläuft sich da von 0-10 000, wobei es meistens im Bereich von 500-2000 ist. ;)

PS: Am liebsten wäre es mit gleich mit einem Link zu der Erklärung des Verfahrens und am besten gleich mit Quelltext, der aber nicht zwingend dabei sein muss ;).


uall@ogc - Mi 05.10.05 15:40

Wofür brauchst du was schnelleres als Qicksort? Das wird doch wohl für deine Zwecke reichen. Qucksort hat schon nen laufzeitverhalten von n*log(n) was schnell ist, gibt noch heapsort und mergesort die gleich schnell sind. aber glaub für die paar elemente ist das egal. ich kenn nur einen alg mit dem laufzeitverhalten n, aber den kannste auf strings nicht anwenden.


Gausi - Mi 05.10.05 15:45

Wenn es sehr viele relativ kurze Strings sind, würde sich RadixSort [http://de.wikipedia.org/wiki/Radixsort] empfehlen. den müsste man evtl. nur anpassen, da deine Elemente (Strings) nicht alle gleich lang sind.

Da du aber nur ziemlich wenige Strings sortieren musst, und du für jedes Zeichen einen Durchlauf machen musst (z.B wenn du das Wort "Sortierverfahren" in deiner Stringliste hast, musst du 16 Durchläufe machen), dürfte der Aufwand von O(N LogN) im Mittel bei Quicksort in deinem Fall fast besser sein als der lineare Aufwand bei RadixSort.
Hinzu kommt ein erhöhter Speicherbedarf für RadixSort.

Überschlagsrechnung:

Quelltext
1:
2:
3:
16 * N ?<? logN * N
16 ?<? logN
2^16 ?<? N


Fazit: Wenn du so in etwa mehr als 2^16 (bzw. 2^x, wobei x die Länge deines längsten Wortes ist) Strings sortieren musst, dann würde ich von Quicksort abraten, ansonsten bleib besser dabei.
Anmerkung: Ich weiss, dass in der Laufzeit von Quicksort auch noch Konstanten drin sind, und man deshalb eigentlich nicht so rechnen kann, aber das Prinzip sollte klar sein...


Grishnak - Mi 05.10.05 15:47

@Heiko: Wichtig zu wissen: Sind diese Strings a) aufsteigend vorsortiert, b) absteigend vorsortiert oder c) unsortiert? Bei aufsteigend vorsortierten Feldern ist Quicksort mWn nicht so effektiv und wird von "primitiveren" Algorithmen geschlagen. Ansonsten kenne ich Quicksort als schnellste Möglichkeit.


Gausi - Mi 05.10.05 15:55

user profile iconGrishnak hat folgendes geschrieben:
Bei aufsteigend vorsortierten Feldern ist Quicksort mWn nicht so effektiv und wird von "primitiveren" Algorithmen geschlagen.

Das kommt sehr darauf an, wie man bei Quicksort das Pivotelement wählt. Die Methode "Sort" bei Delphi verwendet einen Quicksort mit Pivot-Element = mittleres Element(*), sodass bei auf-/ absteigend sortierten Listen eine optimale Aufteilung in zwei Teilfolgen erreicht wird. Daher zweifele ich deine Aussage zumindest teilweise an.

Wenn man als Pivotelement jedoch das erste/letzte Element wählt, bekommt man bei solchen Listen natürlich eine quadratische Laufzeit. Da Bubblesort die Vorsortierung in dem Fall besser ausnutzt, liegt dann Quicksort weit hinten.


(*) hab ich herausgefunden, als ich Sort aufgerufen habe mit einer Liste von 10.000 Elementen, die dadurch entstanden ist, dass ich 2 identische 5000er Listen aneinandergehägt habe. Der Sortiervorgang hat solange gedauert, dass ich das Prog mit Gewalt beenden musste. Hab ich diese Liste vorher "randomisiert", oder eine sortierte Liste mit 10.000 Elementen sortiert, war er in nullkommanix fertig.


Udontknow - Mi 05.10.05 16:03

Ein allgemeine Praxishinweis (vielleicht ein wenig Offtopic, aber bei 5000-10000 Strings und trotdem langer Wartezeit liegt es vielleicht sogar daran):

Variablen immer mit const oder var an Subroutinen übergeben, ansonsten kann sich die benötigte Zeit dramatisch erhöhen, da das Programm bei jedem Aufruf von Subroutinen erst den Variableninhalt kopieren muß. Bei Pointern, Integerwerten etc. ist das egal, aber alle Variablen, die größer als ein Pointer (also 4 Byte) sind, sollten so an Subroutinen übergeben werden (es sei denn, das spezielle Verhalten, daß Variablen innehrhalb der Routine änderbar sind, aber die Änderung nicht an die aufgerufene "Ebene" zurückgegeben werden, ist erwünscht).

Cu,
Udontknow


Grishnak - Mi 05.10.05 16:54

@Gausi:

Ich hab jetzt mal 50.000 Zufalls-Strings der Länge 8 in einer StringList gespeichert und mittels Sort-Methode sortieren lassen. Das ganze wurde anschließend nochmals sortiert.

Dabei hat das erneute Sortierten der bereits sortierten Liste ca. 66% der Zeit gedauert, die das Sortieren einer unsortierten Liste dauerte. Das deutet mMn nicht auf die Wahl des mittleren Elementes als Pivot-Element hin!


Gausi - Mi 05.10.05 17:05

user profile iconGrishnak hat folgendes geschrieben:
@Gausi:

Dabei hat das erneute Sortierten der bereits sortierten Liste ca. 66% der Zeit gedauert, die das Sortieren einer unsortierten Liste dauerte. Das deutet mMn nicht auf die Wahl des mittleren Elementes als Pivot-Element hin!


Hast du dich hier vertippt, oder bestätigst du damit nicht meine Aussage? Wenn das sortieren der sortierten Liste nur 66% der Zeit zum sortieren der unsortieren Liste benötigt, dann ist das weniger ;-)

Wie ich sagte, basiert meine Vermutung auf einem Test, den ich gemacht habe. Und es erscheint mir auch sinnvoll, nicht das letzt Element zu nehmen, da es doch öfter vorkommt, dass man eine fast sortierte Liste novhmal schnell "drübersortiert".

Und Schwankung um den Faktor 2 sind durchaus noch im "Messfehlerbereich" ;-)


Heiko - Mi 05.10.05 17:12

So bin jetzt wieder vom Einkaufen zurück und kann auf ein paar Nachfragen jetzt Antwort geben :).

@Länge der Strings: Sie ist ganz verschieden. Wie sich bei dieser Typendeklaration sicherlich denken kann.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
type
  TMusikInfo=record
    DateiPfad: String;
    Titel: String;
    Interpret: String;
    Album: String;
    Genre: String;
    Jahr: Integer;
    TitelNr: Integer;
    Kommentar: String;
    Komponist: String;
    Encoder: String;
    Copyright: String;
    Sprache: String;
    Link: String;
    Format: String;
  end;


Ich will also immer nach einem dieser Elemente sortieren (dynamisches Array), wodurch ich vorher nicht sagen kann, wie lang ein String ist, da es ja je nach dem Kriterium verschieden ist und ich nicht für jedes Element ein extra Verfahren haben will.

@Vorsortiert: Jein, die sind nach einem Kriterium sortiert, welches keine Rolle für das Suchverfahren besitzten wird. Sprich: Es kann sein das die Liste nach dem titel sortiert ist und jetzt nach dem Album sortiert werden soll ;).


Udontknow - Mi 05.10.05 17:43

Noch einmal offtopic:

Weitere Geschwindigkeitsvorteile gewinnst du, indem du in deinem Array nur Pointer auf per Getmem erstellte Records speicherst, sodaß beim Sortieren nicht der gesamte Record, sondern nur der Pointer auf einen Record ausgetauscht wird.

Cu,
Udontknow


Heiko - Mi 05.10.05 17:45

Mhm, ich kenne mich nicht so gut mit Pointern aus. Das mit GetMem und so bekomme ich hin, aber wie ich genau auf die einzelnen Elemente zeigen kann nicht. Muss ich mir mal angucken ;).

//Edit: Wie macht man es am besten, wenn in man die Größe eines Strings nicht kennt, den man einlesen will?


Udontknow - Mi 05.10.05 18:19

Hier mal ein objektorientierter Ansatz. Dadurch, daß nun der Record als Objekt (sprich Pointer) definiert ist, sollte das auch schon einen Effekt auf die Geschwindigkeit haben.

Ich hätte es wohl von Anfang an als DB-Projekt konzipiert, aber naja...

Du musst ein TMusikinfos-Objekt erstellen. Bei diesem kannst du dann sooft du eben Daten hast Add aufrufen und den neuen Eintrag mit Daten bestücken. Sortiert wird diese Liste dann über die angegebenen zwei Methoden (ist leider unflexibel, da man die einzelnen Felder des Records nicht dynamisch über einen Index ansprechen kann, wie man es bei Feldern eines Datasets machen könnte).


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:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
unit U_Musikinfo;

interface

uses
  SysUtils, Classes;

type
  TMusikInfo=Class
    DateiPfad: String;  
    Titel: String;  
    Interpret: String;  
    Album: String;  
    Genre: String;
    Jahr: Integer;  
    TitelNr: Integer;  
    Kommentar: String;  
    Komponist: String;  
    Encoder: String;  
    Copyright: String;  
    Sprache: String;  
    Link: String;  
    Format: String;  
  end;

type TMusikInfos=class
  private
    FItems:TList;
    function GetItems(Index: Integer): TMusikInfo;
    procedure SetItems(Index: Integer; const Value: TMusikInfo);
    function GetCount: Integer;
  public
    function Add:TMusikInfo;
    procedure Delete(Index:Integer);
    procedure Clear;
    property Items[Index:Integer]:TMusikInfo read GetItems write SetItems;
    property Count:Integer read GetCount;

    procedure SortierenNachDateiPfad;
    procedure SortierenNachTitel;

    constructor Create; virtual;
    destructor Destroy; override;
end;

implementation

{$R *.dfm}

//Interne Funktionen zum Vergleichen

function Vergleich_Titel(Item1, Item2: Pointer): Integer;
var I1,I2:TMusikInfo;
begin
  I1:=TMusikInfo(Item1);
  I2:=TMusikInfo(Item2);

  Result:=0;
  if I1.Titel<I2.Titel then
    Result:=-1
  else
  if I1.Titel>I2.Titel then
    Result:=1
end;

function Vergleich_DateiPfad(Item1, Item2: Pointer): Integer;
var I1,I2:TMusikInfo;
begin
  I1:=TMusikInfo(Item1);
  I2:=TMusikInfo(Item2);

  Result:=0;
  if I1.DateiPfad<I2.DateiPfad then
    Result:=-1
  else
  if I1.DateiPfad>I2.DateiPfad then
    Result:=1
end;

{ TMusikInfos }

function TMusikInfos.GetItems(Index: Integer): TMusikInfo;
begin
  //Listenobjekt ausgeben
  Result:=TMusikInfo(FItems[Index]);
end;

procedure TMusikInfos.SetItems(Index: Integer; const Value: TMusikInfo);
begin
  //Listenobjekt setzen
  FItems[Index]:=Value;
end;

function TMusikInfos.Add: TMusikInfo;
begin
  //Neues Element erstellen und in Liste einfügen
  Result:=TMusikInfo.Create;
  FItems.Add(Result);
end;

procedure TMusikInfos.Delete(Index: Integer);
begin
  //Element entfernen (dabei freigeben)
  TMusikInfo(FItems[Index]).Free;
  FItems.Delete(Index);
end;

procedure TMusikInfos.Clear;
begin
  //Liste leeren (dabei Elemente freigeben)
  While FItems.Count>0 do
    Delete(0);
end;

function TMusikInfos.GetCount: Integer;
begin
  //Anzahl der Elemente zuückgeben
  Result:=FItems.Count;
end;

constructor TMusikInfos.Create;
begin
  //Interne Liste erstellen
  FItems:=TList.Create;
end;

destructor TMusikInfos.Destroy;
begin
  //Alle Elemente freigeben
  Clear;
  //interne Liste freigeben
  FItems.Free;
  inherited;
end;

procedure TMusikInfos.SortierenNachTitel;
begin
  FItems.Sort(Vergleich_Titel);
end;

procedure TMusikInfos.SortierenNachDateiPfad;
begin
  FItems.Sort(Vergleich_DateiPfad);
end;

end.


Cu,
Udontknow


Gausi - Mi 05.10.05 18:31

Also, wenn deine Strings SO aussehen ;-), dann kann ich dir aus eigener Erfahrung sagen: Nimm ruhig die Standardvariante mit dem Quicksort. Das ist schnell genug. Und insbesondere beim Sortieren nach Pfad dürfte Quicksort wesentlich schneller sein.

Ich hab auch ziemlich viele solche "Strings", und wenn ich auf den Header in meinem VirtualTreeView klicke, dann gibt es ne Verzögerung von ca. 0,5 Sekunden, bis "1910 Fruitgum Company - Simon Says" ganz oben und "ZZ Top - What Would You Do" ganz unten ist ;-) Und bei mir ist die Anzahl noch etwas über deinem Maximalbereich (0-10.000)


Grishnak - Mi 05.10.05 19:03

@Gausi: "Schneller" ist nicht das gleiche wie "schnell"! Wenn ein Sortiert-Algorithmus ca. 2/3 der Zeit für das sortieren eines bereits sortierten Feldes braucht, dann ist er dafür weniger geeignet - genau das wollte ich andeuten! Aber: back-to-topic!


alzaimar - Mi 05.10.05 19:21

Soweit ich weiss, soll Mergesort etwas schneller sein, aber ich denke, es lohnt den Aufwand nicht. Kopier dier das Quicksort von TStringlist und experimentier damit rum.

Wenn Du noch schneller werden willst, dann nimm ein iteratives Quicksort, das ist nochmal 10% schneller. Grundsätzlich sollte auf Rekursion verzichtet werden (so schon, wie sie ist), wenn es schnell gehen soll.

Hier, QnD (Quick'n Dirty):

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:
procedure QuickSortIt (a : TIntegerArray; L,R : Integer);
var
  sp, I, J,p,t: Integer;
  s : Array [0..100,0..1Of Integer; // Stack

begin
  fillchar (s,sizeOf(s),0);
  sp := 1;
  s[0,0] := L;
  s[0,1] := R;
  While sp>0 Do Begin           // solange noch Teillisten abzuarbeiten sind ...
    dec (sp);                   // Hole nächste Teillösung von Stack
    l := s[sp,0];
    r := s[sp,1];

    repeat                      // --------- Quicksort Anfang
      I := L;
      J := R;
      P := a[(L + R) shr 1];
      repeat
        while a[i]<p do inc (i);
        while a[j]>p do dec (j);
        if I <= J then
        begin
          T := a[I];a[I] := a[J];a[J] := T;
          Inc(I);
          Dec(J);
        end;
      until I > J;
      if L < J then begin       // Statt rekursivem Aufruf wird die Teilliste
        s[sp,0] := l; s[sp,1] := j; // auf den Stack geschoben und später
        inc (sp);               // sortiert
        End;
      L := I;
    until I >= R;               // --------- Quicksort Ende
    End;
end;


Gausi - Mi 05.10.05 19:25

Ist ja eigentlich nicht OT, da es ja um geeignete (=schnelle) Sortierverfahren geht. Ich hatte dich da falsch verstanden. Ich dachte, du vergleichst beide Verfahren, aber die Angaben waren wohl für Quicksort bei 2 Listen.
Das widerspricht aber nicht meiner Vermutung. Sie bestätigt nur die Güte von Quicksort und die Tatsache, dass "im Mittel" die Liste gut aufgeteilt wird. Nur daher kommt ja die schnelle Laufzeitabschätzung.
D.h. eine unsortierte Liste wird nur um 30% langsamer sortiert, als wenn man eine optimale Aufteilung hat (ich gehe weiter von meiner Vermutung aus).

Es ist richtig, dass Quicksort keine Vorsortierung ausnutzen kann. Deshalb ist Quicksort nie dann optimal, wenn man sortierte Listen sortiert. Aber Qicksort ist in vielen Fällen sehr "quick", daher ja auch der Name. In der Praxis hat der sich ganz einfach bewährt.

Und da du hier keinen "Spezialfall" hast, wo du ganz spezielle Voraussetzungen an deine Liste stellst, würde ich einfach dabei bleiben. Einen spürbar schnelleren Algo wirst du kaum finden. (und das ist wieder voll On-Topic ;-))


MSCH - Mi 05.10.05 19:33

ich auch mal; bei einer solchen Menge, die wie ich vermute, als File of Record (?) irgentwo auf der Platte dümpelt, ist da nicht eine kleine Datenbank besser? Es reicht ja dazu Access und Ado. Und du hast deine Probleme mehr oder weniger gelöst, wenn du dort die richtigen Indizes setzt. Dann ist das Laden und Umsortieren nur noch abhängig von der Hardware.
grez
msch


alzaimar - Mi 05.10.05 19:40

Kurze Frage am Rande:
Wozu sortieren? Zum schnellen finden? Dann wäre eine Dictionary (Hashliste) ideal. Einfügen ist O(1), Suchen ist O(1), Löschen auch. Nur mit der sortierten Ausgabe haperts.

Eine weitere Möglichkeit könnten die Skiplist sein. Das hat ein nahezu lineares Laufzeitverhalten.


Heiko - Mi 05.10.05 20:25

Ne, die ich erListe sortiere ich nicht zum schnellen finden. Das sortieren soll für den mp3-Tag-Editor von [url=http://www.killprocess.de.vu]KillProcess[/url] sein. Dabei suche ich nach allen Musikdateien und will dem User die Daten dann nach z.B. dem Titel sortiert präsentieren.


alzaimar - Mi 05.10.05 20:33

Ach so. Na dann Quicksort. Oder einfach TStringlist.Sort einfach gehts doch nicht. Und ob der anwender 100 oder 120ms wartet ist doch peng.

Hier: Teste selbst


Heiko - Do 06.10.05 10:51

user profile iconalzaimar hat folgendes geschrieben:
Ach so. Na dann Quicksort. Oder einfach TStringlist.Sort einfach gehts doch nicht. Und ob der anwender 100 oder 120ms wartet ist doch peng.

Mit StringListen zu arbeiten schlägt ja völlig gegen Performancevorteile und es ich kann da ja nicht die Zusammengehörigkeit behalten, oder nur sehr schwer ;)


user profile iconalzaimar hat folgendes geschrieben:
Hier: Teste selbst

:gruebel: Was willst du damit aussagen?


alzaimar - Do 06.10.05 11:46

user profile iconHeiko hat folgendes geschrieben:
Mit StringListen zu arbeiten schlägt ja völlig gegen Performancevorteile...

Ich fülle ein Stringarray (dynamisch) mit 10000 Elementen in 1,8ms. Eine Stringlist (Capacity vorher gesetzt) in 3,5ms. einerseits ist das die doppelte Zeit, andereseits: Drauf gesch*****. Das Sortieren dauert mit der Stringlist per s.sort 60ms. Das ist in jedem Fall unterhalb der Warnehmungsschwelle. Du schreibst doch keine zeitkritische RT-Anwendung.

Ich würde keine Sekunde mehr daran verschwenden, was denn nun schneller ist. Es *ist* schnell genug, und das zählt. Eine Stringliste ist doch völlig ok für weniger als so 10-20k Elemente. Darüber wirds dann natürlich sehr laangsaam.
user profile iconHeiko hat folgendes geschrieben:
und es ich kann da ja nicht die Zusammengehörigkeit behalten, oder nur sehr schwer ;)

Kapier ich nicht
user profile iconalzaimar hat folgendes geschrieben:
Hier: Teste selbst

:gruebel: Was willst du damit aussagen?[/quote]
Na, das Du es selbst testen sollst. das 'Hier:' bezog sich auf die (auch topologische) Nähe zum Attachment 'TestSorting.Zip', welches, wie der Name impliziert, Sortierungen testet.


Heiko - Do 06.10.05 13:27

user profile iconalzaimar hat folgendes geschrieben:
user profile iconHeiko hat folgendes geschrieben:
Mit StringListen zu arbeiten schlägt ja völlig gegen Performancevorteile...

Ich fülle ein Stringarray (dynamisch) mit 10000 Elementen in 1,8ms. Eine Stringlist (Capacity vorher gesetzt) in 3,5ms. einerseits ist das die doppelte Zeit, andereseits: Drauf gesch*****. Das Sortieren dauert mit der Stringlist per s.sort 60ms. Das ist in jedem Fall unterhalb der Warnehmungsschwelle. Du schreibst doch keine zeitkritische RT-Anwendung.

Ich würde keine Sekunde mehr daran verschwenden, was denn nun schneller ist. Es *ist* schnell genug, und das zählt. Eine Stringliste ist doch völlig ok für weniger als so 10-20k Elemente. Darüber wirds dann natürlich sehr laangsaam.
user profile iconHeiko hat folgendes geschrieben:
und es ich kann da ja nicht die Zusammengehörigkeit behalten, oder nur sehr schwer ;)

Kapier ich nicht

Naja ich habe doch einen Record und jetzt möchte ich nach einem dieser Strings aus dem Record sortieren. Dafür muss ich ja die StringList mit diesesn Strings füllen. Aber woher weiß ich dann noch, welcher Titel z.B. zu den neu sortierten Alben gehört?


user profile iconalzaimar hat folgendes geschrieben:
user profile iconHeiko hat folgendes geschrieben:
user profile iconalzaimar hat folgendes geschrieben:
Hier: Teste selbst

:gruebel: Was willst du damit aussagen?

Na, das Du es selbst testen sollst. das 'Hier:' bezog sich auf die (auch topologische) Nähe zum Attachment 'TestSorting.Zip', welches, wie der Name impliziert, Sortierungen testet.


Ich hatte den Anhang mal wieder nicht gesehen ;). Wo du es jetzt sagtest, das dort ein Anhang habe ich einmal die Seite aktualisiert so das ich ihn jetzt sehe ;).


alzaimar - Do 06.10.05 13:49

Sei A : Array Of TMyRecord und TMyRecord.Caption der String, nachdem Du sortieren willst:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
Var
  sl : TStringList;

Begin
  sl := TStringlist.creatE;
  sl.capacity := Length (A);
  For i:=0 To High (A) Do sl.addObject (a[i].Caption, @A[i]);
  sl.sort;
  // In sl.Objects[i] ist ein Zeiger auf das entsprechend TMyRecord