首页 文章

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

提问于
浏览
342

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

例子:

[
    '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或更多字符,因此循环遍历每个字符尝试构建模式然后检查模式与字符串的其余部分似乎非常慢 . 乘以可能数百个字符串,我看不到任何直观的解决方案 .

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

如何判断一个字符串是否在重复,如果是,那么最短的重复子序列是什么?

13 回答

  • 180

    非正则表达式解决方案:

    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快,而对于短字符串,零的正则表达式解决方案更胜一筹 . 它以最快的速度出现(根据dithidism对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
    
  • 84

    这是使用正则表达式的解决方案 .

    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+$ 分为三个部分:

    • (.+?) 是一个匹配组,其中包含至少一个(但尽可能少)任何字符(因为+? is non-greedy) .

    • \1+ 检查第一部分中匹配组的至少一次重复 .

    • $ 检查字符串的结尾,以确保在重复的子字符串之后没有额外的,非重复的内容(并且使用re.match()确保在重复的子字符串之前没有非重复的文本) .

    在Python 3.4及更高版本中,您可以删除 $ 并使用re.fullmatch(),或者(在任何Python中至少早于2.3)转向另一种方式并使用re.search()与正则表达式 ^(.+?)\1+$ ,所有这些都更符合个人品味比什么都重要 .

  • 561

    这是一个简洁的解决方案,它避免了正则表达式和缓慢的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倍 .

    (那个答案的话,不是我的 . )

    这是基于以下观察结果:当且仅当字符串等于其自身的非平凡旋转时,该字符串是周期性的 . 感谢@AleksiTorhamo意识到我们可以从 (s+s)[1:-1] 中第一次出现 s 的索引中恢复主要时段,并通知我Python的 string.find 的可选 startend 参数 .

  • 2

    你可以观察到一个字符串被认为是重复的,它的长度必须能够被重复序列的长度整除 . 鉴于此,这是一个生成从 1n / 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中, / 运算符默认情况下已更改为浮点除法 . 要从Python 2获得 int 除法,可以使用 // 运算符 . 感谢@ TigerhawkT3引起我的注意 .

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

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

    Further update for high performance: 经过多次测试后,我发现@ TigerhawkT3 's book and updated my solution. It'的叶片速度已经超过以前的6倍,明显比Tigerhawk 's solution but slower than David'更快 .

  • 1

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

    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 案件 .

  • 37

    在带有前缀功能的最坏情况下,问题也可以在 O(n) 中解决 .

    注意,它可能比一般情况下更慢(UPD:并且要慢得多),而其他解决方案依赖于 n 的除数的数量,但通常会很快发现失败,我认为其中一个不好的情况将是 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
    
  • 90

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

    对于从第0个索引开始的 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"
    
  • 9

    在David Zhang的回答中,如果我们有某种循环缓冲区,这将不起作用: 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'
    
  • 23

    以下是针对此问题的各种答案的一些基准 . 有一些令人惊讶的结果,包括根据被测试的字符串的不同性能 .

    修改了一些函数以使用Python 3(主要通过用 // 替换 / 来确保整数除法) . 如果您发现错误,想要添加您的功能,或想要添加另一个测试字符串,请在Python chatroom中ping @ZeroPiraeus .

    总结:OP here(通过this评论)提供的大量示例数据的最佳和最差性能解决方案之间的差异约为50倍 . David Zhang's solution是明显的赢家,在大型示例集中表现优于其他所有人约5倍 .

    在非常大的案例中,有几个答案非常缓慢 . 否则,根据测试,功能似乎是相同匹配或明确的赢家 .

    以下是结果,包括使用matplotlib和seaborn制作的图表来显示不同的分布:


    Corpus 1 (supplied examples - small set)

    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 2 (supplied examples - large set)

    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 3 (edge cases)

    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
    

    测试和原始结果可用here .

  • 15

    我从这个问题开始有八个以上的解决方案 . 一些基于正则表达式(匹配,查找,拆分),一些字符串切片和测试,一些使用字符串方法(查找,计数,拆分) . 每个都有代码清晰度,代码大小,速度和内存消耗的好处 . 当我注意到执行速度被评为重要时,我打算在这里发布我的答案,所以我做了更多的测试和改进来达到这个目的:

    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的字符串创建与乘法运算符和字符串比较是高度优化的 .

  • -1

    这个功能运行得非常快(经过测试,它比快速解决方案快3倍以上,字符串超过100k字符,重复模式越长,差异越大) . 它试图最小化获得答案所需的比较次数:

    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'
    
  • 16

    首先,将字符串减半,只要它是"2 part"重复 . 如果存在偶数个重复,则会减少搜索空间 . 然后,向前工作以找到最小的重复字符串,检查是否通过越来越大的子字符串分割整个字符串仅导致空值 . 只需要测试最多 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
    

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

  • 16

    这是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在字符串'004608294930875576036866359447'中找不到重复的子字符串

相关问题