私は与えられた文字列が文字列全体に対してそれ自身を繰り返すかどうかをテストする方法を探しています。
例:
[
'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文字以上になることがあります。それに何百もの文字列を掛けても直感的な解決策は見当たりません。
私は正規表現について少し調べましたが、あなたが探しているもの、あるいは少なくとも探しているパターンの長さを知っているときにはそれらは良いようです。残念ながら、私はどちらも知りません。
文字列がそれ自体を繰り返しているかどうか、そして繰り返しているかどうかを判断するにはどうすればよいですか。
これは、正規表現を避け、Pythonのループを遅くする簡潔な解決策です。
def principal_period(s):
i = (s+s).find(s, 1, -1)
return None if i == -1 else s[:i]
ベンチマーク結果については、@ davidismが始めた Community Wiki answer をご覧ください。要約すれば、
David Zhangのソリューションは明らかに勝者であり、大規模な例のセットでは他のすべてのものよりも少なくとも5倍優れています。
(その答えは言葉であり、私のものではありません。)
これは、文字列がそれ自体の自明ではない回転に等しい場合に限り、その文字列が周期的であるという観察に基づいています。 (s+s)[1:-1]
で最初に出現したs
のインデックスからプリンシパルピリオドを回復できること、そしてPythonのstring.find
のオプションのstart
およびend
引数について知らせるために@AleksiTorhamoに感謝します.
これが正規表現を使った解決策です。
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+$
は3つの部分に分けられます。
(.+?)
は、少なくとも1つ(ただしできるだけ少ない数)の任意の文字を含む一致グループです( +?
は欲張りではない )。
\1+
は最初の部分で一致するグループの少なくとも1回の繰り返しをチェックします。
$
は、文字列の末尾をチェックして、繰り返し部分文字列の後に余分な繰り返しのないコンテンツがないことを確認します(そして re.match()
を使用すると、繰り返しのないテキストがないことが保証されます繰り返し部分文字列の前。
Python 3.4以降では、代わりに$
を削除して re.fullmatch()
を使用するか、(少なくともPython 2.3以降の場合は)他の方法で re.search()
を使用することができます。正規表現^(.+?)\1+$
を使うと、これらはすべて他のものよりも個人的な好みにかかっています。
文字列が繰り返しと見なされるためには、その長さがその繰り返しシーケンスの長さで割り切れる必要があるという観察をすることができます。ここで、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
EDIT: Python 3では、/
演算子はデフォルトでfloat除算をするように変更されました。 Python 2からint
を取得するには、代わりに//
演算子を使用できます。私の注意を引くために@ TigerhawkT3をありがとう。
//
演算子は、Python 2とPython 3の両方で整数除算を実行するので、両方のバージョンをサポートするように回答を更新しました。すべての部分文字列が等しいかどうかをテストする部分は、all
とジェネレータ式を使用した短絡演算です。
PDATE:元の質問の変更に対応して、コードは最小の繰り返し部分文字列が存在する場合はそれを返し、存在しない場合はNone
を返すように更新されました。 @godlygeekは、divmod
ジェネレータの反復回数を減らすためにdivisors
を使用することを提案し、それに合わせてコードも更新しました。 n
自体を除く、n
のすべての正の約数を昇順で返すようになりました。
高性能のための更なる更新複数のテストの結果、Pythonのスライスやイテレータソリューションの中では、単純に文字列の等価性をテストするのが最高のパフォーマンスであるという結論に達しました。したがって、私は@ TigerhawkT3の本から葉を取り出し、私の解決策を更新しました。今では以前の6倍以上の速さで、Tigerhawkのソリューションよりは明らかに速いがDavidのものよりは遅い。
これが、この質問に対するさまざまな回答のベンチマークです。テスト対象の文字列によってパフォーマンスが大きく異なるなど、驚くべき結果がいくつかありました。
一部の関数はPython 3で動作するように修正されました(主に整数除算を確実にするために/
を//
に置き換えることによって)。何か問題がある場合は、関数を追加するか、別のテスト文字列を追加する必要があります。 Python chatroom で@ZeroPiraeusをpingします。
要約すると、OP here (via this comment)によって提供される例のデータの大規模なセットに対する最高のパフォーマンスと最低のパフォーマンスのソリューションの間に約50倍の違いがあります。 David Zhangのソリューション は明らかな勝者であり、大規模な例のセットでは他のすべてのものよりも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
コーパス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
コーパス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
テストと生の結果は利用可能です ここ 。
非正規表現ソリューション:
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
上記の解決策がオリジナルのものより数パーセント遅くなることはめったにありませんが、通常はかなり速いです - 時にはずっと速いです。長い文字列に対するdavidismの速度よりはまだ速くはありません。短い文字列に対してはzeroの正規表現による解決策が優れています。それは約1000 - 1500文字の文字列で最速(githubでのdavidismのテストによると - 彼の答えを見てください)で出てきます。それにもかかわらず、私がテストしたすべてのケースで、確実に2番目に速い(またはそれ以上)のです。ありがとう、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
まず、「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を返します。
プレフィックス関数を使用すると、最悪の場合、O(n)
で問題が解決することもあります。
注:一般的な場合(UPD:およびはるかに遅い)は、n
の約数に依存する他のソリューションよりも遅くなる場合がありますが、通常はより早く失敗し、それらの悪いケースの1つはaaa....aab
、n - 1 = 2 * 3 * 5 * 7 ... *p_n - 1
a
がある
まず、プレフィックス関数を計算する必要があります
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
(k != n and n % k == 0
の場合、答えはs[:k]
かどうかを確認する必要があります。そうでない場合、答えはありません。
証拠を確認できます here (ロシア語ですが、おそらくオンライン翻訳者がトリックを行います)
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
このバージョンでは、文字列長の要因である候補シーケンス長のみを試します。 *
演算子を使用して、候補シーケンスから全長文字列を作成します。
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
length // 2
がない+ 1
がabab
の場合と一致しないことに気づいたTigerhawkT3に感謝します。
これは正規表現を使わないで簡単な解決策です。
長さが1からlen(s)
までの、0番目のインデックスから始まる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"
私はこの問題に対する8つ以上の解決策から始めました。いくつかは正規表現(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]
を直接使用することで、各ループで変数を作成することを避けます。私は、これが一般的なハードウェアを使った標準テストでどのように機能するのかを知りたいと思います。私は、ほとんどのテストでDavid Zhangの優れたアルゴリズムには程遠いと思いますが、そうでなければかなり速いはずです。
私はこの問題が非常に直感的ではないことがわかりました。私が速いと思う解決策は遅かったです。遅く見えた解決策は速かった!乗算演算子を使ったPythonの文字列生成と文字列比較は非常に最適化されているようです。
この関数は非常に速く実行されます(テストされ、100k文字を超える文字列に対してテストされた最速のソリューションよりも3倍以上速く、繰り返しパターンが長くなればなるほど違いは大きくなります)。それは答えを得るために必要な比較の数を最小にしようとします:
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'
621
という00456621
の開始のせいで、principal_period('6210045662100456621004566210045662100456621')
という循環型バッファがある場合、David Zhangの答えではこれはうまくいきません。
彼の解決策を拡張すると、次のようになります。
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'