Entwickler-Ecke

Algorithmen, Optimierung und Assembler - Schnelles Finden in TList


wulfskin - Do 10.02.05 15:12
Titel: Schnelles Finden in TList
Hallo,

ich suche nach einer Möglichkeit in einer Liste (TList) mit ca. 2000 Elemente eine 4-stellige Zeichenkette schnell zu finden. Die Liste enthält Elemente mit folgendem Muster:

Delphi-Quelltext
1:
2:
3:
Item = record
  Id: String[4];
  Data: Pointer;
Die Liste ist nach Id alphabetisch sortiert.
Zur Zeit suche ich das Item durch eine einfache Schleife, wobei dass bei zu vielen Zugriffen einfach zu lange dauert. Welche Möglichkeiten gibt es, die Suche zu verschnellern?

Gruß Hape!


Moderiert von user profile iconGausi: Topic aus CLX / Delphi Language (Object-Pascal) verschoben am Fr 11.02.2005 um 15:02


jasocul - Do 10.02.05 15:25

Wenn du nach der Id suchst, würde ich zunächst mit einem Halbierungsverfahren rangehen.
Andere Möglichkeit wäre eine zweite List zu führen, die die 26 Buchstaben des Alphabets enthält mit einem Index für deine TList.


wulfskin - Do 10.02.05 19:04

jasocul hat folgendes geschrieben:
Wenn du nach der Id suchst, würde ich zunächst mit einem Halbierungsverfahren rangehen.
Andere Möglichkeit wäre eine zweite List zu führen, die die 26 Buchstaben des Alphabets enthält mit einem Index für deine TList.
Danke für die Antwort,

leider musst du mir das noch etwas genauer erklären:
  1. Wie funktioniert das Halbierungsverfahren?
  2. Wie meinst du das mit dem Index
Danke dir!

Hape


Delete - Do 10.02.05 19:20

Er meint ne Intervallschachtelung. Sagt dir das etwas :?:


tommie-lie - Do 10.02.05 19:21

wulfskin hat folgendes geschrieben:
Wie funktioniert das Halbierungsverfahren?
Du sagtest, dein Index ist alphabetisch sortiert. Also gehst du zum Element in der Mitte, und guckst, ob der String "größer" oder "kleiner" ist. Ist er größer, liegt das gesuchte Objekt irdendwo weiter vorne, also überprüfst du das Objekt in der Mitte der Mitte.
Beispiel:
Liste:
a b c d e f g h i jDu suchst nach g, das mittlere Element wäre e. g ist aber größer als e, also nimmst du die Mitte der Hälfte, wäre h. g ist aber kleiner als h, also nimmst du die Mitte des verbleibenden Restes, wäre f. g ist größer als f, also nimmst du wieder die Mitte, wäre g. g ist g, also hast du das Objekt gefunden. Du halbierst also immer das Intervall, in dem du suchst, und schränkst es so auf ein Intervall der Größe 1 ein, welches das gesuchte Element enthält.


jasocul - Do 10.02.05 19:35

Danke tommie-lie :wink:
Sollte wulfskin noch fragen haben, kann er uns das ja mitteilen.


wulfskin - Do 10.02.05 19:51

Hallo,

ist klar! Hätte man auch selber draufkommen können. Ich bastel mir mal was zusammen und schau ob es dadurch viel schneller geht!

Gruß Hape!


UC-Chewie - Do 10.02.05 20:40

Zur Info: Das ganze nennt man binäre Suche und die Anzahl der Zugriffe beträgt max. floor(ld(n)), wobei floor() aufrundet und ld() der Logarithmus zur Basis 2 ist.
Bei 2.000 Einträgen brauchst du also maximal 11 Zugriffe.


wulfskin - Fr 11.02.05 14:33

UC-Chewie hat folgendes geschrieben:
Zur Info: Das ganze nennt man binäre Suche und die Anzahl der Zugriffe beträgt max. floor(ld(n)), wobei floor() aufrundet und ld() der Logarithmus zur Basis 2 ist.
Bei 2.000 Einträgen brauchst du also maximal 11 Zugriffe.
Danke schön,

