Python中最快的方法是在长排序的字符串列表中查找“startswith”子字符串

时间:2011-07-17 09:34:23

标签: python sorting performance

我做了很多谷歌搜索,但没有找到任何东西,所以如果我只是在寻找错误的东西,我真的很抱歉。

我正在为Ghost编写MIT Introduction to Programming, assignment 5的实现。

作为其中的一部分,我需要确定一串字符是否是任何有效单词的开头。我有一个有效单词列表(“wordlist”)。

更新:我每次都可以使用在列表中迭代的内容,例如Peter的简单建议:

def word_exists(wordlist, word_fragment):
return any(w.startswith(word_fragment) for w in wordlist)

我之前有过:

wordlist = [w for w in wordlist if w.startswith(word_fragment)]

(从here)将列表缩小到以该片段开头的有效单词列表,如果wordlist为空则认​​为它是一个丢失。我采用这种方法的原因是我(错误地,见下文)认为这将节省时间,因为后续查找只需搜索较小的列表。

在我看来,这是通过原始词汇表中的每个项目(38,000个单词)检查每个项目的开始。当wordlist被排序时,这似乎很愚蠢,一旦它击中单词片段之后的某些内容,理解就会停止。我试过这个:

newlist = []
for w in wordlist:
    if w[:len(word_fragment)] > word_fragment:
        # Take advantage of the fact that the list is sorted
        break
    if w.startswith(word_fragment):
        newlist.append(w)
return newlist

但是这个速度大致相同,我认为可能是因为列表推导作为编译代码运行了?

然后我认为再次更高效的是列表中的某种形式的二进制搜索,以找到匹配单词的块。这是要走的路,还是我错过了一些非常明显的东西?

显然,在这种情况下这并不是什么大不了的事,但我刚开始编程并且想要正确地做事。

UPDATE:

我已经用simple test script测试了以下建议。虽然Peter的二分搜索/ bisect对于单次运行显然会更好,但我对缩小列表是否会胜过一系列片段感兴趣。事实上,它没有:

The totals for all strings "p", "py", "pyt", "pyth", "pytho" are as follows:
In total, Peter's simple test took 0.175472736359
In total, Peter's bisect left test took 9.36985015869e-05
In total, the list comprehension took 0.0499348640442
In total, Neil G's bisect took 0.000373601913452

创建第二个列表等的开销明显比搜索更长的列表花费更多时间。事后看来,这可能是最好的方法,因为“减少列表”方法增加第一次运行的时间,这是最糟糕的情况。

感谢所有人提出了一些很好的建议,并为彼得做了最好的答案!

6 个答案:

答案 0 :(得分:14)

生成器表达式被懒惰地评估,所以如果你只需要确定你的单词是否有效,我希望以下内容更有效率,因为它不一定会强制它在找到一个完整列表后构建它匹配:

def word_exists(wordlist, word_fragment):
    return any(w.startswith(word_fragment) for w in wordlist)

请注意,缺少方括号对于此工作非常重要。

然而,在最坏的情况下,这显然仍是线性的。你是正确的,二元搜索会更有效率;你可以使用内置的bisect模块。它可能看起来像这样:

from bisect import bisect_left
def word_exists(wordlist, word_fragment):
    try:
        return wordlist[bisect_left(wordlist, word_fragment)].startswith(word_fragment)
    except IndexError:
        return False # word_fragment is greater than all entries in wordlist

bisect_left在O(log(n))中运行,因此对于大型词表来说会快得多。

编辑:如果你的word_fragment真的很普通(比如't'),我猜想你给出的例子会失败,在这种情况下,它可能花费大部分时间来组合一大堆有效单词,并获得收益从只需要对列表进行部分扫描就可以忽略不计了。很难说肯定,但它有点学术,因为无论如何二进制搜索更好。

答案 1 :(得分:4)

你是对的,因为列表已经排序,你可以更有效地做到这一点。

我正在建立@Peter的答案,它会返回一个元素。我看到你想要所有以给定前缀开头的单词。这是你如何做到的:

from bisect import bisect_left
wordlist[bisect_left(wordlist, word_fragment):
         bisect_left(wordlist, word_fragment[:-1] + chr(ord(word_fragment[-1])+1))]

这将返回原始排序列表中的切片。

答案 2 :(得分:1)

彼得建议我使用Bisect模块。特别是如果你正在阅读一大堆文字。

