如何判断字符串是否在Python中重复?

How can I tell if a string repeats itself in Python?

我正在寻找一种方法来测试给定的字符串是否在整个字符串中重复自身。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
    '0045662100456621004566210045662100456621',             # '00456621'
    '0072992700729927007299270072992700729927',             # '00729927'
    '001443001443001443001443001443001443001443',           # '001443'
    '037037037037037037037037037037037037037037037',        # '037'
    '047619047619047619047619047619047619047619',           # '047619'
    '002457002457002457002457002457002457002457',           # '002457'
    '001221001221001221001221001221001221001221',           # '001221'
    '001230012300123001230012300123001230012300123',        # '00123'
    '0013947001394700139470013947001394700139470013947',    # '0013947'
    '001001001001001001001001001001001001001001001001001',  # '001'
    '001406469760900140646976090014064697609',              # '0014064697609'
]

是重复的字符串,并且

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
    '004608294930875576036866359447',
    '00469483568075117370892018779342723',
    '004739336492890995260663507109',
    '001508295625942684766214177978883861236802413273',
    '007518796992481203',
    '0071942446043165467625899280575539568345323741',
    '0434782608695652173913',
    '0344827586206896551724137931',
    '002481389578163771712158808933',
    '002932551319648093841642228739',
    '0035587188612099644128113879',
    '003484320557491289198606271777',
    '00115074798619102416570771',
]

是一些不这样的例子。

给我的字符串的重复部分可能很长,字符串本身可以是500个或更多的字符,所以在每个字符之间循环,尝试构建一个模式,然后检查模式与字符串的其余部分,似乎非常慢。乘以可能有数百个字符串,我看不到任何直观的解决方案。

我已经研究了一些正则表达式,当你知道你在寻找什么,或者至少知道你在寻找的模式的长度时,它们看起来是很好的。不幸的是,我也不知道。

我怎样才能知道一个字符串是否在重复本身,如果是,那么最短的重复子序列是什么?


下面是一个简洁的解决方案,它避免了正则表达式,并且在python循环中速度较慢:

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

有关基准测试结果,请参阅@davidim启动的社区wiki答案。总之,

David Zhang's solution is the clear winner, outperforming all others by at least 5x for the large example set.

(那是我的回答,不是我的回答。)

这是基于这样一个观察:如果且仅当一个字符串等于其自身的非平凡旋转时,它才是周期性的。感谢@aleksitorhamo认识到我们可以从(s+s)[1:-1]中第一次出现s的索引中恢复主周期,并通知我关于python的string.find的可选startend参数。


这里有一个使用正则表达式的解决方案。

1
2
3
4
5
6
7
import re

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

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

重复问题中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)

