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

String 在Python 3中,检查字符串是否包含重复字符的最快方法是什么?,string,list,python-3.x,filter,set,String,List,Python 3.x,Filter,Set,我需要根据字符串不包含字符的条件过滤字符串两次 这些字符串有很多(比如说1.4万亿) 字符串是短的(大约8个字符) 字符串是唯一的(缓存不起作用) 字符串有一个大字符集(比如任何Unicode字符) 字符串通常符合标准(例如2/3没有重复字符) 使用的代码如下所示: >>> candidate_strings = ["foobnehg", "barfnehg", "bazfnehg"] >>> result_strings = [s if unique_c

我需要根据字符串不包含字符的条件过滤字符串两次

  • 这些字符串有很多(比如说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
我的第二个好主意是使用
集合
,因此我实现了:

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
这表明对于该数据集,
唯一字符集
的速度快了约15%


有没有更快的方法?也许是正则表达式?标准库中是否有某种方法可以做到这一点?

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

尽管这是一种排序算法,但您可以将解决方案基于此,基本上,您定义了一个由n个位置组成的数组,对于每个字符,您将其放置在由其ASCII或UNICODE值(请参见下面的UNICODE值)给定的位置上的数组中,每次迭代中的时间复杂度最多为O(n)(当数组中的位置已被使用时,您可以中断)。。。但是我认为您不会在这两种方法中发现很大的差异,因为您可以简单地首先检查
if(string.length<255)
或字符串中可能的有效值的数量

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

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


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

我不知道是否会更快,但是这个正则表达式可能会满足您的要求:

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

让我首先说,我怀疑您在不需要优化的时候正在优化。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中):

结论:
set
比其他许多明显的方法优雅、快速。但是差别太小了,反正也没关系


有关更多基准测试,请参阅的答案。

我创建了一个带有计时和测试工具的文件,以尝试各种不同的方法

我发现的最快的方法是基于regex的,但它只比基于len(set())的方法快一点点。这是下面的
isunique\u 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()

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

候选字符串中可能有重复的字符串。?使用较长的单词进行测试。所有字符串是否只包含小写字母?如果不是,它们是否只包含ASCII字符?可能使用一个包含26个元素的数组-您可以非常快地将每个字符映射到此数组中的一个元素,因为字符只是整数。然后,您可以在单元格中设置一个标志,如果字母存在,并且如果已经设置,则有一个重复的字母。这将是桶排序的一种变体。我怀疑任何解决方案都会比使用set的更快,即使对于较长的单词也是如此。对字符串进行迭代是一种O(N)解决方案,因此对于较大的字符串不会更快。但对于每个字符,您仍然需要以某种方式与前面的所有字符进行比较。我当然不相信这是你说的O(N)。虽然排序方法与
集合
相似,但它确实很有趣。不过,它有提前终止的好处。有关排序和查找连续相同字母的解决方案,请参见中的
unique\u chars\u sort(s)
。这比最快的解决方案慢两倍。当然,如果需要支持所有可能的unicode字符,则应考虑不同的因素。必须支持所有可能的unicode字符不是常见的情况。但是,不能只支持某些unicode字符。要么全力以赴,要么一事无成。:)@J0HN:Welp,从技术上讲,ASCII码点也是有效的UTF-8码点。因此,我可以说,通过支持ASCII,我支持的是Unicode的“有限子集”:o) @voithosааа,ааааааа:这是一些来自unicode子集的符号。你很幸运,所以使用UTF来处理它,所以你和我一样看到它。但是,如果它只支持unicode的“有限子集”(如您标记的ASCII),您将得到一堆不可读的字符,这些字符恰好位于您的机器区域设置中的相同代码点上。所以,你不会得到一致的结果。我不会称之为“支持”:(正如你所建议的,我试着使用一套。对于我示例中的3个字符串,它比使用列表慢40%。插入时间似乎在短时间内超过了成员资格测试的时间。我添加了很少重复的字符作为一项要求。@bngtlrs,我通过调查方法和迪斯科舞厅完全重写了我的答案
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()