web-dev-qa-db-ja.com

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文字以上になることがあります。それに何百もの文字列を掛けても直感的な解決策は見当たりません。

私は正規表現について少し調べましたが、あなたが探しているもの、あるいは少なくとも探しているパターンの長さを知っているときにはそれらは良いようです。残念ながら、私はどちらも知りません。

文字列がそれ自体を繰り返しているかどうか、そして繰り返しているかどうかを判断するにはどうすればよいですか。

348
John

これは、正規表現を避け、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に感謝します.

564
David Zhang

これが正規表現を使った解決策です。

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つ(ただしできるだけ少ない数)の任意の文字を含む一致グループです( +?は欲張りではない )。

  2. \1+は最初の部分で一致するグループの少なくとも1回の繰り返しをチェックします。

  3. $は、文字列の末尾をチェックして、繰り返し部分文字列の後に余分な繰り返しのないコンテンツがないことを確認します(そして re.match() を使用すると、繰り返しのないテキストがないことが保証されます繰り返し部分文字列の前

Python 3.4以降では、代わりに$を削除して re.fullmatch() を使用するか、(少なくともPython 2.3以降の場合は)他の方法で re.search() を使用することができます。正規表現^(.+?)\1+$を使うと、これらはすべて他のものよりも個人的な好みにかかっています。

180
Zero Piraeus

文字列が繰り返しと見なされるためには、その長さがその繰り返しシーケンスの長さで割り切れる必要があるという観察をすることができます。ここで、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のものよりは遅い。

90
Shashank

これが、この質問に対するさまざまな回答のベンチマークです。テスト対象の文字列によってパフォーマンスが大きく異なるなど、驚くべき結果がいくつかありました。

一部の関数は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

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 


テストと生の結果は利用可能です ここ

84
davidism

非正規表現ソリューション:

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

まず、「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を返します。

23
davidism

プレフィックス関数を使用すると、最悪の場合、O(n)で問題が解決することもあります。

注:一般的な場合(UPD:およびはるかに遅い)は、nの約数に依存する他のソリューションよりも遅くなる場合がありますが、通常はより早く失敗し、それらの悪いケースの1つはaaa....aabn - 1 = 2 * 3 * 5 * 7 ... *p_n - 1aがある

まず、プレフィックス関数を計算する必要があります

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 == 0k != 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
16
RiaD

このバージョンでは、文字列長の要因である候補シーケンス長のみを試します。 *演算子を使用して、候補シーケンスから全長文字列を作成します。

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がない+ 1ababの場合と一致しないことに気づいたTigerhawkT3に感謝します。

16
Antti Haapala

これは正規表現を使わないで簡単な解決策です。

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

私はこの問題に対する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の文字列生成と文字列比較は非常に最適化されているようです。

9
Logic Knight

この関数は非常に速く実行されます(テストされ、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'
2
Piotr Dabkowski

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