Ich habe ein Array von 100.000 Zeichenfolgen, die alle die Länge . Ich möchte jede Zeichenfolge mit jeder anderen Zeichenfolge vergleichen, um festzustellen, ob sich zwei Zeichenfolgen um ein Zeichen unterscheiden. Im Moment, wenn ich jede Zeichenfolge zum Array hinzufüge, überprüfe ich sie mit jeder Zeichenfolge, die sich bereits im Array befindet und eine zeitliche Komplexität von .n ( n - 1 )
Gibt es eine Datenstruktur oder einen Algorithmus, mit denen Zeichenfolgen schneller miteinander verglichen werden können, als ich es bereits tue?
Einige zusätzliche Informationen:
Reihenfolge ist wichtig:
abcde
undxbcde
unterscheiden sich um 1 Zeichen, währendabcde
undedcba
unterscheiden sich um 4 Zeichen.Für jedes Zeichenfolgenpaar, das sich um ein Zeichen unterscheidet, entferne ich eine dieser Zeichenfolgen aus dem Array.
Im Moment suche ich nach Zeichenfolgen, die sich nur um 1 Zeichen unterscheiden. Es wäre jedoch schön, wenn der Unterschied von 1 Zeichen auf beispielsweise 2, 3 oder 4 Zeichen erhöht werden könnte. In diesem Fall ist jedoch die Effizienz meiner Meinung nach wichtiger als die Möglichkeit, das Zeichenunterschiedslimit zu erhöhen.
liegt üblicherweise im Bereich von 20 bis 40.
Antworten:
Es ist möglich, die Worst-Case-Laufzeit von zu erreichen .O ( n k logk )
Fangen wir einfach an. Wenn Sie sich für eine einfach zu implementierende Lösung interessieren, die für viele Eingaben, aber nicht für alle, effizient ist, finden Sie hier eine einfache, pragmatische und einfach zu implementierende Lösung, die in der Praxis für viele Situationen ausreicht. Im schlimmsten Fall wird jedoch auf die quadratische Laufzeit zurückgegriffen.
Nehmen Sie jede Zeichenfolge und speichern Sie sie in einer Hash-Tabelle, die auf der ersten Hälfte der Zeichenfolge angegeben ist. Dann iterieren Sie über die Eimer mit den Hashtabellen. Überprüfen Sie für jedes Zeichenfolgenpaar im selben Bucket, ob sie sich in einem Zeichen unterscheiden (dh, ob sich ihre zweite Hälfte in einem Zeichen unterscheidet).
Nehmen Sie dann jede Zeichenfolge und speichern Sie sie in einer Hash-Tabelle, diesmal in der zweiten Hälfte der Zeichenfolge. Überprüfen Sie erneut jedes Saitenpaar im selben Eimer.
Unter der Annahme , werden die Saiten gut verteilt ist , wird die Laufzeit wahrscheinlich etwa . Wenn es ein Paar von Zeichenfolgen gibt, die sich um 1 Zeichen unterscheiden, wird es in einem der beiden Durchgänge gefunden (da sie sich nur um 1 Zeichen unterscheiden, muss sich dieses unterschiedliche Zeichen entweder in der ersten oder in der zweiten Hälfte der Zeichenfolge befinden). Die zweite oder erste Hälfte der Zeichenfolge muss also identisch sein. Im ungünstigsten Fall (z. B. wenn alle Zeichenfolgen mit denselben k / 2 Zeichen beginnen oder enden ) wird die Laufzeit auf 0 ( n 2 k ) herabgesetzt, sodass die Laufzeit im ungünstigsten Fall keine Verbesserung der Brute Force darstellt .O ( n k ) k / 2 O ( n2k )
Wenn in einem Bucket zu viele Zeichenfolgen enthalten sind, können Sie zur Leistungsoptimierung denselben Vorgang rekursiv wiederholen, um nach einem Paar zu suchen, das sich um ein Zeichen unterscheidet. Der rekursive Aufruf erfolgt auf Zeichenketten der Länge .k / 2
Wenn Ihnen die Worst-Case-Laufzeit am Herzen liegt:
Mit der obigen Leistungsoptimierung glaube ich, dass die Laufzeit im ungünstigsten Fall .O ( n k logk )
quelle
Meine Lösung ähnelt der von j_random_hacker, verwendet jedoch nur einen einzigen Hash-Satz.
Ich würde einen Hash-Satz von Zeichenfolgen erstellen. Fügen Sie für jede Zeichenfolge in der Eingabe die Menge Zeichenfolgen hinzu. Ersetzen Sie in jeder dieser Zeichenfolgen einen der Buchstaben durch ein Sonderzeichen, das in keiner der Zeichenfolgen enthalten ist. Überprüfen Sie beim Hinzufügen, ob sie nicht bereits im Satz enthalten sind. Wenn dies der Fall ist, haben Sie zwei Zeichenfolgen, die sich nur um (höchstens) ein Zeichen unterscheiden.k
Ein Beispiel mit Strings 'abc', 'adc'
Für abc fügen wir '* bc', 'a * c' und 'ab *' hinzu
Für adc addieren wir '* dc', 'a * c' und 'ad *'
Wenn wir 'a * c' zum zweiten Mal hinzufügen, bemerken wir, dass es bereits in der Menge enthalten ist, sodass wir wissen, dass es zwei Zeichenfolgen gibt, die sich nur durch einen Buchstaben unterscheiden.
Die Gesamtlaufzeit dieses Algorithmus beträgt . Dies liegt daran, dass wir k neue Zeichenfolgen für alle n Zeichenfolgen in der Eingabe erstellen . Für jede dieser Zeichenfolgen müssen wir den Hash berechnen, der normalerweise O ( k ) -Zeit benötigt.O ( n ∗ k2) k n O ( k )
Das Speichern aller Zeichenfolgen benötigt Platz.O ( n ∗ k2)
Weitere Verbesserungen
Wir können den Algorithmus weiter verbessern, indem wir die geänderten Zeichenfolgen nicht direkt speichern, sondern ein Objekt mit einem Verweis auf die ursprüngliche Zeichenfolge und den Index des maskierten Zeichens. Auf diese Weise brauchen wir nicht alle Saiten zu schaffen und wir brauchen nur Raum alle Objekte zu speichern.O ( n ∗ k )
Sie müssen eine benutzerdefinierte Hash-Funktion für die Objekte implementieren. Wir können die Java-Implementierung als Beispiel nehmen, siehe die Java-Dokumentation . Der Java-Hashcode multipliziert den Unicode-Wert jedes Zeichens mit (wobei k die Zeichenfolgenlänge und i der Index des Zeichens auf einer Basis ist. Beachten Sie, dass sich jede geänderte Zeichenfolge nur um ein Zeichen vom Original unterscheidet. Wir können dies leicht berechnen den Beitrag dieses Zeichens zum Hash-Code. Wir können diesen subtrahieren und stattdessen unser Maskierungszeichen hinzufügen. Für die Berechnung wird O ( 1 ) benötigt . Dadurch können wir die Gesamtlaufzeit auf O ( n) verringern31k - i k ich O ( 1 ) O ( n ∗ k )
quelle
equals
undhashCode
Methoden, die funktionieren könnten. Nur die a * b-artige Zeichenfolge in diesen Methoden zu erstellen, sollte es kugelsicher machen; Ich vermute, dass einige der anderen Antworten hier Hash-Kollisionsprobleme haben werden.*bc
,a*c
,ab*
. Ich frage mich, ob es unmöglich gezeigt werden könnte?Ich würde Hashtabellen H 1 , ... , H k erstellen , von denen jede eine ( k - 1 ) -lange Zeichenfolge als Schlüssel und eine Liste von Zahlen (Zeichenfolgen-IDs) als Wert hat. Die Hash-Tabelle H i enthält alle Zeichenfolgen, die bisher verarbeitet wurden, jedoch mit dem Zeichen an der Position, an der ich gelöscht habe . Wenn beispielsweise k = 6 ist , enthält H 3 [ A B D E F ] eine Liste aller bisher gesehenen Zeichenfolgen mit dem Muster Ak H1, … , Hk ( k - 1 ) Hich ich k = 6 H3[ A B D EF] , wobei ⋅ "beliebiges Zeichen" bedeutet. Dann, um die j- te Eingabezeichenfolge s j zu verarbeiten :A B ⋅ D EF ⋅ j sj
Wenn wir jeden Hash-Schlüssel explizit speichern, müssen wir den -Raum verwenden und damit mindestens zeitliche Komplexität haben. Aber wie von Simon Prins beschrieben , ist es möglich, eine Reihe von Modifikationen an einer Zeichenkette (in seinem Fall als Ändern einzelner Zeichen in , in meinem Fall als Löschen beschrieben) implizit so darzustellen, dass alle k Hash-Schlüssel für eine bestimmte Zeichenkette nur brauchen O ( k ) Raum, was zu O ( n k ) Raum insgesamt führt und die Möglichkeit von O ( n k ) eröffnetO ( n k2) k O ( k ) O ( n k ) O ( n k ) Zeit auch. Um diese Zeitkomplexität zu erreichen, müssen die Hashes für alle Variationen einer Länge k in O ( k ) berechnet werden. Dies kann beispielsweise mithilfe von Polynom-Hashes erfolgen, wie von DW vorgeschlagen (und das ist der Fall) wahrscheinlich viel besser als einfach das gelöschte Zeichen mit dem Hash für die ursprüngliche Zeichenkette zu XOREN.k k O ( k )
*
Der implizite Repräsentationstrick von Simon Prins bedeutet auch, dass das "Löschen" der einzelnen Zeichen nicht tatsächlich ausgeführt wird, sodass wir die übliche Array-basierte Repräsentation einer Zeichenfolge ohne Leistungseinbußen verwenden können (anstelle von verknüpften Listen, wie ich ursprünglich vorgeschlagen hatte).
quelle
Hier ist ein robusterer Hashtable-Ansatz als die Polynom-Hash-Methode. Generieren Sie zunächst zufällige positive ganze Zahlen r 1 .. k , die mit der Hash-Tabellengröße M übereinstimmen . Es gilt nämlich 0 ≤ r i < M . Dann hash jede Saite x 1 .. k bis ( Σ k i = 1 x i r i ) mod M . Es gibt fast nichts, was ein Gegner tun kann, um sehr ungleichmäßige Kollisionen zu verursachen, da Sie zur Laufzeit r 1 .. k und damit k erzeugenk r1 .. k M 0 ≤ rich<M x1..k (∑ki=1xiri)modM r1..k k erhöht die maximale Wahrscheinlichkeit einer Kollision zweier beliebiger von verschiedenen Saiten schnell geht . Es ist auch offensichtlich, wie in O ( k ) -Zeit alle möglichen Hashes für jede Zeichenfolge mit einem geänderten Zeichen berechnet werden .1/M O(k)
Wenn Sie wirklich ein einheitliches Hashing garantieren möchten, können Sie für jedes Paar ( i , c ) für i von 1 bis k und für jedes Zeichen c eine zufällige natürliche Zahl kleiner als M generieren und dann jede Zeichenfolge hashen x 1 .. k bis ( ∑ k i = 1 r ( i , x i ) ) mod Mr(i,c) M (i,c) i 1 k c x1..k (∑ki=1r(i,xi) ) mod M . Dann ist die Wahrscheinlichkeit der Kollision zweier beliebiger von verschiedenen Zeichenfolgen genau . Dieser Ansatz ist besser, wenn Ihr Zeichensatz im Vergleich zu n relativ klein ist .1/M n
quelle
Viele der hier veröffentlichten Algorithmen belegen ziemlich viel Platz in Hash-Tabellen. Hier ist ein einfacher -Zusatzspeicher- O ( ( n lg n ) ⋅ k 2 ) -Laufzeitalgorithmus.O(1) O((nlgn)⋅k2)
Der Trick besteht darin, , einen Komparator zwischen zwei Werten a und b , der true zurückgibt, wenn a < b (lexikographisch), während das k- te Zeichen ignoriert wird . Dann ist der Algorithmus wie folgt.Ck(a,b) a b a<b k
Sortieren Sie zunächst die Zeichenfolgen regelmäßig und führen Sie einen linearen Scan durch, um alle Duplikate zu entfernen.
Dann gilt für jedes :k
Sortieren Sie die Zeichenfolgen mit als Komparator.Ck
Zeichenfolgen, die sich nur in liegen jetzt nebeneinander und können in einem linearen Scan erkannt werden.k
quelle
Zwei Zeichenfolgen der Länge k , die sich in einem Zeichen unterscheiden, teilen sich ein Präfix der Länge l und ein Suffix der Länge m, so dass k = l + m + 1 ist .
Die Antwort von Simon Prins kodiert das alles durch das Speichern Präfix / Suffix - Kombinationen explizit, dh
abc
wird*bc
,a*c
undab*
. Das ist k = 3, l = 0,1,2 und m = 2,1,0.Wie valarMorghulis betont, können Sie Wörter in einem Präfixbaum organisieren. Es gibt auch den sehr ähnlichen Suffixbaum. Es ist ziemlich einfach, den Baum mit der Anzahl der Blattknoten unter jedem Präfix oder Suffix zu erweitern. Dies kann in O (k) aktualisiert werden, wenn ein neues Wort eingefügt wird.
Der Grund, warum Sie diese Anzahl von Geschwistern wünschen, ist, dass Sie bei einem neuen Wort wissen, ob Sie alle Zeichenfolgen mit demselben Präfix oder alle Zeichenfolgen mit demselben Suffix aufzählen möchten. ZB für "abc" als Eingabe sind die möglichen Präfixe "", "a" und "ab", während die entsprechenden Suffixe "bc", "c" und "" sind. Wie es offensichtlich ist, ist es für kurze Suffixe besser, Geschwister im Präfixbaum aufzulisten und umgekehrt.
Wie @einpoklum hervorhebt, ist es durchaus möglich, dass alle Zeichenfolgen dasselbe k / 2- Präfix haben. Das ist für diesen Ansatz kein Problem. Der Präfixbaum ist linear bis zur Tiefe k / 2, wobei jeder Knoten bis zur Tiefe k / 2 der Vorfahr von 100.000 Blattknoten ist. Infolgedessen wird der Suffixbaum bis zu einer Tiefe von (k / 2-1) verwendet, was gut ist, da sich die Zeichenfolgen in ihren Suffixen unterscheiden müssen, da sie Präfixe gemeinsam haben.
[Bearbeiten] Wenn Sie als Optimierung das kürzeste eindeutige Präfix eines Strings ermittelt haben, wissen Sie, dass es das letzte Zeichen des Präfixes sein muss , wenn es ein anderes Zeichen gibt, und Sie hätten das nahezu doppelte gefunden, wenn Überprüfung eines kürzeren Präfixes. Wenn "abcde" also das kürzeste eindeutige Präfix "abc" hat, bedeutet dies, dass es andere Zeichenfolgen gibt, die mit "ab?" Beginnen. aber nicht mit "abc". Wenn sie sich also nur in einem Zeichen unterscheiden würden, wäre dies das dritte Zeichen. Sie müssen nicht mehr nach "abc? E" suchen.
Wenn Sie nach der gleichen Logik feststellen würden, dass "cde" ein eindeutiges kürzestes Suffix ist, müssen Sie nur das Präfix "ab" der Länge 2 und nicht die Präfixe der Länge 1 oder 3 überprüfen.
Beachten Sie, dass diese Methode nur für genau einen Zeichenunterschied funktioniert und nicht auf zwei Zeichenunterschiede verallgemeinert wird. Dabei wird ein einziges Zeichen als Trennung zwischen identischen Präfixen und identischen Suffixen verwendet.
quelle
Das Speichern von Zeichenfolgen in Eimern ist ein guter Weg (es gibt bereits unterschiedliche Antworten, die dies umreißen).
Eine alternative Lösung könnte darin bestehen, Zeichenfolgen in einer sortierten Liste zu speichern . Der Trick besteht darin, nach einem lokalitätsabhängigen Hashing-Algorithmus zu sortieren . Dies ist ein Hash-Algorithmus, der ähnliche Ergebnisse liefert, wenn die Eingabe ähnlich ist [1].
Jedes Mal, wenn Sie eine Zeichenfolge untersuchen möchten, können Sie ihren Hash berechnen und die Position dieses Hashs in Ihrer sortierten Liste nachschlagen (wobei Sie für Arrays oder O ( n ) für verknüpfte Listen verwenden). Wenn Sie feststellen, dass die Nachbarn (unter Berücksichtigung aller engen Nachbarn, nicht nur derjenigen mit einem Index von +/- 1) dieser Position ähnlich sind (um ein Zeichen versetzt), haben Sie Ihre Übereinstimmung gefunden. Wenn es keine ähnlichen Zeichenfolgen gibt, können Sie die neue Zeichenfolge an der gefundenen Position einfügen (wobei O ( 1 ) für verknüpfte Listen und O ( n ) für Arrays verwendet wird).O ( l o g( n ) ) O ( n ) O ( 1 ) O ( n )
Ein möglicher lokalitätsabhängiger Hashing-Algorithmus könnte Nilsimsa sein (mit Open-Source-Implementierung, die beispielsweise in Python verfügbar ist ).
[1]: Beachten Sie, dass häufig Hash-Algorithmen wie SHA1 auf das Gegenteil ausgelegt sind: Sie erzeugen sehr unterschiedliche Hashes für ähnliche, aber nicht gleiche Eingaben.
Haftungsausschluss: Um ehrlich zu sein, würde ich persönlich eine der verschachtelten / baumstrukturierten Bucket-Lösungen für eine Produktionsanwendung implementieren. Die sortierte Listenidee erschien mir jedoch als interessante Alternative. Beachten Sie, dass dieser Algorithmus stark vom gewählten Hash-Algorithmus abhängt. Nilsimsa ist ein Algorithmus, den ich gefunden habe - es gibt jedoch noch viele andere (zum Beispiel TLSH, Ssdeep und Sdhash). Ich habe nicht überprüft, ob Nilsimsa mit meinem beschriebenen Algorithmus funktioniert.
quelle
Man könnte die Lösung in erzielt Zeit und O ( n k ) Raum unter Verwendung von verbesserten Suffixarray ( Suffixarray zusammen mit dem LCP - Array ) , die konstante Zeit LCP (längster gemeinsamen Präfix) Abfrage ermöglicht (dh Für zwei gegebene Indizes eines Strings, wie lang ist das längste Präfix der Suffixe, die bei diesen Indizes beginnen? Hier könnten wir die Tatsache ausnutzen, dass alle Saiten gleich lang sind. Speziell,O ( n k + n2) O ( n k )
Erstellen Sie das erweiterte Suffix-Array aller zusammen verketteten Zeichenfolgen. Sei X = x 1 . x 2 . x 3 . . . . x n wobei x i , ∀ 1 ≤ i ≤ n eine Zeichenfolge in der Auflistung ist. Baue das Suffix - Array und LCP - Array für X .n X= x1. x2. x3. . . . xn xich, ≤ 1 ≤ i ≤ n X
Nun beginnt jedes an der Position ( i - 1 ) k in der auf Null basierenden Indizierung. Nehmen Sie für jede Zeichenfolge x i LCP mit jeder Zeichenfolge x j, so dass j < i ist . Wenn LCP über das Ende von x j hinausgeht, ist x i = x j . Andernfalls liegt eine Nichtübereinstimmung vor (z. B. x i [ p ] ≠ x j [ p ]).xich ( i - 1 ) k xich xj j < i xj xich= xj xich[ p ] ≠ xj[ p ] ); Nehmen Sie in diesem Fall ein weiteres LCP, beginnend an den entsprechenden Positionen nach der Nichtübereinstimmung. Wenn die zweite LCP über das Ende geht dann x i und x j von nur ein Zeichen unterscheiden; Ansonsten gibt es mehr als eine Fehlanpassung.xj xich xj
Sie können die SDSL-Bibliothek verwenden , um das Suffix-Array in komprimierter Form zu erstellen und die LCP-Abfragen zu beantworten.
Analyse: Der Aufbau des verbesserten Suffixarray ist linear in der Länge von dh O ( n k ) . Jede LCP-Abfrage benötigt eine konstante Zeit. Die Abfragezeit ist also O ( n 2 ) .X O ( n k ) O ( n2)
quelle
k
*
*bcde
a*cde
Sie können diesen Ansatz auch verwenden, um die Arbeit auf mehrere CPU- / GPU-Kerne aufzuteilen.
quelle
Dies ist eine Kurzversion der Antwort von @SimonPrins ohne Hashes.
Angenommen, keine Ihrer Zeichenfolgen enthält einen Stern:
Eine alternative Lösung mit impliziter Verwendung von Hashes in Python (kann der Schönheit nicht widerstehen):
quelle
Hier ist meine Einstellung zum 2+ Mismatches Finder. Beachten Sie, dass ich in diesem Beitrag jede Zeichenfolge als kreisförmig betrachte, z. B. besteht die Teilzeichenfolge mit der Länge 2 am Index
k-1
aus dem Symbolstr[k-1]
gefolgt vonstr[0]
. Und der Teilstring mit der Länge 2 am Index-1
ist derselbe!M
k
M
k=20
M=4
abcd*efgh*ijkl*mnop*
Der Algorithmus zum Durchsuchen aller
M
Symbole stimmt jetzt nicht mit den Symbolfolgenk
überein:str[i..i+L-1]
, nach denenL = mlen(k,M)
. WennL=4
Sie beispielsweise ein Alphabet mit nur 4 Symbolen (aus DNA) haben, werden 256 Gruppen gebildet.L
, die wir bereits abgeglichen habenstr[i..i+L1-1]
, nach denenL1 = mlen(k-L,M)
. ZB wennk=20, M=4, alphabet of 4 symbols
jaL=4
undL1=3
das ergibt 64 Gruppen.Warum fangen wir nicht
j
bei 0 an? Da wir diese Gruppen bereits mit demselben Wert von erstellt habeni
, entspricht job withj<=i-L
genau dem Job mit vertauschten i- und j-Werten.Weitere Optimierungen:
str[i..i+L-2] & str[i+L]
. Dies verdoppelt nur die Anzahl der geschaffenen Arbeitsplätze, ermöglicht aber eine ErhöhungL
um 1 (wenn meine Rechnung korrekt ist). Anstelle von 256 Gruppen werden Sie also Daten in 1024 Gruppen aufteilen.*
0..k-1
M-1
k-1
quelle
Ich arbeite jeden Tag daran, Algen zu erfinden und zu optimieren. Wenn Sie also ein bisschen Leistung benötigen, ist dies der Plan:
*
in jeder Position unabhängig, dh anstelle von einzelnenn*k
Jobverarbeitungszeichenfolgenvarianten - starten Siek
unabhängige Jobs, die jeweilsn
Zeichenfolgen überprüfen . Sie können diesek
Jobs auf mehrere CPU- / GPU-Kerne verteilen . Dies ist besonders wichtig, wenn Sie Unterschiede zwischen 2 und mehr Zeichen überprüfen möchten. Eine geringere Auftragsgröße verbessert auch die Cache-Lokalität, wodurch das Programm 10x schneller wird.*
der i-ten Position) und des Zeichenfolgenindex enthalten, und sortieren Sie sie dann oder erstellen Sie eine Hash-Tabelle aus diesen Datensätzen.Zum Sortieren können Sie die folgende Kombination ausprobieren:
quelle