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

Joh*_*ohn 350 python string pattern-matching

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

例子:

[
    '0045662100456621004566210045662100456621',             # '00456621'
    '0072992700729927007299270072992700729927',             # '00729927'
    '001443001443001443001443001443001443001443',           # '001443'
    '037037037037037037037037037037037037037037037',        # '037'
    '047619047619047619047619047619047619047619',           # '047619'
    '002457002457002457002457002457002457002457',           # '002457'
    '001221001221001221001221001221001221001221',           # '001221'
    '001230012300123001230012300123001230012300123',        # '00123'
    '0013947001394700139470013947001394700139470013947',    # '0013947'
    '001001001001001001001001001001001001001001001001001',  # '001'
    '001406469760900140646976090014064697609',              # '0014064697609'
]
Run Code Online (Sandbox Code Playgroud)

是重复自己的字符串,和

[
    '004608294930875576036866359447',
    '00469483568075117370892018779342723',
    '004739336492890995260663507109',
    '001508295625942684766214177978883861236802413273',
    '007518796992481203',
    '0071942446043165467625899280575539568345323741',
    '0434782608695652173913',
    '0344827586206896551724137931',
    '002481389578163771712158808933',
    '002932551319648093841642228739',
    '0035587188612099644128113879',
    '003484320557491289198606271777',
    '00115074798619102416570771',
]
Run Code Online (Sandbox Code Playgroud)

是那些没有的例子.

我给出的字符串的重复部分可能很长,并且字符串本身可以是500或更多字符,因此循环遍历每个字符尝试构建模式然后检查模式与字符串的其余部分似乎非常慢.乘以可能数百个字符串,我看不到任何直观的解决方案.

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

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

Dav*_*ang 565

这是一个简洁的解决方案,它避免了正则表达式和缓慢的Python循环:

def principal_period(s):
    i = (s+s).find(s, 1, -1)
    return None if i == -1 else s[:i]
Run Code Online (Sandbox Code Playgroud)

请参阅@davidism为基准测试结果启动的社区Wiki答案.综上所述,

大卫张的解决方案是明显的赢家,在大型示例集中表现优于其他所有人至少5倍.

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

