Как определить, повторяется ли строка в Python?

Я ищу способ проверить, повторяется ли данная строка для всей строки или нет.

Примеры:

[
    '0045662100456621004566210045662100456621',             # '00456621'
    '0072992700729927007299270072992700729927',             # '00729927'
    '001443001443001443001443001443001443001443',           # '001443'
    '037037037037037037037037037037037037037037037',        # '037'
    '047619047619047619047619047619047619047619',           # '047619'
    '002457002457002457002457002457002457002457',           # '002457'
    '001221001221001221001221001221001221001221',           # '001221'
    '001230012300123001230012300123001230012300123',        # '00123'
    '0013947001394700139470013947001394700139470013947',    # '0013947'
    '001001001001001001001001001001001001001001001001001',  # '001'
    '001406469760900140646976090014064697609',              # '0014064697609'
]

- это строки, которые повторяются, и

[
    '004608294930875576036866359447',
    '00469483568075117370892018779342723',
    '004739336492890995260663507109',
    '001508295625942684766214177978883861236802413273',
    '007518796992481203',
    '0071942446043165467625899280575539568345323741',
    '0434782608695652173913',
    '0344827586206896551724137931',
    '002481389578163771712158808933',
    '002932551319648093841642228739',
    '0035587188612099644128113879',
    '003484320557491289198606271777',
    '00115074798619102416570771',
]

- примеры тех, которые этого не делают.

Повторяющиеся разделы строк, которые я даю, могут быть довольно длинными, и сами строки могут быть 500 или более символов, поэтому цикл каждого символа, пытающегося создать шаблон, затем проверяет шаблон и остальную строку ужасно медленно. Умножьте это на потенциальные сотни строк, и я не вижу никакого интуитивного решения.

Я немного искал регулярные выражения, и они кажутся хорошими, когда вы знаете, что ищете, или, по крайней мере, длину шаблона, который вы ищете. К сожалению, я тоже не знаю.

Как я могу определить, повторяется ли строка, и если это так, какова самая кратчайшая повторяющаяся подпоследовательность?

Ответ 1

Здесь краткое решение, которое позволяет избежать регулярных выражений и медленных циклов в Python:

def principal_period(s):
    i = (s+s).find(s, 1, -1)
    return None if i == -1 else s[:i]

См. Ответ сообщества Wiki, начатый @davidism для результатов теста. Итак,

Решение Дэвида Чжана является явным победителем, превосходящим все остальные не менее чем на 5 раз для большого набора примеров.

(Ответьте на слова, а не мои.)

Это основано на наблюдении, что строка является периодической тогда и только тогда, когда она равна нетривиальному вращению самого себя. Престижность @AleksiTorhamo за то, что мы можем восстановить основной период из индекса первого вхождения s в (s+s)[1:-1] и сообщить мне необязательные аргументы start и end Python string.find.

Ответ 2

Здесь используется решение с использованием регулярных выражений.

import re

REPEATER = re.compile(r"(.+?)\1+$")

def repeated(s):
    match = REPEATER.match(s)
    return match.group(1) if match else None

Итерация над примерами в вопросе:

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)

... производит этот вывод:

'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.

Регулярное выражение (.+?)\1+$ делится на три части:

  • (.+?) - это сопоставимая группа, содержащая хотя бы один (но как можно меньше) любой символ (поскольку +? жадные).

  • \1+ проверяет, по крайней мере, одно повторение группы соответствия в первой части.

  • $ проверяет конец строки, чтобы убедиться, что после повторяющихся подстрок нет лишнего, не повторяющегося содержимого (и используя re.match() гарантирует, что перед повторяющимися подстроками нет не повторяющегося текста.)

В Python 3.4 и более поздних версиях вы можете отказаться от $ и использовать re.fullmatch() или (в любом Python по крайней мере как еще в 2.3), идите в re.search() с регулярным выражением ^(.+?)\1+$, все из которых более похожи на личный вкус, чем ничего другого.

Ответ 3

Вы можете сделать наблюдение, что для того, чтобы строка считалась повторяющейся, ее длина должна быть делимой на длину ее повторяющейся последовательности. Учитывая это, вот решение, которое генерирует делители длины от 1 до n / 2 включительно, делит исходную строку на подстроки с длиной делителей и проверяет равенство набора результатов:

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

РЕДАКТИРОВАТЬ: В Python 3 оператор / изменил значение по умолчанию для float-деления. Чтобы получить разделение int от Python 2, вместо этого вы можете использовать оператор //. Спасибо @TigerhawkT3 за то, что привлекли мое внимание.

