web-dev-qa-db-fra.com

Comment savoir si une chaîne se répète en Python?

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?

348
John

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.

565
David Zhang

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:

  1. (.+?) est un groupe de correspondance contenant au moins un caractère (mais aussi peu que possible) (car +? n'est pas glouton ).

  2. \1+ vérifie au moins une répétition du groupe correspondant dans la première partie.

  3. $ 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.

180
Zero Piraeus

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.

90
Shashank

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 1 graph


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 1 graph


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

Corpus 3 graph


Les tests et les résultats bruts sont disponibles ici .

85
davidism

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
37
TigerhawkT3

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.

24
davidism

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 - 1a '

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
16
RiaD

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.

16
Antti Haapala

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"
15
Saksham Varma

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,
  • si une chaîne d'entrée a une longueur impaire, ne cochez aucune sous-chaîne de longueur paire,
  • en utilisant 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.

9
Logic Knight

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'
2
Piotr Dabkowski

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'
_
1
sachinruk