如果你真的需要速度,你可以制作一个具有适合任务的预处理数据结构的守护进程(How do you create a daemon in Python?

我建议你可以使用“尝试”

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=usingTries

  

索引和搜索有许多算法和数据结构   文本中的字符串,其中一些包含在标准中   图书馆,但不是全部; trie数据结构很好   一个不是的例子。

     

让word成为单个字符串,让字典成为一大组   话。如果我们有一本字典,我们需要知道一个单词   在字典里面,try是一个可以的数据结构   帮我们。但你可能会问自己,“为什么要设置尝试    和散列表可以做同样的事情吗?“有两个主要原因:

     
      
  • 尝试可以在O(L)时间内插入和查找字符串(其中L代表   一个单词的长度)。这比设置快得多,但是它   比哈希表快一点。
  •   
  • 集合和哈希表   只能在字典中找到与单身完全匹配的单词   我们找到的词;特里允许我们找到有文字的单词   单个字符不同,前缀共同,字符缺失,   等。
  •   
     

尝试在TopCoder问题中很有用,但也有一个   软件工程中的大量应用。例如,   考虑一个Web浏览器。你知道网络浏览器是如何自动的   完成您的文本或向您展示您的文本的许多可能性   可写吗?是的,有了特里,你可以非常快地完成。你呢   知道正交校正器如何检查你的每一个字   类型是在字典中?再一次特里。你也可以使用trie   建议对文本中出现的单词进行更正,但是   不在字典里。

一个例子是:

start={'a':nodea,'b':nodeb,'c':nodec...}
nodea={'a':nodeaa,'b':nodeab,'c':nodeac...}
nodeb={'a':nodeba,'b':nodebb,'c':nodebc...}
etc..

然后,如果你想要所有以ab开头的单词,你就会遍历 开始['a'] ['b']这就是你想要的所有单词。

要构建它,你可以遍历你的单词列表,并为每个单词,遍历字符,在必要时添加一个新的默认字典。

答案 3 :(得分:0)

在二进制搜索的情况下(假设wordlist已排序),我正在考虑这样的事情:

wordlist = "ab", "abc", "bc", "bcf", "bct", "cft", "k", "l", "m"
fragment = "bc"
a, m, b = 0, 0, len(wordlist)-1
iterations = 0

while True:
    if (a + b) / 2 == m: break # endless loop = nothing found
    m = (a + b) / 2
    iterations += 1
    if wordlist[m].startswith(fragment): break # found word
    if wordlist[m] > fragment >= wordlist[a]: a, b = a, m
    elif wordlist[b] >= fragment >= wordlist[m]: a, b = m, b

if wordlist[m].startswith(fragment):
    print wordlist[m], iterations
else:
    print "Not found", iterations

它会找到一个匹配的单词,或者没有。然后,您必须查看其左侧和右侧以查找其他匹配的单词。我的算法可能不正确,它只是我思想的粗略版本。

答案 4 :(得分:0)

这是我将列表 wordlist 缩小到以给定片段开头的有效单词列表的最快方法:

sect()是一个生成器函数,它使用优秀的Peter的想法来使用 bisect islice()函数:

from bisect import bisect_left
from itertools import islice

from time import clock

A,B = [],[]


iterations = 5
repetition = 10

with open('words.txt') as f:
    wordlist = f.read().split()

wordlist.sort()
print 'wordlist[0:10]==',wordlist[0:10]


def sect(wordlist,word_fragment):
    lgth = len(word_fragment)
    for w in islice(wordlist,bisect_left(wordlist, word_fragment),None):
        if w[0:lgth]==word_fragment:
            yield w
        else:
            break


def hooloo(wordlist,word_fragment):
    usque = len(word_fragment)
    for w in wordlist:
        if w[:usque] > word_fragment:
            break
        if w.startswith(word_fragment):
            yield w


for rep in xrange(repetition):
    te = clock()
    for i in xrange(iterations):
        newlistA = list(sect(wordlist,'VEST'))
    A.append(clock()-te)

    te = clock()
    for i in xrange(iterations):
        newlistB = list(hooloo(wordlist,'VEST'))
    B.append(clock() - te)


print '\niterations =',iterations,'   number of tries:',repetition,'\n'
print newlistA,'\n',min(A),'\n'
print newlistB,'\n',min(B),'\n'

结果

wordlist[0:10]== ['AA', 'AAH', 'AAHED', 'AAHING', 'AAHS', 'AAL', 'AALII', 'AALIIS', 'AALS', 'AARDVARK']

iterations = 5    number of tries: 30 

['VEST', 'VESTA', 'VESTAL', 'VESTALLY', 'VESTALS', 'VESTAS', 'VESTED', 'VESTEE', 'VESTEES', 'VESTIARY', 'VESTIGE', 'VESTIGES', 'VESTIGIA', 'VESTING', 'VESTINGS', 'VESTLESS', 'VESTLIKE', 'VESTMENT', 'VESTRAL', 'VESTRIES', 'VESTRY', 'VESTS', 'VESTURAL', 'VESTURE', 'VESTURED', 'VESTURES'] 
0.0286089433154 

['VEST', 'VESTA', 'VESTAL', 'VESTALLY', 'VESTALS', 'VESTAS', 'VESTED', 'VESTEE', 'VESTEES', 'VESTIARY', 'VESTIGE', 'VESTIGES', 'VESTIGIA', 'VESTING', 'VESTINGS', 'VESTLESS', 'VESTLIKE', 'VESTMENT', 'VESTRAL', 'VESTRIES', 'VESTRY', 'VESTS', 'VESTURAL', 'VESTURE', 'VESTURED', 'VESTURES'] 
0.415578236899

sect() holloo()

快14.5倍

PS:

我知道 timeit 的存在,但是在这里,对于这样的结果, clock()就足够了

答案 5 :(得分:-3)

在列表中进行二进制搜索并不能保证任何事情。我不确定这是怎么回事。

你有一个订购的清单,这是一个好消息。你的两种情况的算法性能复杂度都是O(n),这也不错,你只需要迭代整个wordlist一次。

但在第二种情况下,性能(工程性能)应该更好,因为一旦发现其他情况不适用,您就会破裂。尝试列出第一个元素匹配的列表,其余的38000个元素不匹配,第二个元素将匹配第一个元素。