Autor |
Beitrag |
Gausi
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Do 26.07.07 16:09
Ich habe eine Liste von Strings gegeben. Die Strings in dieser Liste sind entweder sehr verschieden oder sehr ähnlich. Ich möchte nun aus dieser Liste den String bestimmen, der die Liste am besten beschreibt. Ein paar Beispiele:
Liste1: Otto; Karl; Gustav; Heinz; Ludwig; Thorsten; Otto; Fritz;
Ergebnis: "Verschiedene"
Liste2: Otto; Otto; Otto; Otto; Otto und Gustav; Otto und Heinz; Otto
Ergebnis: Otto
Liste3: Otto; Otto; Otto; Otto; Heinz; Otto; Otto; Otto;
Ergbnis: Otto
Liste4: Ottos Bilder (Kiste 1 von 2); Ottos Bilder (Kiste 2 von 2); Ottos Bilder (Kiste 1 von 2); Ottos Bilder (Kiste 1 von 2); (...)
Ergebnis: Ottos Bilder
Das ganze soll angewendet werden auf Listen mit in der Regel 10-50 Elementen, das aber sehr oft (ca. 1000-5000mal, ggf. auch öfter) und soll nicht länger als 2-3 Sekunden dauern. Also 2-3 Sekunden insgesamt. Nicht pro Liste  .
Ich steh nur irgendwie auf dem Schlauch, wie ich das anstellen soll. Ein Ansatz wäre der "längste gemeinsame Start-String", aber das würde bei Liste4 "Ottos Bilder (Kiste " zurückliefern (wobei ich damit leben könnte, das könnte man durch Heuristiken wegfrickeln - da steht nämlich fast immer "Kiste" oder "Box" [nicht wirklich, aber so was ähnliches  ]). Nur wie kriege ich dann den Fall 3 da noch einigermaßen mit rein? Und zwar einigermaßen sauber ohne viel Fallunterscheidungen, in die man sich nur verennt?
Oder, so zum Anfangen: Wie bestimme ich "den längsten gemeinsamen Anfangsstring" effizient? (d.h. wie bekomme ich die Ergebnisse für Liste 1, 2 und 4?)
_________________ We are, we were and will not be.
|
|
Narses
      

Beiträge: 10183
Erhaltene Danke: 1256
W10ent
TP3 .. D7pro .. D10.2CE
|
Verfasst: Do 26.07.07 16:19
Moin!
Schonmal hier geschaut?
cu
Narses
_________________ There are 10 types of people - those who understand binary and those who don´t.
|
|
Agawain
      
Beiträge: 460
win xp
D5, MySQL, devxpress
|
Verfasst: Do 26.07.07 16:29
Hi
Spontan fällt mir dazu eine phonetische Prüfung ein, wie Soundex oder Metaphone
en.wikipedia.org/wiki/Metaphone
Gruß
Aga
|
|
Stefan.Buchholtz
      
Beiträge: 612
WIN 2000, WIN XP, Mac OS X
D7 Enterprise, XCode, Eclipse, Ruby On Rails
|
Verfasst: Do 26.07.07 16:32
Mein Ansatz wäre, die Listeneinträge in einzelne Wörter zu zerlegen und für jedes Wort die Häufigkeit zu zählen - ggf. vorher noch Füllwörter wie und, der, die, das usw. rausfiltern.
Dann kannst du Prüfen, ob die relative Häufigkeit des häufigsten Wortes über einem Schwellwert liegt, den du durch Experimentieren herausfindest. Bei deinen Beispielen ist es sehr eindeutig:
1. Otto - 28,57 %
2. Otto - 77,78 %
2. Otto - 87,5 %
Stefan
_________________ Ein Computer ohne Windows ist wie eine Schokoladentorte ohne Senf.
|
|
Stefan.Buchholtz
      
Beiträge: 612
WIN 2000, WIN XP, Mac OS X
D7 Enterprise, XCode, Eclipse, Ruby On Rails
|
Verfasst: Do 26.07.07 16:35
Ups, gar nicht gesehen, dass du auch nach Begriffen aus mehreren Wörtern suchst. Dann vergiss das wohl eher...
Stefan
_________________ Ein Computer ohne Windows ist wie eine Schokoladentorte ohne Senf.
|
|
Gausi 
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Do 26.07.07 16:44
Zum Wiki-Artikel: Das ist zuviel des Guten. Außerdem braucht man da schon beim Ermitteln der Gemeinsamkeit von zwei Strings N^2 Schritte, und das ist zuviel. Das muss linear bleiben. Ich suche ja auch nicht die gemeinsame Teilsequenz, sondern nur den längsten gemeinsamen Teilstring, der noch dazu am Anfang stehen soll (also eine weitere Vereinfachung). Allerdings möchte ich in der Liste ein oder zwei Ausreißer erlauben. (Was eine Verkomplizierung bedeutet, wo man wahrscheinlich nur mit Heuristiken rangehen kann, z.B. die Liste sortieren und versuchen, aufzuteilen oder so.)
SoundEx bringt mir auch nichts. Denn das liefert mir dann zwischen zwei Strings "Ähnlich" oder "nicht ähnlich", hilft aber hier nicht weiter.
@Stefan: So in die Richtung habe ich auch schon gedacht, aber dann kam ich auch mit den mehreren Worten durcheinander bzw. komme mit diversen Fallunterscheidungen durcheinander...
Als Mensch sieht man das auf einen Blick, das muss doch auch am Rechner irgendwie gehen...
Edit: Wenn ich den Links bei Wiki folge: Um den Suffix-Tree möchte ich eigentlich drumrum kommen 
_________________ We are, we were and will not be.
|
|
Regan
      
