Как определить, повторяется ли строка в 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'