Оператор // выполняет целочисленное деление как в Python 2, так и в Python 3, поэтому я обновил ответ для поддержки обеих версий. Часть, в которой мы проверяем, были ли все подстроки равны, теперь является короткозамкнутой операцией с использованием выражения all и выражения генератора.

UPDATE:. В ответ на изменение исходного вопроса код теперь обновлен, чтобы вернуть наименьшую повторяющуюся подстроку, если она существует, и None, если она не указана. @godlygeek предложил использовать divmod для уменьшения числа итераций в генераторе divisors, и код также был обновлен, чтобы соответствовать этому. Теперь он возвращает все положительные делители n в порядке возрастания, кроме самого n.

Дальнейшее обновление для высокой производительности:. После нескольких тестов я пришел к выводу, что простое тестирование для равенства строк имеет лучшую производительность из любого решения нарезания или итератора в Python. Таким образом, я взял лист из книги @TigerhawkT3 и обновил свое решение. Это теперь более чем в 6 раз быстрее, чем раньше, заметно быстрее, чем решение Tigerhawk, но медленнее, чем у Дэвида.

Ответ 4

Вот несколько критериев для различных ответов на этот вопрос. Были некоторые неожиданные результаты, в том числе совершенно разные характеристики в зависимости от тестируемой строки.

Некоторые функции были изменены для работы с Python 3 (главным образом путем замены / на // для обеспечения целочисленного деления). Если вы видите что-то не так, хотите добавить свою функцию или хотите добавить еще одну тестовую строку, пинг @ZeroPiraeus в чат Python.

Вкратце: существует разница в 50 раз между наилучшими и наихудшими решениями для большого набора примерных данных, предоставленных OP здесь (через этот комментарий). Решение Дэвида Чжана является явным победителем, превосходящим всех остальных примерно на 5 раз для большого набора примеров.

Несколько ответов очень медленны в чрезвычайно больших случаях "без соответствия". В противном случае функции кажутся одинаково согласованными или четкими победителями в зависимости от теста.

Вот результаты, в том числе графики, сделанные с использованием matplotlib и seaborn, чтобы показать разные дистрибутивы:


Корпус 1 (прилагаемые примеры - небольшой набор)

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


Корпус 2 (прилагаемые примеры - большой набор)

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


Корпус 3 (краевые случаи)

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


Тестирование и исходные результаты доступны здесь.

Ответ 5

Решение без регулярных выражений:

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]

Более быстрое решение без регулярных выражений благодаря @ThatWeirdo (см. комментарии):

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

Вышеупомянутое решение происходит очень редко, чем оригинал, на несколько процентов, но, как правило, это немного быстрее - иногда намного быстрее. Он все еще не быстрее, чем давизм для более длинных строк, а решение с нулевым регулярным выражением превосходит короткие строки. Он выходит на самый быстрый (согласно давидистскому тесту на github - см. Его ответ) со строками около 1000-1500 символов. Несмотря на это, он надежно второй - самый быстрый (или лучше) во всех проверенных мной случаях. Спасибо, ThatWeirdo.

Тест:

print(repeat('009009009'))
print(repeat('254725472547'))
print(repeat('abcdeabcdeabcdeabcde'))
print(repeat('abcdefg'))
print(repeat('09099099909999'))
print(repeat('02589675192'))

Результаты:

009
2547
abcde
None
None
None

Ответ 6

Во-первых, уменьшите длину строки до тех пор, пока она дублируется "2 части". Это уменьшает пространство поиска, если существует четное число повторений. Затем, работая вперед, чтобы найти наименьшую повторяющуюся строку, проверьте, разрешает ли расщепление полной строки все более значительную подстроку только в пустых значениях. Только подстроки до length // 2 должны быть протестированы, так как все, что над этим не будет иметь повторений.

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

Это возвращает кратчайшее совпадение или None, если совпадение отсутствует.

Ответ 7

В этой версии используются только длины последовательности кандидатов, которые являются факторами длины строки; и использует оператор * для построения полноразмерной строки из последовательности-кандидата:

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

Благодаря TigerhawkT3, заметив, что length // 2 без + 1 не сможет соответствовать случаю abab.

Ответ 8

Проблема также может быть решена в O(n) в худшем случае с префиксной функцией.

