Je cherche un moyen de tester si une chaîne donnée se répète ou non pour la chaîne entière.
Exemples:
[
'0045662100456621004566210045662100456621', # '00456621'
'0072992700729927007299270072992700729927', # '00729927'
'001443001443001443001443001443001443001443', # '001443'
'037037037037037037037037037037037037037037037', # '037'
'047619047619047619047619047619047619047619', # '047619'
'002457002457002457002457002457002457002457', # '002457'
'001221001221001221001221001221001221001221', # '001221'
'001230012300123001230012300123001230012300123', # '00123'
'0013947001394700139470013947001394700139470013947', # '0013947'
'001001001001001001001001001001001001001001001001001', # '001'
'001406469760900140646976090014064697609', # '0014064697609'
]
sont des chaînes qui se répètent, et
[
'004608294930875576036866359447',
'00469483568075117370892018779342723',
'004739336492890995260663507109',
'001508295625942684766214177978883861236802413273',
'007518796992481203',
'0071942446043165467625899280575539568345323741',
'0434782608695652173913',
'0344827586206896551724137931',
'002481389578163771712158808933',
'002932551319648093841642228739',
'0035587188612099644128113879',
'003484320557491289198606271777',
'00115074798619102416570771',
]
sont des exemples de ceux qui ne le font pas.
Les sections répétées des chaînes que je reçois peuvent être assez longues, et les chaînes elles-mêmes peuvent contenir 500 caractères ou plus. Par conséquent, parcourir en boucle chaque personnage essayant de créer un modèle puis vérifiant le modèle par rapport au reste de la chaîne semble terriblement lent. Multipliez cela par potentiellement des centaines de chaînes et je ne vois aucune solution intuitive.
J'ai un peu examiné les expressions rationnelles et elles semblent bonnes lorsque vous savez ce que vous recherchez, ou du moins la longueur du motif que vous recherchez. Malheureusement, je ne connais ni l'un ni l'autre.
Comment savoir si une chaîne se répète et si c'est le cas, quelle est la sous-séquence la plus courte?
Voici une solution concise qui évite les expressions régulières et les boucles in-Python lentes:
def principal_period(s):
i = (s+s).find(s, 1, -1)
return None if i == -1 else s[:i]
Voir le réponse du wiki de la communauté commencé par @davidism pour obtenir des résultats de référence. En résumé,
La solution de David Zhang est clairement gagnante, surpassant toutes les autres d'au moins 5 fois celle du grand exemple.
(Les mots de cette réponse, pas les miens.)
Ceci est basé sur l'observation qu'une chaîne est périodique si et seulement si elle est égale à une rotation non triviale d'elle-même. Félicitations à @AleksiTorhamo pour avoir réalisé que nous pouvons ensuite récupérer la période principale de l’index de la première occurrence de s
dans (s+s)[1:-1]
et pour m'avoir informé des arguments optionnels start
et end
de Python's string.find
.
Voici une solution utilisant des expressions régulières.
import re
REPEATER = re.compile(r"(.+?)\1+$")
def repeated(s):
match = REPEATER.match(s)
return match.group(1) if match else None
Itérant sur les exemples de la question:
examples = [
'0045662100456621004566210045662100456621',
'0072992700729927007299270072992700729927',
'001443001443001443001443001443001443001443',
'037037037037037037037037037037037037037037037',
'047619047619047619047619047619047619047619',
'002457002457002457002457002457002457002457',
'001221001221001221001221001221001221001221',
'001230012300123001230012300123001230012300123',
'0013947001394700139470013947001394700139470013947',
'001001001001001001001001001001001001001001001001001',
'001406469760900140646976090014064697609',
'004608294930875576036866359447',
'00469483568075117370892018779342723',
'004739336492890995260663507109',
'001508295625942684766214177978883861236802413273',
'007518796992481203',
'0071942446043165467625899280575539568345323741',
'0434782608695652173913',
'0344827586206896551724137931',
'002481389578163771712158808933',
'002932551319648093841642228739',
'0035587188612099644128113879',
'003484320557491289198606271777',
'00115074798619102416570771',
]
for e in examples:
sub = repeated(e)
if sub:
print("%r: %r" % (e, sub))
else:
print("%r does not repeat." % e)
... produit cette sortie:
'0045662100456621004566210045662100456621': '00456621'
'0072992700729927007299270072992700729927': '00729927'
'001443001443001443001443001443001443001443': '001443'
'037037037037037037037037037037037037037037037': '037'
'047619047619047619047619047619047619047619': '047619'
'002457002457002457002457002457002457002457': '002457'
'001221001221001221001221001221001221001221': '001221'
'001230012300123001230012300123001230012300123': '00123'
'0013947001394700139470013947001394700139470013947': '0013947'
'001001001001001001001001001001001001001001001001001': '001'
'001406469760900140646976090014064697609': '0014064697609'
'004608294930875576036866359447' does not repeat.
'00469483568075117370892018779342723' does not repeat.
'004739336492890995260663507109' does not repeat.
'001508295625942684766214177978883861236802413273' does not repeat.
'007518796992481203' does not repeat.
'0071942446043165467625899280575539568345323741' does not repeat.
'0434782608695652173913' does not repeat.
'0344827586206896551724137931' does not repeat.
'002481389578163771712158808933' does not repeat.
'002932551319648093841642228739' does not repeat.
'0035587188612099644128113879' does not repeat.
'003484320557491289198606271777' does not repeat.
'00115074798619102416570771' does not repeat.
L'expression régulière (.+?)\1+$
est divisée en trois parties:
(.+?)
est un groupe de correspondance contenant au moins un caractère (mais aussi peu que possible) (car +?
n'est pas glouton ).
\1+
vérifie au moins une répétition du groupe correspondant dans la première partie.
$
vérifie la fin de la chaîne pour s'assurer qu'il n'y a pas de contenu supplémentaire non répétitif après les sous-chaînes répétées (et que re.match()
garantit qu'il n'y a pas de texte non répétitif avant les sous-chaînes répétées).
Dans Python3.4 et versions ultérieures, vous pouvez supprimer le $
et utiliser re.fullmatch()
, ou (dans tout Python au moins aussi loin que 2.3) allez dans l’autre sens et utilisez re.search()
avec la regex ^(.+?)\1+$
, qui sont tous plus personnels que toute autre chose.
Vous pouvez faire remarquer que pour qu'une chaîne soit considérée comme répétée, sa longueur doit être divisible par la longueur de sa séquence répétée. Dans ce cas, voici une solution qui génère des diviseurs de longueur comprise entre 1
et n / 2
inclus, divise la chaîne d'origine en sous-chaînes avec la longueur des diviseurs et teste l'égalité du jeu de résultats:
from math import sqrt, floor
def divquot(n):
if n > 1:
yield 1, n
swapped = []
for d in range(2, int(floor(sqrt(n))) + 1):
q, r = divmod(n, d)
if r == 0:
yield d, q
swapped.append((q, d))
while swapped:
yield swapped.pop()
def repeats(s):
n = len(s)
for d, q in divquot(n):
sl = s[0:d]
if sl * q == s:
return sl
return None
EDIT: Dans Python 3, l'opérateur /
a changé pour effectuer la division flottante par défaut. Pour obtenir la division int
de Python 2, vous pouvez utiliser l'opérateur //
. Merci à @ TigerhawkT3 d’avoir porté cela à mon attention.
L'opérateur //
effectue une division entière dans Python 2 et Python 3. J'ai donc mis à jour la réponse pour prendre en charge les deux versions. La partie où nous testons pour voir si toutes les sous-chaînes sont égales est maintenant une opération de court-circuit utilisant all
et une expression génératrice.
PDATE: En réponse à une modification de la question d'origine, le code a maintenant été mis à jour pour renvoyer la plus petite sous-chaîne répétée, si elle existe, et None
, si ce n'est pas le cas. @godlygeek a suggéré d'utiliser divmod
pour réduire le nombre d'itérations sur le générateur divisors
, et le code a également été mis à jour pour correspondre à cela. Il renvoie maintenant tous les diviseurs positifs de n
par ordre croissant, à l'exclusion de n
lui-même.
Nouvelle mise à jour pour des performances élevées: Après plusieurs tests, je suis parvenu à la conclusion que le simple test d'égalité des chaînes offre les meilleures performances, quelle que soit la solution de découpage ou d'itérateur de Python. Ainsi, j'ai pris une feuille du livre de @ TigerhawkT3 et mis à jour ma solution. Il est maintenant six fois plus rapide qu'avant, sensiblement plus rapide que la solution de Tigerhawk mais plus lent que celui de David.
Voici quelques repères pour les différentes réponses à cette question. Il y a eu des résultats surprenants, notamment des performances très différentes selon la chaîne testée.
Certaines fonctions ont été modifiées pour fonctionner avec Python 3 (principalement en remplaçant /
par //
pour assurer la division entière). Si vous voyez quelque chose de mal, souhaitez ajouter votre fonction ou si vous souhaitez ajouter une autre chaîne de test, envoyez une requête ping à @ZeroPiraeus dans le salle de discussion Python .
En résumé: il existe une différence environ 50x entre les solutions les plus performantes et les moins performantes pour le grand ensemble de données d'exemple fournies par OP ici (via this comment). La solution de David Zhang est le grand gagnant, surperformant tous les autres d'environ 5x pour le grand exemple.
Quelques réponses sont très lentes dans les très gros cas "sans correspondance". Autrement, les fonctions semblent être à égalité ou égales, selon le test.
Voici les résultats, y compris les parcelles réalisées avec matplotlib et seaborn pour illustrer les différentes distributions:
Corpus 1 (exemples fournis - petit ensemble)
mean performance:
0.0003 david_zhang
0.0009 zero
0.0013 antti
0.0013 tigerhawk_2
0.0015 carpetpython
0.0029 tigerhawk_1
0.0031 davidism
0.0035 saksham
0.0046 shashank
0.0052 riad
0.0056 piotr
median performance:
0.0003 david_zhang
0.0008 zero
0.0013 antti
0.0013 tigerhawk_2
0.0014 carpetpython
0.0027 tigerhawk_1
0.0031 davidism
0.0038 saksham
0.0044 shashank
0.0054 riad
0.0058 piotr
Corpus 2 (exemples fournis - grand jeu)
mean performance:
0.0006 david_zhang
0.0036 tigerhawk_2
0.0036 antti
0.0037 zero
0.0039 carpetpython
0.0052 shashank
0.0056 piotr
0.0066 davidism
0.0120 tigerhawk_1
0.0177 riad
0.0283 saksham
median performance:
0.0004 david_zhang
0.0018 zero
0.0022 tigerhawk_2
0.0022 antti
0.0024 carpetpython
0.0043 davidism
0.0049 shashank
0.0055 piotr
0.0061 tigerhawk_1
0.0077 riad
0.0109 saksham
Corpus 3 (cas limites)
mean performance:
0.0123 shashank
0.0375 david_zhang
0.0376 piotr
0.0394 carpetpython
0.0479 antti
0.0488 tigerhawk_2
0.2269 tigerhawk_1
0.2336 davidism
0.7239 saksham
3.6265 zero
6.0111 riad
median performance:
0.0107 tigerhawk_2
0.0108 antti
0.0109 carpetpython
0.0135 david_zhang
0.0137 tigerhawk_1
0.0150 shashank
0.0229 saksham
0.0255 piotr
0.0721 davidism
0.1080 zero
1.8539 riad
Les tests et les résultats bruts sont disponibles ici .
Solution non regex:
def repeat(string):
for i in range(1, len(string)//2+1):
if not len(string)%len(string[0:i]) and string[0:i]*(len(string)//len(string[0:i])) == string:
return string[0:i]
Solution plus rapide sans regex, grâce à @ThatWeirdo (voir les commentaires):
def repeat(string):
l = len(string)
for i in range(1, len(string)//2+1):
if l%i: continue
s = string[0:i]
if s*(l//i) == string:
return s
La solution ci-dessus est très rarement plus lente que l'originale de quelques pour cent, mais elle est généralement un peu plus rapide - parfois beaucoup plus rapide. Ce n'est toujours pas plus rapide que le davidisme pour les chaînes plus longues, et la solution regex de zéro est supérieure pour les chaînes courtes. Selon le test de davidism sur github - voir sa réponse, il apparaît comme le plus rapide, avec des chaînes d'environ 1000-1500 caractères. Quoi qu'il en soit, c'est le deuxième plus rapide (ou mieux) dans tous les cas que j'ai testés. Merci, ce weirdo.
Tester:
print(repeat('009009009'))
print(repeat('254725472547'))
print(repeat('abcdeabcdeabcdeabcde'))
print(repeat('abcdefg'))
print(repeat('09099099909999'))
print(repeat('02589675192'))
Résultats:
009
2547
abcde
None
None
None
Commencez par diviser par deux la chaîne tant qu’il s’agit d’un doublon en 2 parties. Cela réduit l'espace de recherche s'il existe un nombre pair de répétitions. Puis, en recherchant la plus petite chaîne répétée, vérifiez si le fractionnement de la chaîne complète en sous-chaîne de plus en plus volumineuse ne produit que des valeurs vides. Seules les sous-chaînes allant jusqu'à length // 2
doivent être testées car tout ce qui est au-dessus n'aurait pas de répétition.
def shortest_repeat(orig_value):
if not orig_value:
return None
value = orig_value
while True:
len_half = len(value) // 2
first_half = value[:len_half]
if first_half != value[len_half:]:
break
value = first_half
len_value = len(value)
split = value.split
for i in (i for i in range(1, len_value // 2) if len_value % i == 0):
if not any(split(value[:i])):
return value[:i]
return value if value != orig_value else None
Ceci retourne la correspondance la plus courte ou None s'il n'y a pas de correspondance.
Le problème peut également être résolu dans O(n)
dans le pire des cas avec la fonction préfixe.
Notez que cela peut être plus lent dans le cas général (UPD: et est beaucoup plus lent) que d’autres solutions qui dépendent du nombre de diviseurs de n
, mais que la recherche échoue généralement plus tôt, je pense que l’un des mauvais cas pour eux sera aaa....aab
, où il y a n - 1 = 2 * 3 * 5 * 7 ... *p_n - 1
a
'
Tout d’abord, vous devez calculer la fonction préfixe
def prefix_function(s):
n = len(s)
pi = [0] * n
for i in xrange(1, n):
j = pi[i - 1]
while(j > 0 and s[i] != s[j]):
j = pi[j - 1]
if (s[i] == s[j]):
j += 1
pi[i] = j;
return pi
alors soit il n'y a pas de réponse ou la période la plus courte est
k = len(s) - prefix_function(s[-1])
et il vous suffit de vérifier si k != n and n % k == 0
(si k != n and n % k == 0
alors la réponse est s[:k]
, sinon il n'y a pas de réponse.
Vous pouvez vérifier la preuve ici (en russe, mais le traducteur en ligne fera probablement l'affaire)
def riad(s):
n = len(s)
pi = [0] * n
for i in xrange(1, n):
j = pi[i - 1]
while(j > 0 and s[i] != s[j]):
j = pi[j - 1]
if (s[i] == s[j]):
j += 1
pi[i] = j;
k = n - pi[-1]
return s[:k] if (n != k and n % k == 0) else None
Cette version teste uniquement les longueurs de séquence candidates qui sont des facteurs de la longueur de la chaîne; et utilise l'opérateur *
pour créer une chaîne complète à partir de la séquence candidate:
def get_shortest_repeat(string):
length = len(string)
for i in range(1, length // 2 + 1):
if length % i: # skip non-factors early
continue
candidate = string[:i]
if string == candidate * (length // i):
return candidate
return None
Merci à TigerhawkT3 d’avoir remarqué que length // 2
sans + 1
ne correspondrait pas à la casse abab
.
Voici une solution simple, sans regex.
Pour les sous-chaînes de s
à partir d'un index égal à zéro, de longueur comprise entre 1 et len(s)
, vérifiez si cette sous-chaîne, substr
est le motif répété. Cette vérification peut être effectuée en concaténant substr
avec lui-même ratio
fois, de sorte que la longueur de la chaîne ainsi formée soit égale à la longueur de s
. D'où ratio=len(s)/len(substr)
.
Retourne quand la première sous-chaîne est trouvée. Cela fournirait la plus petite sous-chaîne possible, s'il en existe une.
def check_repeat(s):
for i in range(1, len(s)):
substr = s[:i]
ratio = len(s)/len(substr)
if substr * ratio == s:
print 'Repeating on "%s"' % substr
return
print 'Non repeating'
>>> check_repeat('254725472547')
Repeating on "2547"
>>> check_repeat('abcdeabcdeabcdeabcde')
Repeating on "abcde"
J'ai commencé avec plus de huit solutions à ce problème. Certains étaient basés sur regex (match, findall, split), d'autres sur le découpage et le test de chaînes, et d'autres sur des méthodes de chaîne (find, count, split). Chacun avait des avantages en termes de clarté de code, de taille de code, de vitesse et de consommation de mémoire. J'allais poster ma réponse ici quand j'ai remarqué que la vitesse d'exécution était considérée comme importante. J'ai donc effectué davantage de tests et d'amélioration pour parvenir à ceci:
def repeating(s):
size = len(s)
incr = size % 2 + 1
for n in xrange(1, size//2+1, incr):
if size % n == 0:
if s[:n] * (size//n) == s:
return s[:n]
Cette réponse semble similaire à quelques autres réponses ici, mais elle comporte quelques optimisations de vitesse que d’autres n’ont pas utilisées:
xrange
est un peu plus rapide dans cette application,s[:n]
directement, nous évitons de créer une variable dans chaque boucle.Je serais intéressé de voir comment cela fonctionne dans les tests standard avec le matériel commun. Je pense que l'excellent algorithme de David Zhang sera largement absent dans la plupart des tests, mais il devrait être assez rapide sinon.
J'ai trouvé ce problème très contre-intuitif. Les solutions que je pensais rapides seraient lentes. Les solutions qui semblaient lentes étaient rapides! Il semble que la création de chaînes de Python avec l'opérateur Multiply et les comparaisons de chaînes soient hautement optimisées.
Cette fonction est très rapide (testée et plus de 3 fois plus rapide que la solution la plus rapide ici pour les chaînes de plus de 100 000 caractères et la différence est d'autant plus grande que le motif répété est long). Il essaie de minimiser le nombre de comparaisons nécessaires pour obtenir la réponse:
def repeats(string):
n = len(string)
tried = set([])
best = None
nums = [i for i in xrange(2, int(n**0.5) + 1) if n % i == 0]
nums = [n/i for i in nums if n/i!=i] + list(reversed(nums)) + [1]
for s in nums:
if all(t%s for t in tried):
print 'Trying repeating string of length:', s
if string[:s]*(n/s)==string:
best = s
else:
tried.add(s)
if best:
return string[:best]
Notez que par exemple, pour une chaîne de longueur 8, il ne vérifie que le fragment de taille 4 et il n'a pas besoin de faire de test supplémentaire, car un motif de longueur 1 ou 2 entraînerait un motif répétitif de longueur 4:
>>> repeats('12345678')
Trying repeating string of length: 4
None
# for this one we need only 2 checks
>>> repeats('1234567812345678')
Trying repeating string of length: 8
Trying repeating string of length: 4
'12345678'
Dans la réponse de David Zhang, si nous avons une sorte de tampon circulaire, cela ne fonctionnera pas: principal_period('6210045662100456621004566210045662100456621')
en raison du _621
_ de départ, où j'aurais aimé qu'il crache: _00456621
_.
En étendant sa solution, nous pouvons utiliser les éléments suivants:
_def principal_period(s):
for j in range(int(len(s)/2)):
idx = (s[j:]+s[j:]).find(s[j:], 1, -1)
if idx != -1:
# Make sure that the first substring is part of pattern
if s[:j] == s[j:][:idx][-j:]:
break
return None if idx == -1 else s[j:][:idx]
principal_period('6210045662100456621004566210045662100456621')
>>> '00456621'
_