das geht jetzt wirklich richtig schnell! 8) Für alle die es interessiert, hier mein Quelltext:

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:
function TW3gItemList.GetItemById(const AId: TItemId): TW3gItem;
  function FindInList(const AStart, AEnd: Integer; const AId: TItemId): TW3gItem;
  var
    I: Integer;
  begin
    Result := nil;
    if CompareText(Items[AStart].Id, AId) = 0 then
      Result := Items[AStart]
    else
    if CompareText(Items[AEnd].Id, AId) = 0 then
      Result := Items[AEnd]
    else begin
      if  (AStart <> AEnd)
      and ((AEnd - AStart) > 1then begin
        I := (AStart + AEnd) div 2;
        if AId > Items[I].Id then
          Result := FindInList(I, AEnd, AId)
        else
          Result := FindInList(AStart, I, AId);
      end
      else
        {Abbrechen};
    end;
  end;
  
begin
  Result := nil;
  if Count > 0 then
    Result := FindInList(0, Count - 1, AId);
end;
Jetzt muss ich nur noch schauen, wie ich ein Item schnell einfügen kann ohne die Liste jedesmal neu sortieren zu müssen.

Gruß Hape!


Gausi - Fr 11.02.05 16:01

Das geht auch einfach: Such das Item, was du einfügen willst, in deiner Liste, und merke dir die Position, an der die Suche (erfolglos) endet. Das ist dann die Einfügeposition (oder 1 davor oder dahinter).

Edit: Aber das gehört ja nicht hierhin *verschieb*


Lossy eX - Fr 11.02.05 17:32

Hi Wulfskin. Habe deine Frage erst jetzt gesehen. Und ich habe die ultimative Lösung für dich. ;-)

Ich sage nur Hash. Dort werden die Einträge nach einem bestimmten Verfahren in einem Array verteilt und du kannst so wahnsinnig schnell auf die entsprechenden Einträge zugreifen. Es wird intern nur eine Zahl anhand des Namens errechnen. Simples addieren und bits rotieren. Zu einem Namen kannst du einen Pointer ablegen. Das Einfügen geht auch wahnsinnig flott, da die Daten nun mal nicht sortiert werden müssen und auch keine Unmengen von Einträgen verschoben werden müssen. Kann auch problemlos Mengen von 100.000 Einträgen und mehr ab. 8)

Hier kommst du zu meiner Hashklasse [http://dev-center.de/index.php?cat=header&file=hash]


UC-Chewie - Fr 11.02.05 19:24

Hashes sind u.U. gut geeignet, in diesem Fall seh ich aber keinen Grund, wieso man einen benutzen soll. Die Daten sind ja sowieso schon sortiert und werden anscheinend nur nach dieser Sortierung verwendet, also bietet sich die binäre Suche an, die im Übrigen oft schneller als jedes Hashverfahren ist (aufgrund der Kollisionen, die dort auftreten).


wulfskin - Fr 11.02.05 19:35

Lossy eX hat folgendes geschrieben:
Hi Wulfskin. Habe deine Frage erst jetzt gesehen. Und ich habe die ultimative Lösung für dich. ;-)
Danke,

Hash kenn ich, aber ist für mein Problem ein bisschen zu aufwenidig (und wohl auch nicht schneller), da ich maximal 2000 Einträge habe.

Trotzdem danke,
Hape!


wulfskin - Fr 11.02.05 20:49

So hier der komplette Quelltext zum Einfügen und Finden:

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:
function TW3gItemList.BinarySearch(const AId: TItemId;
  var AFound: Boolean): Integer;
  
  function FindInList(const AStart, AEnd: Integer): Integer;
  var
    I: Integer;
  begin
    if CompareText(Items[AStart].Id, AId) = 0 then begin
      Result := AStart;
      AFound := True;
    end
    else
    if CompareText(Items[AEnd].Id, AId) = 0 then begin
      Result := AEnd;
      AFound := True;
    end
    else begin
      if (AEnd - AStart) > 1 then begin
        I := (AStart + AEnd) div 2;
        if CompareText(Items[I].Id, AId) = 0 then begin
          Result := I;
          AFound := True;
        end
        else
        if AId > Items[I].Id then
          Result := FindInList(I, AEnd)
        else
          Result := FindInList(AStart, I);
      end
      else
        Result := AStart;
    end;
  end;
  