Обратите внимание, что это может быть медленнее в общем случае (UPD: и намного медленнее), чем другие решения, зависящие от числа делителей n, но обычно найти не удается раньше, я думаю, что один из плохих случаев для них будет aaa....aab, где n - 1 = 2 * 3 * 5 * 7 ... *p_n - 1 a 's

Прежде всего вам нужно вычислить функцию префикса

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

то либо нет ответа, либо самый короткий период

k = len(s) - prefix_function(s[-1])

и вам просто нужно проверить, если k != n and n % k == 0 (if k != n and n % k == 0, тогда ответ будет s[:k], иначе нет ответа

Вы можете проверить доказательство здесь (на русском языке, но онлайн-переводчик, вероятно, сделает трюк)

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

Ответ 9

Здесь прямое решение, без регулярных выражений.

Для подстрок s, начиная с нулевого индекса, от 1 до len(s), проверьте, является ли эта подстрока substr повторяющейся. Эта проверка может быть выполнена путем объединения substr с самим собой ratio раз, так что длина сформированной таким образом строки равна длине s. Следовательно, ratio=len(s)/len(substr).

Возвращается, когда первая такая подстрока найдена. Это обеспечило бы наименьшую возможную подстроку, если таковая существует.

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"

Ответ 10

Я начал с более чем восьми решений этой проблемы. Некоторые из них были основаны на регулярном выражении (match, findall, split), некоторые нарезки и тестирование строк, а некоторые - на строковые методы (find, count, split). У каждого были преимущества в ясности кода, размере кода, скорости и потреблении памяти. Я собирался опубликовать свой ответ здесь, когда заметил, что скорость выполнения оценивается как важная, поэтому я сделал больше тестов и улучшений, чтобы достичь этого:

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]

Этот ответ кажется похожим на несколько других ответов здесь, но он имеет несколько оптимизаций скорости, которые другие не использовали:

  • xrange в этом приложении немного быстрее,
  • если входная строка является нечетной длиной, не проверяйте подстроки любой длины,
  • используя s[:n] напрямую, мы не создаем переменную в каждом цикле.

Мне было бы интересно увидеть, как это выполняется в стандартных тестах с общим оборудованием. Я считаю, что в большинстве тестов будет недостаточно алгоритма Дэвида Чжана, но в противном случае он должен быть довольно быстрым.

Я нашел эту проблему очень противоречивой. Решения, которые, как я думал, были бы быстрыми, были медленными. Решения, которые выглядели медленными, были быстрыми! Похоже, что создание строки Python с умножением операционных и строковых сравнений сильно оптимизировано.

Ответ 11

Эта функция работает очень быстро (протестировано и в 3 раза быстрее, чем самое быстрое решение здесь на строках с более чем 100 тыс. символов, и разница становится больше, чем длиннее повторяющийся шаблон). Он пытается свести к минимуму количество сравнений, необходимых для получения ответа:

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]

Обратите внимание, что, например, для строки длиной 8 он проверяет только фрагмент размера 4, и ему не нужно тестировать дальше, потому что шаблон длиной 1 или 2 приведет к повторению паттерна длины 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'

Ответ 12

В ответ Дэвид Чжан ответ, если у нас есть какой-то круговой буфер, это не сработает: principal_period('6210045662100456621004566210045662100456621') из-за стартового 621, где мне бы хотелось, чтобы он выплюнул: 00456621.

Расширяя свое решение, мы можем использовать следующее:

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'

Ответ 13

Вот код в python, который проверяет повторение подстроки в основной строке, заданной пользователем.

print "Enter a string...."
#mainstring = String given by user
mainstring=raw_input(">")
if(mainstring==''):
    print "Invalid string"
    exit()
#charlist = Character list of mainstring
charlist=list(mainstring)
strarr=''
print "Length of your string :",len(mainstring)
for i in range(0,len(mainstring)):
    strarr=strarr+charlist[i]
    splitlist=mainstring.split(strarr)
    count = 0
    for j in splitlist:
        if j =='':
            count+=1
    if count == len(splitlist):
        break
if count == len(splitlist):
    if count == 2:
        print "No repeating Sub-String found in string %r"%(mainstring)

    else:
        print "Sub-String %r repeats in string %r"%(strarr,mainstring)
else :
    print "No repeating Sub-String found in string %r"%(mainstring)

Вход:

0045662100456621004566210045662100456621

Выход:

Длина строки: 40

Подстрока '00456621' повторяется в строке '0045662100456621004566210045662100456621'

Вход:

+004608294930875576036866359447

Выход:

Длина строки: 30

Не повторяется Sub-String, найденная в строке '004608294930875576036866359447'