检查字符串是否包含Python 3中的重复字符的最快方法是什么?

时间:2013-04-01 20:12:17

标签: string list python-3.x filter set

我需要按照不包含任何字符的标准来过滤字符串。

  • 字符串很多(比如1.4万亿)。
  • 字符串(大约8个字符)。
  • 字符串唯一(缓存无效)。
  • 字符串有大字符集(比如任何Unicode字符)。
  • 字符串通常符合标准(比如2/3没有重复字符)。

使用代码如下所示:

>>> candidate_strings = ["foobnehg", "barfnehg", "bazfnehg"]
>>> result_strings = [s if unique_chars(s) for s in candidate_strings]
>>> print(result_strings)
["barfnehg", "bazfnehg"]

我实现了一个简单的版本,只需迭代字符串:

def unique_chars_naive(string_given):
    """
    Checks if a given string contains only unique characters.
    This version iterates the given string, saving all occurred characters.
    """
    chars_seen = []
    for char in string_given:
        if char in chars_seen:
            return False
        chars_seen.append(char)
    return True

我的下一个最好的想法是使用set,所以我实现了:

def unique_chars_set(string_given):
    """
    Checks if a given string contains only unique characters.
    This version exploits that a set contains only unique entries.
    """
    return len(string_given) == len(set(string_given))

将函数保存到文件UniqueCharacters.py,将它们计时:

$ python3 -m timeit -n 100000 --setup='import UniqueCharacters; candidate_strings = ["foobnehg", "barfnehg", "bazfnehg"]' '[UniqueCharacters.unique_chars_naive(s) for s in candidate_strings]'
100000 loops, best of 3: 20.3 usec per loop

$ python3 -m timeit -n 100000 --setup='import UniqueCharacters; candidate_strings = ["foobnehg", "barfnehg", "bazfnehg"]' '[UniqueCharacters.unique_chars_set(s) for s in candidate_strings]'
100000 loops, best of 3: 17.7 usec per loop

这表明此数据集的unique_chars_set速度提高了约15%。

有更快的方法吗?用正则表达式可能吗?标准库中是否有一些方法可以做到这一点?

5 个答案:

答案 0 :(得分:12)

首先我要说的是,我怀疑你在不需要时进行优化。 Python是一种高级语言,支持以高级方式思考计算。可读,优雅且可重复使用的解决方案通常会比速度极快但难以理解的解决方案更好。

当您确定速度是个问题时,时,您应继续进行优化。甚至可能为计算密集的部分编写C扩展名。

话虽如此,这里是一些技巧的比较:

def unique_chars_set(s):
    return len(s) == len(set(s))

def unique_chars_frozenset(s):
    return len(s) == len(frozenset(s))

def unique_chars_counter(s):
    return Counter(s).most_common(1)[0][1] > 1

def unique_chars_sort(s):
    ss = ''.join(sorted(s))
    prev = ''
    for c in ss:
        if c == prev:
            return False
        prev = c
    return True

def unique_chars_bucket(s):
    buckets = 255 * [False]
    for c in s:
        o = ord(c)
        if buckets[o]:
            return False
        buckets[o] = True
    return True

这是性能比较(在IPython中):

In [0]: %timeit -r10 [unique_chars_set(s) for s in candidate_strings]
100000 loops, best of 10: 6.63 us per loop

In [1]: %timeit -r10 [unique_chars_frozenset(s) for s in candidate_strings]
100000 loops, best of 10: 6.81 us per loop

In [2]: %timeit -r10 [unique_chars_counter(s) for s in candidate_strings]
10000 loops, best of 10: 83.1 us per loop

In [3]: %timeit -r10 [unique_chars_sort(s) for s in candidate_strings]
100000 loops, best of 10: 13.1 us per loop

In [4]: %timeit -r10 [unique_chars_bucket(s) for s in candidate_strings]
100000 loops, best of 10: 15 us per loop

结论:set比许多其他明显的方法更优雅,更快捷。但差异很小,无论如何都无关紧要。

有关更多基准,请参阅@FrancisAvila的答案。

答案 1 :(得分:4)

我创建了一个带有计时和测试工具的文件来尝试一系列不同的方法。

我找到的最快的是基于正则表达式的,但它只比基于len(set())的最快方法快一点。它是下面的isunique_reg()函数。

import re
import array
import collections
import bisect

re_dup_g = re.compile(r'(.).*\1', re.DOTALL)
re_dup_ng = re.compile(r'(.).*?\1', re.DOTALL)


def isunique_reg(s, search=re_dup_g.search):
    return search(s) is None

def isunique_reng(s, search=re_dup_ng.search):
    return search(s) is None