begin
  Result := -1;
  AFound := False;
  if Count > 0 then
    Result := FindInList(0, Count - 1);
end;
Danke an alle Helfer. Irgend etwas, was man noch optimieren könnte?

Gruß Hape!


alexanm - Sa 12.02.05 06:06

Hallo wulfskin,

also wenn ich mir die Struktur Deiner Daten ansehe so würde ich eine TStringList nehmen. Diese kann den String (der Schlüssel) und die Daten (als Object) aufnehmen.

Weiters hat die StringList ein paar nette Properties:

- Sorted: Wenn true dann werden die Strings sortiert eingefügt und es wird beim Zugriff binär gesucht.
- CaseSensitive: Wenn false dann wird zwischen Groß-/Kleinschreibung nicht unterschieden.

Weiters wäre es echt interessant wie schnell die Stringliste im Gegensatz zu Deiner aktuellen Lösung ist.

cu
Maxx


tommie-lie - Sa 12.02.05 12:28

alexanm hat folgendes geschrieben:
also wenn ich mir die Struktur Deiner Daten ansehe so würde ich eine TStringList nehmen. Diese kann den String (der Schlüssel) und die Daten (als Object) aufnehmen.
Mag zwar komfortabel sein, aber die geht auch nur die einträge der Reihe nach durch und guckt, ob's passt, womit er wieder am Anfang stehen würde.


wulfskin - Sa 12.02.05 12:39

Hallo,

danke für die Tipps, aber mein record ist hier nur sehr vereinfacht dargestellt und sieht in Wirklichkeit etwas komplexer aus, so dass eine StringListe für mich nicht in Frage kommt.

@Tommie-Lie: Ich habe die Funktion IndexOf der StringListe nur kurz überflogen, aber ich glaube da wird auch binär gesucht.

Gruß Hape!


tommie-lie - Sa 12.02.05 12:55

wulfskin hat folgendes geschrieben:
@Tommie-Lie: Ich habe die Funktion IndexOf der StringListe nur kurz überflogen, aber ich glaube da wird auch binär gesucht.
Oops, stimmt, ich habe an eine unsortierte Liste gedacht. Wenn nicht sortiert ist, wird IndexOf() von TStrings aufgerufen, und der geht von 0 bis Count-1 alle Elemente durch, überprüft, ob die Strings übereinstimmen und gibt das gefundene Objekt zurück.
Bei sortierten Listen wird auch binär gesucht.

wulskin hat folgendes geschrieben:
mein record ist hier nur sehr vereinfacht dargestellt und sieht in Wirklichkeit etwas komplexer aus, so dass eine StringListe für mich nicht in Frage kommt.
Das ist eigentlich gar nicht wichtig. Durch das Objekt eines Stringlisteneintrages hast du die Möglichkeit, einen Pointer zu speichern. Deinen Record, egal wie kompliziert er ist, kannst du auch darin speichern, indem du einen Pointer auf das Record nimmst und ihn in ein TObject castest. Beim lesenden Zugriff musst du halt nur den Pointer wieder in einen Pointer auf dein Record casten und dereferenzieren.


wulfskin - Sa 12.02.05 15:00

Hallo Tommie-Lie, hallo Alex,

eigentlich habt ihr Recht, ich könnte wirklich eine StringListe benutzen. Naja, jetzt hab ich es auch so geschafft. Ich werd mal schauen was schneller ist und mich für das bessere entscheiden!
(Obwohl der Thread als abgeschlossen markiert ist, geht er doch immer weiter ;).)

Danke für eure Hilfe!
Hape


tommie-lie - Sa 12.02.05 15:03

wulfskin hat folgendes geschrieben:
eigentlich habt ihr Recht, ich könnte wirklich eine StringListe benutzen. Naja, jetzt hab ich es auch so geschafft. Ich werd mal schauen was schneller ist und mich für das bessere entscheiden!
Wenn du's richtig implementiert hast, dürfte sich deine Implementierung und TStrinList nicht viel nehmen ;-9