…生成此输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'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()和regex ^(.+?)\1+$,所有这些都比其他任何东西更符合个人口味。


    您可以观察到,对于要被视为重复的字符串,其长度必须可以被其重复序列的长度整除。考虑到这一点,这里有一个解决方案,它生成长度为从1n / 2的除数,并用除数的长度将原始字符串划分为子字符串,并测试结果集的相等性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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中,/操作符默认改为做浮点除法。要从python 2获得int划分,可以使用//操作符。感谢@tigerhawkt3让我注意到这一点。

    //操作符在python 2和python 3中都执行整数除法,所以我更新了答案以支持这两个版本。我们测试所有子串是否相等的部分现在是一个使用all和生成器表达式的短路操作。

    更新:为了响应原始问题中的更改,代码现在已被更新,以返回最小的重复子字符串(如果存在),如果不存在,则返回None。@Godlygeek建议使用divmod来减少divisors生成器上的迭代次数,并且代码也已更新以与之匹配。它现在按升序返回n的所有正除数,不包括n本身。

    对高性能的进一步更新:在多次测试之后,我得出了这样一个结论:简单地测试字符串相等性在Python中的任何切片或迭代器解决方案中都有最好的性能。因此,我从@tigerhawkt3的书中摘取了一页并更新了我的解决方案。现在它的速度比以前快了6倍多,明显快于虎威的解决方案,但慢于大卫的解决方案。


    下面是这个问题各种答案的一些基准。有一些令人惊讶的结果,包括根据测试的字符串的不同性能。

    一些函数被修改为使用python 3(主要是用//替换/,以确保整数除法)。如果您看到错误,想要添加您的函数,或者想要添加另一个测试字符串,请在python聊天室中ping@zeropiraeus。

    总而言之:对于由op here提供的大型示例数据集,最佳和最差的解决方案之间有大约50倍的差异(通过此注释)。张大卫的解决方案显然是胜利者,在大型示例集中,其表现优于其他所有示例集约5倍。

    在非常大的"不匹配"情况下,有两个答案非常慢。否则,根据测试结果,功能似乎是相同的或明显的赢家。

    以下是结果,包括使用Matplotlib和Seaborn绘制的图,以显示不同的分布:

    语料库1(提供的示例-小集合)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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(提供的示例-大集合)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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(边缘案例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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

    氧化镁

    测试和原始结果在这里可用。


    非regex溶液:

    1
    2
    3
    4
    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]

    更快的非regex解决方案,感谢@thatweirdo(见评论):

    1
    2
    3
    4
    5
    6
    7
    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

    上面的解决方案很少比原来的慢几个百分点,但它通常会快一点——有时会快很多。对于较长的字符串,它仍然不比davidim快,而zero的regex解决方案对于较短的字符串更为优越。它以最快的速度出现(根据Davidim在Github上的测试——看他的答案),字符串大约有1000-1500个字符。不管怎样,在我测试的所有情况下,它都是第二快(或更好)的。谢谢,真奇怪。

    测试:

    1
    2
    3
    4
    5
    6
    print(repeat('009009009'))
    print(repeat('254725472547'))
    print(repeat('abcdeabcdeabcdeabcde'))
    print(repeat('abcdefg'))
    print(repeat('09099099909999'))
    print(repeat('02589675192'))

    结果:

    1
    2
    3
    4
    5
    6
    009
    2547
    abcde
    None
    None
    None


    首先,将字符串减半,只要它是"2部分"副本。如果重复次数为偶数,这将减少搜索空间。然后,向前查找最小的重复字符串,检查通过越来越大的子字符串拆分整个字符串是否只会导致空值。只有到length // 2的子串需要测试,因为任何超过该子串的子串都不会重复。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

    如果没有匹配,则返回最短匹配或无匹配。


    在最坏情况下,O(n)中的前缀函数也可以解决这个问题。

    注:一般情况下(upd:且慢得多),可能比其它依赖于n除数的解慢,但通常发现失败得更快,我认为其中一个坏的例子是aaa....aab,其中有n - 1 = 2 * 3 * 5 * 7 ... *p_n - 1a的。

    首先需要计算前缀函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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

    那么要么没有答案要么最短的时间是

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

    你只要检查一下k != n and n % k == 0(如果k != n and n % k == 0,那么答案是s[:k],否则就没有答案了

    你可以在这里查看证据(俄语,但在线翻译可能会做到这一点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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


    此版本仅尝试那些作为字符串长度因子的候选序列长度;并使用*运算符从候选序列构建一个完整的字符串:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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注意到没有+ 1length // 2abab的情况不符。


    这里有一个直接的解决方案,没有正则表达式。

    对于长度为1到len(s)的从零索引开始的s子串,检查该子串substr是否为重复模式。此检查可通过将substr与本身ratio次连接来执行,这样形成的字符串长度等于s的长度。因此,ratio=len(s)/len(substr)

    当找到第一个这样的子字符串时返回。这将提供尽可能小的子字符串(如果存在)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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"


    我从八个以上的方法开始解决这个问题。有些是基于regex(match、findall、split)、一些字符串切片和测试,还有一些是使用字符串方法(find、count、split)。每一个都在代码清晰度、代码大小、速度和内存消耗方面有好处。我将在这里发布我的答案,当我注意到执行速度排名同样重要时,我做了更多的测试和改进来达到这一点:

    1
    2
    3
    4
    5
    6
    7
    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的字符串创建似乎得到了高度优化。


    这个函数运行得非常快(在超过100k个字符的字符串上测试,它比这里的最快解决方案快3倍以上,并且重复模式越长,差异越大)。它尽量减少得到答案所需的比较数量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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的重复模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> 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,所以principal_period('6210045662100456621004566210045662100456621'),我希望它能吐出来:00456621

    扩展他的解决方案,我们可以使用以下方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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'

    下面是python中的代码,它检查用户给定的主字符串中的子字符串是否重复。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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

    输出:

    Length of your string : 40

    Sub-String '00456621' repeats in string '0045662100456621004566210045662100456621'

    输入:

    004608294930875576036866359447

    输出:

    Length of your string : 30

    No repeating Sub-String found in string '004608294930875576036866359447'