Beiträge: 2157
Erhaltene Danke: 72
Java (Eclipse), Python (Sublimetext 3)
|
Verfasst: Do 26.07.07 16:52
Wenn die Strings unterteilt werden, also z.b. durch Semikolon, dann könntest du doch einfach die Variante von Stefan.Buchholtz nehmen. Nur würde ich dann erst die Teilstrings nochmals unterteilen und dann den dominierenden String suchen.
Beispiel: Ottos Bilder (Kiste 1 von 2); Ottos Bilder (Kiste 2 von 2); Ottos Bilder (Kiste 1 von 2); Ottos Bilder (Kiste 1 von 2);
1.: 1x Ottos, 1x Bilder, 1x (Kiste 1x 1 1x von 1x 2)
...
Wobei da wieder das problem besteht mit den anderen Teilstrings, die ja auch gleich sein können.
Für die ersten beiden würd ich es so wie Stefan.Buchholtz machen. Das ist die Beste Variante, wenn es darum geht einwörtige (  ) Strings zu seperatieren.
|
|
alzaimar
      
Beiträge: 2889
Erhaltene Danke: 13
W2000, XP
D6E, BDS2006A, DevExpress
|
Verfasst: Do 26.07.07 17:54
Eine uralte Möglichkeit ist es, 4-Gramme zu bilden und deren Häufigkeit zu zählen. 4-Gramme (oder Tupel) sind einfach 4 aufeinanderfolgende Zeichen. Nachdem Du die Füllwörter (von, vom, es, der ,die, das, ab, auf etc.) entfernt hast (*), erstellst du einfach eine Liste der 4-Gramme und deren Häufigkeit. Die häufigsten (Wieviele? rumprobieren) merkst du Dir zusammen mit der relativen Häufigkeit und speicherst sie zusammen mit dem Text. Auf diese Weise kann man eine simple und sehr schnelle Volltextindexierung erstellen.
Zwei Texte sind dann vom Inhalt her ähnlich, wenn deren 4-Gramm Liste ähnlich ist. Und zwei kurze Listen vergleichst Du z.B. mit einem modifizierten Levenshtein oder einem heuristischen Ansatz. Vielleicht hilft auch ein DAWG.
Phonetische Vergleiche werden nicht funktionieren, denn
"Paul,Otto,Otto,Otto" hört sich nun mal komplett anders an als "Otto,Paul,Otto,Karl,Otto'...
(*) Nachtrag: Und natürlich die Sonderzeichen rausschmeissen, Umlaute vereinfachen etc.
Ach, ich sehe gerade, das Du den 'dominierenden String' im Klartext haben willst... Dafür eignet sich das nicht.
_________________ Na denn, dann. Bis dann, denn.
|
|
Gausi 
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Do 26.07.07 18:27
Ich glaube, ich frickel mir das mal so zusammen:
Zuerst suche ich mir in der Liste den kürzesten String raus. (geht fix)
Den entferne ich aus der Liste und teste, ob der bei den anderen passt (d.h. die Zeichen stimmen bis auf höchstens eine Abweichung überein). Dabei lasse ich dann evtl. auch noch einen Fehler zu, d.h. einmal (oder auch zweimal) darf es nicht passen. (geht auch fix)
Wenn das nicht klappt (weil der kürzeste der Ausreißer war, den ich erlauben will), probiere ich dasselbe nochmal (also mit dem zweitkürzesten). Wenn das auch nicht klappt, dann sind mindestens zwei Strings dabei, die nicht in die Reihe passen, und dann ist das Ergebnis "Verschieden". (geht wieder fix)
Da das Problem eigentlich eh nur Fehlerbereinigung ist, könnte das ausreichen. Wenn der User dann nicht damit zufrieden ist, soll der gefälligst seine ID3-Tags aufräumen.  - Nur zur Zeit steht da noch etwas zuoft "Various Artist - Unknown Compilation".
Probier ich mal aus, ob das ausreichend vernünftige Ergebnisse liefert...
Vielleicht was konkreter: Ich habe eine Liste mit Liedern, und ich möchte herausfinden, was das für ein Album ist, und von wem. Und mich ärgern Dinge wie "feat. irgendwer sonst" und "Sampler CD1"/"Sampler CD2", "Sampler Disk 1 of 2"/"Sampler Disk 2 of 2", "Bonus Disk" etc.
_________________ We are, we were and will not be.
|
|
Jakob_Ullmann
      
