Entwickler-Ecke

Delphi Language (Object-Pascal) / CLX - Liste "in sich" nochmals sortieren


galagher - Fr 01.02.13 16:46
Titel: Liste "in sich" nochmals sortieren
Hallo!

Ich möchte eine Liste aufsteigend sortieren, die aus Jahren und Monatsnamen besteht. Momentan ist die Liste bloss nach Jahren sortiert und sieht so aus:

Quelltext
1:
2:
3:
4:
5:
6:
2009 August
2009 Mai
2013 Dezember
2013 Jänner
2014 Dezember
2016 Jänner

Ich möchte diese Liste aber auch "in sich" sortieren, also zunächst nach Jahren, dann, "innerhalb" der Jahre nach Monaten, so dass das so aussieht:

Quelltext
1:
2:
3:
4:
5:
6:
2009 Mai
2009 August
2013 Jänner
2013 Dezember
2014 Dezember
2016 Jänner

Das ist nur ein Beispiel, die Liste kann im Prinzip beliebig lang sein und alle möglichen Jahre und Monate in allen möglichen Kombinationen enthalten.

Frage also: Wie sortiere ich das? Mein erster Gedanke wäre hier:
Merke dir die Jahre, entferne sie, sortiere die Liste alphabetisch nach Monaten, füge dann die Jahre wieder vorne an. Wiederhole das für alle Jahre.
Aber geht das auch einfacher?


jfheins - Fr 01.02.13 16:57

Wenn du einen stabilen Sortieralgorithmus verwendest, kannst du einfach die gesamte Liste zweimal nach unterschiedlichen Kriterien sortieren.
==> http://de.wikipedia.org/wiki/Stabilität_(Sortierverfahren)


Martok - Fr 01.02.13 17:03

Oder du änderst deine Komparatorfunktion (kommt drauf an, wie du sortierst) so, dass sie das tut, was du möchtest. Also z.b.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
function CompareKeys(A,B: TListElement): integer;
begin
  Result:= A.Jahr-B.Jahr;
  if Result = 0 then
    Result:= A.Monat-B.Monat;
  if Result = 0 then
    // Tag, Stunde, Minute... etc ;-)
end

Vorzeichen nach Geschmack vertauschen, ich kann mir nie merken wierum der Standardrückgabewert bei den Dingern ist :P

€: yay, ich hab auch mal ein Rennen gewonnen :mrgreen: Wobei meins glaub ich eher FPC ist. Generic Lists und so.


WasWeißDennIch - Fr 01.02.13 17:03