这是基于以下观察:当且仅当字符串等于其自身的非平凡旋转时,该字符串是周期性的.荣誉对@AleksiTorhamo实现,我们可以从第一次出现的索引,然后收回本金周期s(s+s)[1:-1],并通知我可选的startendPython的论点string.find.

  • 您可以通过使用`.find()`或`.index()`而不是`in`来扩展它以找到最短的重复子序列,例如.`(s + s).find(s,1,-1)`. (19认同)
  • 就像你从[周期函数](http://en.wikipedia.org/wiki/Periodic_function)中取出一个sin或cos波并将其移到右边.由于它是完全周期性的,波浪最终将完美匹配......与此解决方案相似的数学算法只是惊人的.:)我希望我能给你+∞vvotes. (13认同)
  • 另外,我认为`(s + s).find(s,1,-1)`比(s + s)[1:-1] .find(s)`(非常轻微)快,至少对于较大的字符串,因为切片意味着你必须创建(几乎)整个字符串的另一个副本. (11认同)
  • @WayneConrad取一个字符串,比如说,"abcd"`,弹出右边的字符,然后把它贴在左边,得到"dabc".此过程称为*将字符串向右旋转1个字符*.重复"n"次,以"n"个字符向右旋转一个字符串.现在观察一下,如果我们有一串`k`字符,向右旋转`k`的任何倍数都会保持字符串不变.字符串的*非平凡*旋转是字符编号不是字符串长度的倍数的字符串. (8认同)
  • Guido的[最新更新](https://mail.python.org/pipermail/python-dev/2015-April/139054.html)到[PEP 8](https://www.python.org/dev/peps/ pep-0008 /)在这里是相关的:"在return语句中保持一致.函数中的所有return语句都应该返回一个表达式,或者它们都不应该.**如果任何return语句返回一个表达式,**任何return语句where没有返回值应该显式地将其声明为返回None,并且**在函数**的末尾应该存在显式的return语句(如果可以访问)." (6认同)
  • @Kasra我的代码不会在"bcab"中查找"abcabc"的出现,而是在"bcabcabcab"中查找.请注意,它表示`(s + s).find`,而不是`s.find`. (4认同)
  • @ ajkumar25因为你需要从至少为1的偏移量开始(为了阻止它与普通旋转相匹配 - 本身)并且某些字符串包含一个大于它的字符串,所以需要扩展它. (3认同)
  • 它让我想起了这个纯粹优雅的答案:http://stackoverflow.com/a/2553533/2653390我想一般来说,人们应该更多地关注连接自己的字符串.关于前缀,后缀和旋转,有一些非常美妙的东西...... (2认同)
  • 我来这里只是想说,这是我在计算机科学领域遇到过的最令人着迷的事情之一。可能是因为我的编程日子已经远远落后于这位工程总监了。但这是多么酷的观察啊!谢谢你给了我这无尽的魅力。 (2认同)

Zer*_*eus 181

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

import re

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

def repeated(s):
    match = REPEATER.match(s)
    return match.group(1) if match else None
Run Code Online (Sandbox Code Playgroud)

迭代问题中的示例:

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)
Run Code Online (Sandbox Code Playgroud)

...产生这个输出:

'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.
Run Code Online (Sandbox Code Playgroud)

正则表达式(.+?)\1+$分为三个部分:

  1. (.+?)是一个匹配组,包含至少一个(但尽可能少)任何字符(因为+?是非贪婪的).

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

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

在Python 3.4及更高版本中,您可以删除$和使用re.fullmatch(),或者(在任何Python中至少可以追溯到2.3)去另一种方式并使用re.search()正则表达式^(.+?)\1+$,所有这些都比其他任何东西都更符合个人品味.

  • 我认为有两个主要原因:1)一些程序员喜欢数学比他们更喜欢正则表达式,2)因为改变输入字符串的长度和性质会使不同的答案在性能上超前,超长边案例字符串(这可能甚至不出现在真实数据中)使这个解决方案看起来不是最理想的. (9认同)
  • 我不知道为什么这个简洁的两个班轮不是最高投票的答案.其他答案也不错,但这个答案要好得多.(它可能使用经常被诋毁的*正则表达式*,但我可以比其他更长的答案更容易检查,这些答案充满了嵌套块,潜在的错别字,逐个错误等等.)啊,更糟糕的是我想是更好的. (6认同)
  • @Zaibis,我通常会同意,但这是最短和最快的解决方案(http://stackoverflow.com/a/29482936/1212596),除了David's,我发表评论后发布.我实际上更喜欢大卫的方法(聪明!). (2认同)

Sha*_*ank 90

你可以观察到一个字符串被认为是重复的,它的长度必须能够被重复序列的长度整除.鉴于此,这是一个生成从长度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
Run Code Online (Sandbox Code Playgroud)

编辑:在Python 3中,/运算符默认情况下已更改为浮点除法.要从intPython 2中获得除法,您可以使用//运算符.感谢@ TigerhawkT3引起我的注意.

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

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

进一步更新以获得高性能:经过多次测试后,我得出的结论是,简单地测试字符串相等性在Python中的任何切片或迭代器解决方案中都具有最佳性能.因此,我从@ TigerhawkT3的书中摘了一条叶子并更新了我的解决方案.现在它的速度比以前快了6倍,明显快于Tigerhawk的解决方案,但比大卫的速度慢.

  • 这个解决方案很棒.您可以更改除数方法以遵循生产 - 消费者模式.因此,当它们被发现时,它会产生结果.如果这不是最高答案,那将是一种耻辱.其他一切都是蛮力. (3认同)
  • @JustinDanielson它返回一个从生成器表达式创建的生成器对象,它是一个隐式生成器:)它会懒惰地计算除数. (3认同)

