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

例子:

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

我研究了一下正则表达式,当你知道你在寻找什么,或者至少知道你在寻找的模式的长度时,它们似乎很有用。不幸的是,我两个都不知道。

我怎么知道一个字符串是否在重复它自己,如果是的话,最短的重复子序列是什么?


当前回答

首先,将字符串减半,只要它是“2部分”副本。如果重复次数为偶数,这将减少搜索空间。然后,向前查找最小的重复字符串,检查将整个字符串拆分为越来越大的子字符串是否只得到空值。只有长度不超过// 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。

其他回答

首先,将字符串减半,只要它是“2部分”副本。如果重复次数为偶数,这将减少搜索空间。然后,向前查找最小的重复字符串,检查将整个字符串拆分为越来越大的子字符串是否只得到空值。只有长度不超过// 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。

这个版本只尝试那些候选序列长度,是字符串长度的因素;并使用*操作符从候选序列构建一个完整的字符串:

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注意到长度// 2没有+ 1将无法匹配abab情况。

我从这个问题的八个以上的解决方案开始。一些基于正则表达式(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使用乘法操作符创建字符串和字符串比较似乎得到了高度优化。

下面是这个问题的不同答案的一些基准。有一些令人惊讶的结果,包括完全不同的性能取决于测试的字符串。

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

总之:对于OP在这里(通过这条评论)提供的大量示例数据,最佳和最差的解决方案之间大约有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


测试和原始结果可以在这里找到。

您可以观察到,对于一个被认为是重复的字符串,它的长度必须能被其重复序列的长度整除。鉴于此,下面是一个解决方案,它生成长度为1到n / 2(含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

编辑:在python3中,/操作符默认执行浮点除法。要从Python 2中获得int除法,可以使用//操作符。谢谢@TigerhawkT3让我注意到这一点。

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

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

高性能的进一步更新:经过多次测试,我得出的结论是,在Python中的任何切片或迭代器解决方案中,简单地测试字符串相等性具有最好的性能。因此,我借鉴了@TigerhawkT3的经验,更新了我的解决方案。现在它的速度是以前的6倍多,明显比虎鹰的解决方案快,但比大卫的解决方案慢。