Wenn es sich um TList oder einen Nachfahren davon handelt, gibt es ja einen Callback (Vergleichsfunktion) vom Typ TListSortCompare [http://docwiki.embarcadero.com/Libraries/XE2/de/System.Classes.TListSortCompare]. Innerhalb dessen kann man unterschiedliche Kriterien auswerten.

[edit] Einen Hauch zu spät, aber derselbe Gedanke :mrgreen: [/edit]


MeierZwoo - Fr 01.02.13 21:57

Irgendwo hängen bei mir in alten Quellcodes noch ein paar Sortieralgorithmen rum, aber bei neuem Code (bzw. Ergänzungen) bin ich zu faul geworden, mir deswegen einen Kopf zu machen - und bei den fetten CPUs und RAMs ist sortieren über TStringList nicht mehr zeitkritisch. Deshalb sortiere ich nur noch über TStringList und habe dafür (und andere Anwendungen) in allen Programmen ein bis zwei Listen vom Programmstart bis Ende zur allgemeinen Verfügung.

Ich cleare die Liste, erstelle aus den ankommenden Daten einen String mit dem Sortierkriterium und schreibe die Originaldaten an eine bestimmte Lesestelle dahinter und lese aus der sortierten Liste die Originaldaten wieder aus:

Daten unsortiert:

Quelltext
1:
2:
3:
4:
5:
6:
Ute 23. August 2009
Peter 4. Dezember 2014 
Paul 12. Mai 2009
Hans 12. Januar 2013 
Rita 13. Januar 2016
Klaus 3. Dezember 2013

Nach Sortierkriterien und sortierbaren Kriterien (hier Kalenderdatum) mit Original in eine List1:StringList geschrieben (Die leerstellen zwischen Sortierwert und Original können auch unterbleiben):

Quelltext
1:
2:
3:
4:
5:
6:
20090823      Ute 23. August 2009
20141204      Peter 4. Dezember 2014 
20090512      Paul 12. Mai 2009
20130112      Hans 12. Januar 2013 
20160113      Rita 13. Januar 2016
20131203      Klaus 3. Dezember 2013

List1.sort

Quelltext
1:
2:
3:
4:
5:
6:
20090512      Paul 12. Mai 2009
20090823      Ute 23. August 2009
20130112      Hans 12. Januar 2013
20131203      Klaus 3. Dezember 2013
20141204      Peter 4. Dezember 2014 
20160113      Rita 13. Januar 2016

Original wieder ausgelesen (copy(List1[n],Lesestelle,Laenge))

Quelltext
1:
2:
3:
4:
5:
6:
Paul 12. Mai 2009
Ute 23. August 2009
Hans 12. Januar 2013
Klaus 3. Dezember 2013
Peter 4. Dezember 2014 
Rita 13. Januar 2016

Das ist zwar nicht die hohe Schule der Programmierung, aber benötigt nur geringen Zeitaufwand und ist auch nicht unbedingt resourcen-fressender als ein rekursiver Algorithmus.

Die sog. Stabilität läßt sich bei Bedarf durch einen Reihenfolgeanhang am Sortierbegriff auch einhalten.

Zum Zeitverhalten: Ich habe bei einer speziellen Routine, die neben Berechnungen zig dieser Sortierungen pro Aufruf benutzt, das Zeitverhalten nur durch 10.000.000 mal aufrufen messen können (0,0001 bis 0,0007 s für die gesamte Routine). Bei den Werten bleib ich auf der auch in 10 Jahren im Quellcode nachvollziehbaren Methode mit den Listen :)


galagher - Sa 02.02.13 07:36

user profile iconMeierZwoo hat folgendes geschrieben Zum zitierten Posting springen:
- und bei den fetten CPUs und RAMs ist sortieren über TStringList nicht mehr zeitkritisch. Deshalb sortiere ich nur noch über TStringList
Ja, ich sortiere das auch mit einer TStringList.
Bin aber draufgekommen, dass es zumindest für meinen Fall noch einfacher geht: Wenn ich statt der Monatsnamen entsprechende Zahlen verwende, genügt eine einfaches .Sort! Danach tausche ich die Monatszahlen einfach gegen Monatsnamen aus.


MeierZwoo - Sa 02.02.13 08:03

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
Wenn ich statt der Monatsnamen entsprechende Zahlen verwende,


Wie denn sonst?


galagher - Sa 02.02.13 10:54

user profile iconMeierZwoo hat folgendes geschrieben Zum zitierten Posting springen:
Wie denn sonst?
Ich meine, statt:

Quelltext
1:
2009 Mai                    

ist das eben:

Quelltext
1:
2009 05                    

Dann genügt eine einfache aufsteigende Sortierung, wie sie zB. Sort bei TStringList bietet. Ich habe dabei den Vorteil, dass zunächst sowieso nur Zahlen in die Liste geschrieben werden und ich erst danach die Monats-Zahlen durch Monatsnamen ersetze. Ich habe das zuerst übersehen, da es ein altes Projekt ist, das ich jetzt wieder hervorgekramt habe!
Also: Erst die (temporäre) TStringList befüllen, danach sortieren, dann den Inhalt in eine TListBox schreiben und die TStringList wieder freigeben. Das ist alles!


WasWeißDennIch - Sa 02.02.13 12:21

Irgendwie ist das aber eher Sortierung der Darstellung statt der eigentlichen Daten. In diesem Fall ist das noch relativ einfach zu machen, wird aber bei komplexeren Daten schnell kompliziert und unhandlich.