Beiträge: 1747
Erhaltene Danke: 15
Win 7, *Ubuntu GNU/Linux*
*Anjuta* (C, C++, Python), Geany (Vala), Lazarus (Pascal), Eclipse (Java)
|
Verfasst: Do 26.07.07 19:22
Ich hab gewusst, dass das für Nemp bestimmt war. 
|
|
Gausi 
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Do 26.07.07 20:27
Ja nu, das war doch fast klar, oder? 
_________________ We are, we were and will not be.
|
|
Heiko
      
Beiträge: 3169
Erhaltene Danke: 11
|
Verfasst: Do 26.07.07 20:54
Kleine Zwischenfrage, damit ich mich ins Proböem einfinde:
Was soll hier rauskommen?: Otto, Heinz und Otto, Heinz
Soll da ur Heinz rauskommen oder beide?
@nemp: Warum suchst du nicht direkt nach feat etc.? Sind doch meistens die gleichen Wörter...
|
|
Gausi 
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Do 26.07.07 21:13
Bei dem Beispiel wäre es mir fast egal, was bei rauskommt. So genau nehme ich es da nicht.
Nach "feat." direkt suchen: OK, aber dann müsste ich auch nach "ft.", "featuring", "vs." etc suchen und das sind dann ein bissel viel suchen, die man da durchführen müsste. Das würde ich gerne in einem Rutsch erledigen.
_________________ We are, we were and will not be.
|
|
Heiko
      
Beiträge: 3169
Erhaltene Danke: 11
|
Verfasst: Do 26.07.07 21:23
Die Suche nach solchen Substrings müsste aber effizienter sein, denn ansonsten musst du einen ganzen Baum oder so aufspannen, was aufwändiger sein dürfte.
Allerdings gebe ich dir Recht, dass deine Variante besser sein dürfte, denn ich habe auch paat Titel, die mit & getrennt sind...
|
|
MiChri
      
Beiträge: 28
Win XP, Win 2000, Win 98, Linux
D4 Prof
|
Verfasst: Fr 27.07.07 10:43
Hallo
Mach es doch so:
1. Aufteilen in Substrings (";")
2. Iteriere über Buchstaben (0<i<StrLen) von Anfang an und...
Iteriere über alle Substrings
bis 40% rausgeflogen sind (unterscheiden sich vom Durchschnitt)
Beispiel
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:
| Eingabe: Otto Karl;Otto Kalle;Ottomotor;Otmar; Iterationen: i=1 (O)tto Karl (O)tto Kalle (O)ttomotor (O)tmar 100% O
i=2 O(t)to Karl O(t)to Kalle O(t)tomotor O(t)mar 100% t
Ot(t)o Karl Ot(t)o Kalle Ot(t)omotor Ot(m)ar 75% t
Ott(o) Karl Ott(o) Kalle Ott(o)motor //Otmar wird nicht mehr beachtet 75% o
Otto( )Karl Otto( )Kalle Otto(m)otor //Otmar 50% " " => terminiert |
Laufzeit ist linear in der Eingabelänge
mfg
Christian
|
|
Gausi 
      