dav*_*ism 85

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

一些功能进行了修改与Python 3工作(主要是通过替换///以确保整数除法).如果您发现错误,想要添加您的功能,或者想要添加另一个测试字符串,请在Python聊天室中 ping @ZeroPiraeus .

总结:有关于大集由OP提供的示例数据的最好和表现最差的解决方案之间的差别50倍这里(通过评论).大卫张的解决方案是明显的赢家,在大型示例集中表现优于所有其他人约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
Run Code Online (Sandbox Code Playgroud)

语料库1图


语料库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
Run Code Online (Sandbox Code Playgroud)

语料库1图


语料库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
Run Code Online (Sandbox Code Playgroud)

语料库3图


测试和原始结果可在此处获得.


Tig*_*kT3 37

非正则表达式解决方案:

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]
Run Code Online (Sandbox Code Playgroud)

更快的非正则表达式解决方案,感谢@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
Run Code Online (Sandbox Code Playgroud)

上面的解决方案很少比原始解决方案慢几个百分点,但它通常要快一点 - 有时速度要快很多.对于较长的字符串,它仍然不比davidism快,而对于短字符串,零的正则表达式解决方案更胜一筹.它以最快的速度出现(根据dithidism对github的测试 - 请参阅他的回答),其中包含大约1000-1500个字符的字符串.无论如何,在我测试的所有情况下,它都是可靠的第二快(或更好).谢谢,ThatWeirdo.

测试:

print(repeat('009009009'))
print(repeat('254725472547'))
print(repeat('abcdeabcdeabcdeabcde'))
print(repeat('abcdefg'))
print(repeat('09099099909999'))
print(repeat('02589675192'))
Run Code Online (Sandbox Code Playgroud)

结果:

009
2547
abcde
None
None
None
Run Code Online (Sandbox Code Playgroud)

  • @JustinDanielson是的.但是解决方案也是如此. (6认同)
  • 对于短串,我看到1e-5到3e-5秒,对于成功的长(1000个字符)字符串,我看到3e-5到4e-5秒,对于不成功的长串,我看到一点点不到一秒(最坏的情况) .因此,一千个1000个字符的字符串大约需要一秒钟.与数学答案相比,这比匹配要快10倍,但失败时间要长3倍. (3认同)
  • `len(string [0:i])`总是等于`i`(至少在这种情况下).替换这些,并在变量中保存`len(string)`和`string [0:i]`可能会加快速度.IMO也是一个很好的解决方案,很棒;) (2认同)

dav*_*ism 24

首先,将字符串减半,只要它是"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
Run Code Online (Sandbox Code Playgroud)

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


Ant*_*ala 16

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

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
Run Code Online (Sandbox Code Playgroud)

感谢TigerhawkT3注意到length // 2没有+ 1不符合abab案例.


Ria*_*iaD 16

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
Run Code Online (Sandbox Code Playgroud)

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

k = len(s) - prefix_function(s[-1])
Run Code Online (Sandbox Code Playgroud)

你只需要检查一下k != n and n % k == 0(如果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
Run Code Online (Sandbox Code Playgroud)


Sak*_*rma 15

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

对于s从第0个索引开始的长度为1到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"
Run Code Online (Sandbox Code Playgroud)


Log*_*ght 9

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

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]
Run Code Online (Sandbox Code Playgroud)

这个答案似乎与其他一些答案类似,但它有一些速度优化,其他人没有使用:

  • xrange 在这个应用程序中更快一点,
  • 如果输入字符串是奇数长度,则不检查任何偶数长度子字符串,
  • 通过s[:n]直接使用,我们避免在每个循环中创建变量.

我很想知道它如何在使用通用硬件的标准测试中执行.我相信在大多数测试中它都不会出现David Zhang的优秀算法,但是应该非常快.

我发现这个问题非常违反直觉.我认为快速的解决方案很慢.看起来很慢的解决方案很快!似乎Python的字符串创建与乘法运算符和字符串比较是高度优化的.