MeierZwoo - Sa 02.02.13 13:55

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconMeierZwoo hat folgendes geschrieben Zum zitierten Posting springen:
Wie denn sonst?
Ich meine, statt:

Quelltext
1:
2009 Mai                    


Quelltext
1:
2009 05                    


Ich meinte mit "Wie denn sonst?" wie bitte willste du Monatsnamen nach Zeit sortieren, ohne sie in Ordinalwerte umzuwandeln? Es ist doch total selbstverständlich, solchen Sortierbegriffen wie Monatsnamen, Wochentagen ... erst Ordinalwerte zuzuordnen.

user profile iconWasWeißDennIch hat folgendes geschrieben Zum zitierten Posting springen:
Irgendwie ist das aber eher Sortierung der Darstellung statt der eigentlichen Daten. In diesem Fall ist das noch relativ einfach zu machen, wird aber bei komplexeren Daten schnell kompliziert und unhandlich.

Ich will weder meine Methode durchpauken noch jemanden dazu überreden - aber benenn mal bitte irgend etwas, was sich nicht durch ein sortierbares Stringlateral darstellen läßt - entweder durch direkte Umwandlung oder Umwandlung in einen Wert. Und wenn dies nicht geht, wie machst Du dann in einem Sortier-Algorithmus den Größenvergleich?


WasWeißDennIch - Sa 02.02.13 14:16

Wieso soll man denn alles Mögliche in einen String konvertieren? Nur damit man mit einer TStringlist sortieren kann?


MeierZwoo - Sa 02.02.13 14:28

user profile iconWasWeißDennIch hat folgendes geschrieben Zum zitierten Posting springen:
Wieso soll man denn alles Mögliche in einen String konvertieren? Nur damit man mit einer TStringlist sortieren kann?

Ich hatte dich gebeten, etwas zu benennen, was sich weder durch Konvertierung noch Darstellung mittels eines Ordinalwertes darstellen läßt. Und auch gebeten, dann zu erklären, wie Du dann damit den Größenvergleich innerhalb eines Sortieralgorithmus machst. - Mir jedenfalls fällt nichts ein und ist auch noch nichts untergekommen.


WasWeißDennIch - Sa 02.02.13 14:32

Und ich hatte Dich gebeten, mir zu erklären, wieso Du erst in einen String konvertieren willst. Sind wir uns eben gegenseitig eine Antwort schuldig.


MeierZwoo - Sa 02.02.13 14:48

user profile iconWasWeißDennIch hat folgendes geschrieben Zum zitierten Posting springen:
Und ich hatte Dich gebeten, mir zu erklären, wieso Du erst in einen String konvertieren willst.

Ohne in einen String zu konvertieren kann man nunmal nichts in eine StringList eintragen.

Und weshalb ich per StringList sortiere, hatte ich in meinem Beitrag, auf den Du dich ja beziehst, ausführlich gesagt.

Was daran unhandlich sein soll, entzieht sich meiner Kenntnis. Vorallem in Hinblick auf Nachvollziehbarkeit im Quellcode, auch nach 10 Jahren noch, ist es genau das Gegenteil von unhandlich.


WasWeißDennIch - Sa 02.02.13 15:00


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:
type
  TMonat = 1..12;

  PMyItem = ^TMyItem;
  TMyItem = record
    Jahr: Cardinal;
    Monat: TMonat;
  end;

...

function SortCompare(Left, Right: PMyItem): integer;
begin
  Result := Left^.Jahr - Right^.Jahr;
  if Result = 0 then
    Result := Left^.Monat - Right^.Monat;
end;

function TForm1.ItemToString(const Item: TMyItem): string;
const
  sMonate: array[TMonat] of string = ('Januar''Februar''März''April''Mai',
    'Juni''Juli''August''September''Oktober''November''Dezember');
begin
  Result := Format('%s %d', [sMonate[Ord(Item.Monat)], Item.Jahr]);
end;

procedure TForm1.AddData(Jahr: Cardinal; Monat: TMonat; List: TList);
var
  Item: PMyItem;
