J'ai un tableau de 100 000 cordes, toutes de longueur . Je veux comparer chaque chaîne à chaque autre chaîne pour voir si deux chaînes différentes diffèrent d'un caractère. En ce moment, comme j'ajoute chaque chaîne au tableau, je le compare à chaque chaîne déjà dans le tableau, qui a une complexité temporelle de .
Existe-t-il une structure de données ou un algorithme qui peut comparer les chaînes les unes aux autres plus rapidement que ce que je fais déjà?
Quelques informations supplémentaires:
L'ordre est important:
abcde
etxbcde
diffère de 1 caractère,abcde
etedcba
diffère de 4 caractères.Pour chaque paire de chaînes qui diffèrent d'un caractère, je supprimerai l'une de ces chaînes du tableau.
En ce moment, je recherche des chaînes qui ne diffèrent que par 1 caractère, mais ce serait bien si cette différence de 1 caractère pouvait être augmentée à, disons, 2, 3 ou 4 caractères. Cependant, dans ce cas, je pense que l'efficacité est plus importante que la capacité d'augmenter la limite de différence de caractères.
est généralement compris entre 20 et 40.
Réponses:
Il est possible d'atteindre pire des cas.O(nklogk)
Commençons simple. Si vous vous souciez d'une solution facile à implémenter qui sera efficace sur de nombreuses entrées, mais pas sur toutes, voici une solution simple, pragmatique et facile à implémenter qui suffit en pratique pour de nombreuses situations. Cependant, cela revient au temps de fonctionnement quadratique dans le pire des cas.
Prenez chaque chaîne et stockez-la dans une table de hachage, calée sur la première moitié de la chaîne. Ensuite, parcourez les compartiments de table de hachage. Pour chaque paire de chaînes dans le même compartiment, vérifiez si elles diffèrent en 1 caractère (c'est-à-dire, vérifiez si leur seconde moitié diffère en 1 caractère).
Ensuite, prenez chaque chaîne et stockez-la dans une table de hachage, cette fois sur la seconde moitié de la chaîne. Vérifiez à nouveau chaque paire de cordes dans le même seau.
En supposant que les chaînes sont bien réparties, le temps d'exécution sera probablement d'environ . De plus, s'il existe une paire de chaînes qui diffèrent d'un caractère, on la trouvera lors de l'une des deux passes (puisqu'elles diffèrent d'un seul caractère, ce caractère différent doit être dans la première ou la seconde moitié de la chaîne, la deuxième ou la première moitié de la chaîne doit donc être la même). Cependant, dans le pire des cas (par exemple, si toutes les chaînes commencent ou se terminent avec les mêmes caractères k / 2 ), cela se dégrade en temps d'exécution O ( n 2 k ) , donc son temps d'exécution le plus défavorable n'est pas une amélioration de la force brute .O(nk) k/2 O(n2k)
En tant qu'optimisation des performances, si un compartiment contient trop de chaînes, vous pouvez répéter le même processus de manière récursive pour rechercher une paire qui diffère d'un caractère. L'invocation récursive se fera sur des chaînes de longueur .k/2
Si vous vous souciez du pire temps de fonctionnement:
Avec l'optimisation des performances ci-dessus, je crois que le temps d'exécution le plus défavorable est .O(nklogk)
la source
Ma solution est similaire à celle de j_random_hacker mais n'utilise qu'un seul ensemble de hachage.
Je créerais un ensemble de chaînes de hachage. Pour chaque chaîne de l'entrée, ajoutez à l'ensemble des chaînes. Dans chacune de ces chaînes, remplacez l'une des lettres par un caractère spécial, introuvable dans aucune des chaînes. Pendant que vous les ajoutez, vérifiez qu'ils ne sont pas déjà dans l'ensemble. S'ils le sont, vous avez deux chaînes qui ne diffèrent que par (au plus) un caractère.k
Un exemple avec les chaînes 'abc', 'adc'
Pour abc, nous ajoutons '* bc', 'a * c' et 'ab *'
Pour adc, nous ajoutons '* dc', 'a * c' et 'ad *'
Lorsque nous ajoutons 'a * c' la deuxième fois, nous remarquons qu'il est déjà dans l'ensemble, nous savons donc qu'il y a deux chaînes qui ne diffèrent que par une lettre.
Le temps de fonctionnement total de cet algorithme est . C'est parce que nous créons k nouvelles chaînes pour toutes les n chaînes dans l'entrée. Pour chacune de ces chaînes, nous devons calculer le hachage, ce qui prend généralement O ( k ) .O(n∗k2) k n O(k)
Le stockage de toutes les chaînes prend un espace .O(n∗k2)
Améliorations supplémentaires
Nous pouvons améliorer encore l'algorithme en ne stockant pas directement les chaînes modifiées mais en stockant plutôt un objet avec une référence à la chaîne d'origine et l'index du caractère masqué. De cette façon, nous n'avons pas besoin de créer toutes les chaînes et nous n'avons besoin que de l' espace pour stocker tous les objets.O(n∗k)
Vous devrez implémenter une fonction de hachage personnalisée pour les objets. Nous pouvons prendre l'implémentation Java comme exemple, voir la documentation java . Le java hashCode multiplie la valeur unicode de chaque caractère par (avec k la longueur de la chaîne et i l'index à base unique du caractère. Notez que chaque chaîne modifiée ne diffère que d'un caractère de l'original. Nous pouvons facilement calculer la contribution de ce caractère au code de hachage. Nous pouvons soustraire cela et ajouter notre caractère de masquage à la place. Cela prend O ( 1 ) à calculer. Cela nous permet de ramener la durée totale de fonctionnement à O ( n31k−i k i O(1) O(n∗k)
la source
equals
et deshashCode
méthodes qui pourraient fonctionner. La simple création de la chaîne de style a * b dans ces méthodes devrait la rendre à l'épreuve des balles; Je soupçonne que certaines des autres réponses ici auront des problèmes de collision de hachage.*bc
,a*c
,ab*
. Je me demande si cela pourrait être démontré impossible?Je ferais tables de hachage H 1 , … , H k , chacune ayant une chaîne de longueur ( k - 1 ) comme clé et une liste de nombres (ID de chaîne) comme valeur. La table de hachage H i contiendra toutes les chaînes traitées jusqu'à présent mais avec le caractère à la position i supprimé . Par exemple, si k = 6k H1,…,Hk (k−1) Hi i k=6 , alors contiendra une liste de toutes les chaînes vues jusqu'ici qui ont le modèle AH3[ABDEF] , où ⋅ signifie "n'importe quel caractère". Ensuite, pour traiter la j- ème chaîne d'entrée s j :AB⋅DEF ⋅ j sj
Si nous stockons explicitement chaque clé de hachage, alors nous devons utiliser l' espace et donc avoir au moins la complexité temporelle. Mais comme décrit par Simon Prins , il est possible de représenter une série de modifications à une chaîne (dans son cas décrit comme changeant des caractères simples en , dans le mien comme des suppressions) implicitement de telle manière que toutes les k clés de hachage pour une chaîne particulière aient juste besoin O ( k ) espace, menant aussi au O.O(nk2) k O(k) espace global, et ouvrant la possibilité de O ( n k )O(nk) O(nk) temps aussi. Pour atteindre cette complexité temporelle, nous avons besoin d'un moyen de calculer les hachages pour toutes les variations d'une chaîne de longueur k en temps O ( k ) : par exemple, cela peut être fait en utilisant des hachages polynomiaux, comme suggéré par DW (et c'est probablement beaucoup mieux que simplement XOR le caractère supprimé avec le hachage de la chaîne d'origine).k k O(k)
*
L'astuce de représentation implicite de Simon Prins signifie également que la «suppression» de chaque caractère n'est pas réellement effectuée, nous pouvons donc utiliser la représentation habituelle basée sur un tableau d'une chaîne sans pénalité de performance (plutôt que des listes liées comme je l'avais initialement suggéré).
la source
Voici une approche de table de hachage plus robuste que la méthode de hachage polynomial. Tout d' abord générer des nombres entiers positifs aléatoires r 1 .. k qui sont premiers entre eux à la taille table de hachage M . À savoir, 0 ≤ r i < M . Ensuite , chaque chaîne de hachage x 1 .. k à ( Σ k i = 1 x i r i ) mod M . Un adversaire ne peut presque rien faire pour provoquer des collisions très inégales, puisque vous générez r 1 .. kk r1..k M 0≤ri<M x1..k (∑ki=1xiri)modM r1..k au moment de l'exécution et donc k augmente la probabilité maximale de collision selon l' une quelconque paire donnée de chaînes distinctes passe rapidement à . Il est également évident de calculer en O ( k1/M tous les hachages possibles pour chaque chaîne avec un caractère changé.O(k)
Si vous voulez vraiment garantir un hachage uniforme, vous pouvez générer un nombre naturel aléatoire inférieur à M pour chaque paire ( i , c ) pour i de 1 à k et pour chaque caractère c , puis hacher chaque chaîne x 1 .. k à ( ∑ k i = 1 r ( i , x i ) ) mod Mr(i,c) M (i,c) i 1 k c x1..k (∑ki=1r(i,xi))modM . La probabilité de collision d'une paire donnée de chaînes distinctes est exactement . Cette approche est meilleure si votre jeu de caractères est relativement petit par rapport à n .1/M n
la source
De nombreux algorithmes publiés ici utilisent un peu d'espace sur les tables de hachage. Voici un algorithme simple d'exécution de stockage auxiliaire O ( ( n lg n ) ⋅ k 2 ) .O(1) O((nlgn)⋅k2)
L'astuce consiste à utiliser , qui est un comparateur entre deux valeurs a et b qui renvoie vrai si a < b (lexicographiquement) tout en ignorant le k ème caractère. L'algorithme est alors le suivant.Ck(a,b) a b a<b k
Tout d'abord, triez simplement les chaînes régulièrement et effectuez un balayage linéaire pour supprimer les doublons.
Ensuite, pour chaque :k
Triez les chaînes avec comme comparateur.Ck
Les chaînes qui ne diffèrent que par sont désormais adjacentes et peuvent être détectées dans un balayage linéaire.k
la source
Deux chaînes de longueur k , différant par un caractère, partagent un préfixe de longueur l et un suffixe de longueur m tels que k = l + m + 1 .
La réponse de Simon Prins l'encode en stockant explicitement toutes les combinaisons préfixe / suffixe, c'est-à-dire
abc
devient*bc
,a*c
etab*
. C'est k = 3, l = 0,1,2 et m = 2,1,0.Comme le souligne valarMorghulis, vous pouvez organiser les mots dans une arborescence de préfixes. Il y a aussi l'arbre des suffixes très similaire. Il est assez facile d'augmenter l'arborescence avec le nombre de nœuds feuilles sous chaque préfixe ou suffixe; cela peut être mis à jour dans O (k) lors de l'insertion d'un nouveau mot.
La raison pour laquelle vous souhaitez que ces nombres de frères et sœurs soit que vous sachiez, étant donné un nouveau mot, si vous voulez énumérer toutes les chaînes avec le même préfixe ou si vous devez énumérer toutes les chaînes avec le même suffixe. Par exemple, pour "abc" en entrée, les préfixes possibles sont "", "a" et "ab", tandis que les suffixes correspondants sont "bc", "c" et "". Comme il est évident, pour les suffixes courts, il est préférable d'énumérer les frères et sœurs dans l'arborescence des préfixes et vice versa.
Comme le souligne @einpoklum, il est certainement possible que toutes les chaînes partagent le même préfixe k / 2 . Ce n'est pas un problème pour cette approche; l'arbre de préfixe sera linéaire jusqu'à la profondeur k / 2, chaque nœud jusqu'à la profondeur k / 2 étant l'ancêtre de 100 000 nœuds foliaires. Par conséquent, l'arborescence des suffixes sera utilisée jusqu'à la profondeur (k / 2-1), ce qui est bien car les chaînes doivent différer dans leurs suffixes étant donné qu'elles partagent des préfixes.
[edit] Comme optimisation, une fois que vous avez déterminé le préfixe unique le plus court d'une chaîne, vous savez que s'il y a un caractère différent, ce doit être le dernier caractère du préfixe, et vous auriez trouvé le quasi-doublon quand vérifier un préfixe plus court. Donc, si "abcde" a le préfixe unique le plus court "abc", cela signifie qu'il existe d'autres chaînes commençant par "ab?" mais pas avec "abc". C'est-à-dire que s'ils différaient en un seul caractère, ce serait ce troisième caractère. Vous n'avez plus besoin de vérifier "abc? E".
Par la même logique, si vous trouvez que "cde" est un suffixe unique le plus court, alors vous savez que vous devez vérifier uniquement le préfixe "ab" de longueur 2 et non les préfixes de longueur 1 ou 3.
Note that this method works only for exactly one character differences and does not generalize to 2 character differences, it relies one one character being the separation between identical prefixes and identical suffixes.
la source
Storing strings in buckets is a good way (there are already different answers outlining this).
An alternative solution could be to store strings in a sorted list. The trick is to sort by a locality-sensitive hashing algorithm. This is a hash algorithm which yields similar results when the input is similar[1].
Each time you want to investigate a string, you could calculate its hash and lookup the position of that hash in your sorted list (takingO(log(n)) for arrays or O(n) for linked lists).
If you find that the neighbours (considering all close neighbours, not only those with an index of +/- 1) of that position are similar (off by one character) you found your match. If there are no similar strings, you can insert the new string at the position you found (which takes O(1) for linked lists and O(n) for arrays).
One possible locality-sensitive hashing algorithm could be Nilsimsa (with open source implementation available for example in python).
[1]: Note that often hash algorithms, like SHA1, are designed for the opposite: producing greatly differing hashes for similar, but not equal inputs.
Disclaimer: To be honest, I would personally implement one of the nested/tree-organized bucket-solutions for a production application. However, the sorted list idea struck me as an interesting alternative. Note that this algorithm highly depends on the choosen hash algorithm. Nilsimsa is one algorithm I found - there are many more though (for example TLSH, Ssdeep and Sdhash). I haven't verified that Nilsimsa works with my outlined algorithm.
la source
One could achieve the solution inO(nk+n2) time and O(nk) space using enhanced suffix arrays (Suffix array along with the LCP array) that allows constant time LCP (Longest Common Prefix) query (i.e. Given two indices of a string, what is the length of the longest prefix of the suffixes starting at those indices). Here, we could take advantage of the fact that all strings are of equal length. Specifically,
Build the enhanced suffix array of all then strings concatenated together. Let X=x1.x2.x3....xn where xi,∀1≤i≤n is a string in the collection. Build the suffix array and LCP array for X .
Now eachxi starts at position (i−1)k in the zero-based indexing. For each string xi , take LCP with each of the string xj such that j<i . If LCP goes beyond the end of xj then xi=xj . Otherwise, there is a mismatch (say xi[p]≠xj[p] ); in this case take another LCP starting at the corresponding positions following the mismatch. If the second LCP goes beyond the end of xj then xi and xj differ by only one character; otherwise there are more than one mismatches.
You could use SDSL library to build the suffix array in compressed form and answer the LCP queries.
Analysis: Building the enhanced suffix array is linear in the length ofX i.e. O(nk) . Each LCP query takes constant time. Thus, querying time is O(n2) .
Generalisation: This approach can also be generalised to more than one mismatches. In general, running time isO(nk+qn2) where q is the number of allowed mismatches.
If you wish to remove a string from the collection, instead of checking everyj<i , you could keep a list of only 'valid' j .
la source
k
in this formula practically can be omitted, since with SSE you can count matching bytes in 2 CPU cycles per 16 symbols (i.e. 6 cycles for k=40).One improvement to all the solutions proposed. They all requireO(nk) memory in the worst case. You can reduce it by computing hashes of strings with
*
instead each character, i.e.*bcde
,a*cde
... and processing at each pass only variants with hash value in certain integer range. F.e. with even hash values in the first pass, and odd hash values in the second one.You can also use this approach to split the work among multiple CPU/GPU cores.
la source
This is a short version of @SimonPrins' answer not involving hashes.
Assuming none of your strings contain an asterisk:
An alternative solution with implicit usage of hashes in Python (can't resist the beauty):
la source
Here is my take on 2+ mismatches finder. Note that in this post I consider each string as circular, f.e. substring of length 2 at index
k-1
consists of symbolstr[k-1]
followed bystr[0]
. And substring of length 2 at index-1
is the same!If we havemlen(k,M)=⌈k/M⌉−1 since, in the worst case, mismatched symbols split (circular) string into
M
mismatches between two strings of lengthk
, they have matching substring with length at leastM
equal-sized segments. F.e. withk=20
andM=4
the "worst" match may have the patternabcd*efgh*ijkl*mnop*
.Now, the algorithm for searching all mismatches up to
M
symbols among strings ofk
symbols:str[i..i+L-1]
, whereL = mlen(k,M)
. F.e. ifL=4
and you have alphabet of only 4 symbols (from DNA), this will make 256 groups.L
symbols we already matchedstr[i..i+L1-1]
, whereL1 = mlen(k-L,M)
. F.e. ifk=20, M=4, alphabet of 4 symbols
, soL=4
andL1=3
, this will make 64 groups.Why we don't start
j
from 0? Because we already made these groups with the same value ofi
, so job withj<=i-L
will be exactly equivalent to job with i and j values swapped.Further optimizations:
str[i..i+L-2] & str[i+L]
. This only doubles amount of jobs created, but allows to increaseL
by 1 (if my math is correct). So, f.e. instead of 256 groups, you will split data into 1024 groups.*
trick: for each i in in0..k-1
, remove i'th symbol from each string and create job searching forM-1
mismatches in those strings of lengthk-1
.la source
I work everyday on inventing and optimizing algos, so if you need every last bit of performance, that is the plan:
*
in each position independently, i.e. instead of single job processingn*k
string variants - startk
independent jobs each checkingn
strings. You can spread thesek
jobs among multiple CPU/GPU cores. This is especially important if you are going to check 2+ char diffs. Smaller job size will also improve cache locality, which by itself can make program 10x faster.*
at i-th position) and string index, and then either sort them or build hash table from these records.For sorting, you may try the following combo:
la source