def isunique_set(s, set=set, len=len):
    return len(s) == len(set(s))

def isunique_forset(s, set=set):
    seen = set()
    add = seen.add
    for c in s:
        if c in seen:
            return False
        add(c)
    return True

def isunique_array(s, array=array.array):
    seen = array('u')
    append = seen.append
    for c in s:
        if c in seen:
            return False
        append(c)
    return True

def isunique_deque(s, deque=collections.deque):
    seen = deque()
    append = seen.append
    for c in s:
        if c in seen:
            return False
        append(c)
    return True

def isunique_bisect(s, find=bisect.bisect_right, array=array.array):
    seen = array('u')
    insert = seen.insert
    for c in s:
        i = find(seen, c)
        if i and seen[i-1] == c:
            return False
        insert(i, c)
    return True

def isunique_bisectl(s, find=bisect.bisect_right):
    seen = []
    insert = seen.insert
    for c in s:
        i = find(seen, c)
        if i and seen[i-1] == c:
            return False
        insert(i, c)
    return True

def isunique_count(s, Counter=collections.Counter):
    return Counter(s).most_common(1)[0][1]==1

def isunique_list(s):
    seen = []
    append = seen.append
    for c in s:
        if c in seen:
            return False
        append(c)
    return True


def _test():
    funcs = [f for n,f in globals().items() if n.startswith('isunique_')]
    cases = [
        (u'string given', False),
        (u'string uoqzd', True),
    ]
    for func in funcs:
        for s,rv in cases:
            try:
                assert rv is func(s)
            except AssertionError, e:
                print "%s(%r) is not %r" % (func.__name__, s, rv)
                raise e

def _time():
    import timeit
    funcs = [f for n,f in globals().items() if n.startswith('isunique_')]
    funcs.sort(key=lambda f: f.__name__)
    cases = [
        ('!uniq', u'string given', False),
        ('uniq', u'string uoqzd', True),
    ]

    casenames = [name for name, _, _ in cases]
    fwidth = max(len(f.__name__) for f in funcs)
    timeitsetup = 's = {!r}; from __main__ import {} as u'

    print('{: <{fwidth}s}|{: >15s}|{: >15s}'.format('func', *casenames, fwidth=fwidth))
    print('-'*(fwidth+2+15+15))
    for f in funcs:
        times = [timeit.timeit('u(s)', setup=timeitsetup.format(input, f.__name__)) for _, input, _ in cases]
        print('{: <{fwidth}s}|{: >15.10f}|{: >15.10f}'.format(f.__name__, *times, fwidth=fwidth))

if __name__=='__main__':
    _test()
    _time()

在CPython 2.7.1上我得到以下结果(不幸的是我没有CPython 3.x方便):

func            |          !uniq|           uniq
------------------------------------------------
isunique_array  |   6.0237820148|  11.0871050358
isunique_bisect |  10.8665719032|  18.4178640842
isunique_bisectl|   8.2648131847|  13.9763219357
isunique_count  |  23.1477651596|  23.5043439865
isunique_deque  |   4.0739829540|   7.3630020618
isunique_forset |   2.8148539066|   4.1761989594
isunique_list   |   3.6703650951|   6.9271368980
isunique_reg    |   1.7293550968|   2.8794138432
isunique_reng   |   1.9672849178|   3.3768401146
isunique_set    |   2.3157420158|   2.2436211109

您会注意到,当字符串不唯一时,基于正则表达式的方法比基于集合的方法更快,但基于正则表达式的方法的最坏情况比集合更慢。

答案 2 :(得分:1)

我不知道是否会更快,但这个正则表达式可能会让你满意:

couplet = re.compile(r'(.).*\1')
result_strings = [s if not re.search(couplet, s) for s in candidate_strings]

答案 3 :(得分:0)

您可以对字符串进行排序并对其进行迭代以查看是否没有连续相同的字母,但这是N * log(N),所以我不确定这会比设置解决方案更快。

答案 4 :(得分:0)

请参阅bucket sort虽然它是一种排序算法,您可以将解决方案基于此,基本上您定义了一个n个位置的数组,并且对于每个字符,您将它放在数组中由其ASCII或UNICODE值给出的位置(请参阅下面的unicode),每次迭代最多会有O(n)时间复杂度(当数组中的某个位置已被使用时你可以中断)...但我认为你不会发现相当大的差别方法,您可以直接检查if(string.length < 255)或者字符串中可能有效值的数量。

该检查确保任何循环最多超过255个字符,这使得它们小到足以在大多数情况下担心性能

(我不知道python,但我确定有一些string.length属性或等价物)

如@JOHN所述,如果您需要支持大的或所有可能的字符串值,那么这将导致空间和时间问题