begin
  Assert(Assigned(List));
  New(Item);
  Item^.Jahr := Jahr;
  Item^.Monat := Monat;
  List.Add(Item);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Daten: TList;
begin
  Daten := TList.Create;
  try
    AddData(200911, Daten);
    AddData(200912, Daten);
    AddData(20099, Daten);
    AddData(20094, Daten);
    AddData(20097, Daten);
    AddData(20133, Daten);
    AddData(20093, Daten);
    AddData(200910, Daten);
    AddData(20091, Daten);
    AddData(20099, Daten);
    AddData(200011, Daten);
    AddData(20107, Daten);
    AddData(20029, Daten);
    AddData(20122, Daten);
    Daten.Sort(@SortCompare);
    ListData(Daten, Listbox1.Items);
  finally
    Daten.Free;
  end;
end;

procedure TForm1.ListData(List: TList; Output: TStrings);
var
  i: integer;
begin
  Assert(Assigned(List) and Assigned(Output));
  Output.BeginUpdate;
  try
    Output.Clear;
    for i := 0 to List.Count - 1 do
      Output.Add(ItemToString(TMyItem(List[i]^)));
  finally
    Output.EndUpdate;
  end;
end;

Das kommt mit einer einzigen Liste aus, benötigt außer bei der Darstellung keine Konvertierung, man muss sich keine Gedanken machen, wie der String für eine korrekte Sortierung auszusehen hat (führende Nullstellen), etc. Unter Verwendung von Generics wäre es noch einen Tick einfacher, ich habe hier bewusst darauf verzichtet.


MeierZwoo - Sa 02.02.13 15:13

Wo bekommst Du bei "adddata" den Ordinalwert des Monats plötzlich her?


WasWeißDennIch - Sa 02.02.13 15:17

OK, ich hatte im Ausgangspost überlesen, dass es sich um Monatsnamen handelt. Dann schreiben wir eben eine Funktion mehr und machen die Array-Konstante global, spielt aber für das grundsätzliche Vorgehen keine Rolle.


MeierZwoo - Sa 02.02.13 15:58

user profile iconWasWeißDennIch hat folgendes geschrieben Zum zitierten Posting springen:
spielt aber für das grundsätzliche Vorgehen keine Rolle.

... außer, daß eben das, was Du unbedingt vermeiden willst, eine Konvertierung (hier zu einem Ordinalwert) eben doch durchgeführt werden muß. Die vergleichbaren Sortierbegriffe/Werte müssen nun einmal größenvergleichbar vorbereitet bzw. als solche definiert werden, egal womit man sortiert.

Und nochmal: Ich habe mehrfach ausdrücklich betont, daß ich niemanden zur Benutzung von StringLists überreden will. Ich benutze diese.

Nur was an deinem Beispiel handlicher und evtl. auch resoercenschonender sein soll, entzieht sich nun wirklich meiner Kenntnis. Es ist deine Art, eine Aufgabe zu programmieren.

Für mich ist wesentlich, auch nach 25 Jahren meinen eigenen Quellcode schnell verstehen zu können.


WasWeißDennIch - Sa 02.02.13 16:02

Was Du machst, geht mir auch irgendwo hinten vorbei. Ich hatte lediglich darauf hingewiesen, dass hier eine Darstellung sortiert wird und nicht die dahinterstehenden Daten. Wenn Du daraus eine Diskussion anzettelst, ist das Deine Sache.

[edit] Außerdem hatte ich nie behauptet, Konvertierungen unbedingt vermeiden zu müssen/wollen, aber ich muss das je Item nur einmal machen, nämlich beim Einfügen in die Liste. Und dass eine einzelne Liste weniger Ressourcen verbraucht als deren 2 sollte irgendwo logisch sein, oder? [/edit]


MeierZwoo - Sa 02.02.13 16:30

user profile iconWasWeißDennIch hat folgendes geschrieben Zum zitierten Posting springen:
Was Du machst, geht mir auch irgendwo hinten vorbei.

Dann sind wir ja wenigsten in einem Punkt gleicher Meinung :)


WasWeißDennIch - Sa 02.02.13 16:33

Sieht so aus.