Beiträge: 8548
Erhaltene Danke: 477
Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
|
Verfasst: Fr 27.07.07 14:38
@MiChri: Dann bleibt immer noch das Problem, den dominierenden Char zu finden  Das könnte man bei AnsiStrings zwar über ein Array[0..255] of Integer machen, aber besonders effizient wäre das wahrscheinlich nicht. Außerdem brauche ich das ganze auch für Widestrings.
Ich hab das jetzt wie folgt gelöst, und es scheint einigermaßen zu funktionieren.
Zuerst eine Funktion, die bestimmt, ob zwei Strings ähnlich sind. Ist ein String länger, als der andere, ist das egal. Als Distanz nehme ich die Hamming-Distanz. Zusätzlich wird zurückgeliefert, wo der erste Fehler auftritt.
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:
| function SameString(string1, string2: WideString; tolerance: Integer; var Fehlstelle: Integer): Boolean; var tmp: WideString; i, c: Integer; begin if length(string1) > length(string2) then begin tmp := string1; string1 := string2; string2 := tmp; end; c := 0; for i := 1 to length(string1) do begin if WideUpperCase(string1[i]) <> WideUpperCase(string2[i]) then begin inc(c); if Fehlstelle = 0 then Fehlstelle := i; end; if c > tolerance then break; end; result := c <= tolerance; end; |
Diese Funktion verwende ich nun in GetCommonString. Als Vergleichstring nehme ich den kürzesten. Dabei lasse ich einen Fehltritt zu. Wenn das fehlschlägt, probiere ich es nochmal mit dem zweitkürzesten String (die Anzahl der Versuche hängt von der Zahl der Strings ab). Sind auch im letzten Durchlauf zuviele "falsche Strings" gefunden worden, dann gebe ich als Ergebnis den String bis dahin zurück, wo der Fehler auftritt, wenn das weit hinten passiert (Das brauche ich z.B. bei Maxis, wo die Album-Information "EinTollesLied (Radio Edit)", "EinTollesLied (Extended)" etc. ist). Ansonsten gibts keinen gemeinsamen String.
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:
| function GetCommonString(Strings: TTntStringlist; tolerance: Integer; var Fehlstelle: Integer): WideString; var minlength, minidx, i: Integer; checkStr: WideString; durchlauf, durchlaufmax, errorCount: Integer; begin durchlauf := 0; errorCount := 0; Fehlstelle := 0; CheckStr := ''; case Strings.Count of 0..1: durchlaufmax := 0; 2..4: durchlaufmax := 1; 5..9: durchlaufmax := 2 else durchlaufmax := 3; end;
if Strings.Count > 0 then repeat minlength := length(Strings[0]); minidx := 0; for i := 1 to Strings.Count - 1 do begin if length(Strings[i]) < minlength then begin minlength := length(Strings[i]); minidx := i; end; end; checkStr := Strings[minidx]; Strings.Delete(minidx);
errorCount := 0; Fehlstelle := 0; for i := 0 to Strings.Count - 1 do begin if not SameString(checkStr, Strings[i], tolerance, Fehlstelle) then inc(errorCount); if errorCount > 1 then break; end;
inc(durchlauf); until (durchlauf > durchlaufmax) or (errorCount <= 1) or (strings.Count = 0);
if ErrorCount <= 1 then result := CheckStr else begin if fehlstelle <= length(CheckStr) Div 2 + 1 then result := '' else begin result := copy(CheckStr, 1, fehlstelle - 1) + ' ... '; fehlstelle := 0; end; end; end; |
Das ganze läuft sehr fix durch und liefert recht brauchbare Ergebnisse. Das Hinzufügen von "..." bewirkt bei den Mehr-CD-Alben dann etwas wie "Ein Album CD..." anstelle von "Ein Album CD1 von 2".
_________________ We are, we were and will not be.
|
|
MiChri
      
Beiträge: 28
Win XP, Win 2000, Win 98, Linux
D4 Prof
|
Verfasst: Fr 27.07.07 14:49
Hi
Hast natürlich recht, das mit dem dominierenden Zeichen pro Schleifendurchlauf bin ich dir schuldig geblieben.
Mit Ascii/Ansi ist aber wie du schon sagtest das mit nem Array erledigt (O(1)), das geht zwar bei WideString auch, wäre aber dämlich, also nimmt man da ne Hashtable mit average
O(log(n)) oder gleich nen Baum (O(log(n)))
Also bleibt man auf jedenfall unter deinen O(n^2)
Sorry, war nur ne Anmerkung, und in deinem Anwendungsfall wird ja n nicht groß, sondern
vor allem die Anzahl der Ausführungen des ganzen Algos. Da kann deine
Lösung u.U. sogar schneller sein (bei kleiner Anzahl von Substrings).
mfg
Christian
|
|
alzaimar
      
Beiträge: 2889
Erhaltene Danke: 13
W2000, XP
D6E, BDS2006A, DevExpress
|
Verfasst: Fr 27.07.07 15:24
MiChri hat folgendes geschrieben: | H...also nimmt man da ne Hashtable mit average
O(log(n)) oder gleich nen Baum (O(log(n)))
Also bleibt man auf jedenfall unter deinen O(n^2) . |
Nope... Hashtable = O(1), Skiplist auch., Baum ist dann wieder O(log n).
_________________ Na denn, dann. Bis dann, denn.
|
|
MiChri
      
Beiträge: 28
Win XP, Win 2000, Win 98, Linux
D4 Prof
|
Verfasst: Sa 28.07.07 19:37
Moin
Das Einfügen in eine Hashtable ist O(1) das Suchen nicht (worstcase ist MöglicheWerte/FächerInDerTabelle).
In diesem Speziellen Fall wird man eine average Zeit von O(1) aber hinbekommen.
mfg
Christian
|
|
|