wulskin hat folgendes geschrieben:
(Obwohl der Thread als abgeschlossen markiert ist, geht er doch immer weiter ;).)
Nur weil die Frage beantwortet wurde, heißt es doch noch lange nicht, daß es keine weiteren Möglichkeiten zur Problemlösung mehr gibt ;-)


Kunstbanause - Fr 18.02.05 11:59

wulfskin hat folgendes geschrieben:
UC-Chewie hat folgendes geschrieben:
Zur Info: Das ganze nennt man binäre Suche und die Anzahl der Zugriffe beträgt max. floor(ld(n)), wobei floor() aufrundet und ld() der Logarithmus zur Basis 2 ist.
Bei 2.000 Einträgen brauchst du also maximal 11 Zugriffe.
Danke schön,

das geht jetzt wirklich richtig schnell! 8) Für alle die es interessiert, hier mein Quelltext:

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:
function TW3gItemList.GetItemById(const AId: TItemId): TW3gItem;
  function FindInList(const AStart, AEnd: Integer; const AId: TItemId): TW3gItem;
  var
    I: Integer;
  begin
    Result := nil;
    if CompareText(Items[AStart].Id, AId) = 0 then
      Result := Items[AStart]
    else
    if CompareText(Items[AEnd].Id, AId) = 0 then
      Result := Items[AEnd]
    else begin
      if  (AStart <> AEnd)
      and ((AEnd - AStart) > 1then begin
        I := (AStart + AEnd) div 2;
        if AId > Items[I].Id then
          Result := FindInList(I, AEnd, AId)
        else
          Result := FindInList(AStart, I, AId);
      end
      else
        {Abbrechen};
    end;
  end;
  
begin
  Result := nil;
  if Count > 0 then
    Result := FindInList(0, Count - 1, AId);
end;
Jetzt muss ich nur noch schauen, wie ich ein Item schnell einfügen kann ohne die Liste jedesmal neu sortieren zu müssen.

Gruß Hape!

Es geht auch ohne Rekursion:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
function GetIndex(Wert: String[4]; Liste: array of String): Integer;
var
   i, mi, le, ri  : Integer;
begin
   le := Start;
   ri := Ende;
   repeat
      mi    := (le+ri) div 2;
      if Wert<Liste[mi] then
         ri := mi - 1
      else
         le := mi + 1;           
   until (Wert=Liste[mi])or(le>ri);
   if Wert=Liste[mi] then
      Result   := mi
   else
      Result   := -1;
end;


Spaceguide - Fr 18.02.05 14:14

Also bei 2000 Elementen lohnt sich ein(e) HashTable schon. Bei guter Hashfunktion und geeigneter Hashgröße kommt man auf unter 1.2 Kollisionen/(gefüllte Zelle). Mit Halbierungsverfahren bräuchte man immer noch Log2(2000) ~ 11 Schritte.


UC-Chewie - Fr 18.02.05 17:10

Spaceguide hat folgendes geschrieben:
Also bei 2000 Elementen lohnt sich ein(e) HashTable schon. Bei guter Hashfunktion...(gefüllte Zelle).


Und das ist oft das Problem: Eine gute Hashfunktion zu finden. Eine Hashfunktion muss auf dein Problem zugeschnitten sein, um Kollisionen zu minimieren. Das ist nicht gerade triviale Mathamtik.


wulfskin - Fr 18.02.05 17:22

Vor allem, so denke ich zumindest, geht durch die Erstellung des Hashes gewisse Zeit verloren, die bei so einer geringen Menge ins Gewicht fällt.

Danke für Eure Hilfe. Im Endeffekt war die StringListe etwas schneller, warscheinlich weil die explizite Form schneller ist als meine Rekursive.

GRuß Hape!


Spaceguide - So 20.02.05 18:30

Apropos Hashfunktion: Ist es eigentlich garantiert, dass ein (Ansi)String ein #0 am Ende hat?


UC-Chewie - So 20.02.05 18:31

Wenn er nicht mittels Zeigerarithmetik manipuliert wurde: Ja.