32

分析显示这是我编写的一个小文字游戏代码中最慢的部分:

def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

笔记:

  • distance()被调用超过 500 万次,其中大部分来自 getchildren,它应该获取单词列表中与word恰好相差 1 个字母的所有单词。
  • wordlist 被预先过滤为仅包含包含相同数量字母的单词,word因此可以保证word1并且word2具有相同数量的字符。
  • 我对 Python 相当陌生(3 天前开始学习它),所以对命名约定或其他样式的评论也很感激。
  • 对于单词表,使用“2+2lemma.txt”文件获取12dict 单词表

结果:

谢谢大家,结合不同的建议,我现在让程序运行速度提高了两倍(除了我在询问之前自己进行的优化,所以速度比我最初的实现提高了大约 4 倍)

我用两组输入进行了测试,我将它们称为 A 和 B

优化 1:迭代 word1,2 的索引... from

for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

迭代字母对使用zip(word1, word2)

for x,y in zip (word1, word2):
        if x != y:
            difference += 1
    return difference

输入 A 的执行时间从 11.92 到 9.18,输入 B 的执行时间从 79.30 到 74.59

优化 2:除了距离方法(我在其他地方仍然需要 A* 启发式)之外,还添加了一种单独的方法

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in zip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

输入 A 的执行时间从 9.18 到 8.83,输入 B 的执行时间从 74.59 到 70.14

优化3:这里的大赢家是使用izip而不是zip

输入 A 的执行时间从 8.83 到 5.02,输入 B 的执行时间从 70.14 到 41.69

我可能会更好地用较低级别的语言编写它,但我现在对此感到满意。谢谢大家!

再次编辑:更多结果使用 Mark 的方法检查第一个字母不匹配的情况使其从 5.02 -> 3.59 和 41.69 -> 29.82 下降

在此基础上并合并izip而不是range,我最终得到了这个:

def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different

挤压得更多,使时间从 3.59 -> 3.38 和 29.82 -> 27.88

甚至更多的结果!

尝试 Sumudu 的建议,即我生成一个与“word”相差 1 个字母的所有字符串的列表,然后检查哪些字符串在 wordlist中,而不是 is_neighbor 函数,我最终得到了这个:

def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return ( w for w in oneoff if w in wordlist )

最终速度变慢了(3.38 -> 3.74 和 27.88 -> 34.40),但看起来很有希望。起初我认为我需要优化的部分是“one_letter_off_strings”,但分析显示并非如此,而且慢的部分实际上是

( w for w in oneoff if w in wordlist )

我想如果我切换“oneoff”和“wordlist”并在我正在寻找两个列表的交集时以另一种方式进行比较,是否会有任何区别。我用字母上的 set-intersection替换它:

return set(oneoff) & set(wordlist)

砰!3.74 -> 0.23 和 34.40 -> 2.25

这真是令人惊讶,与我最初的幼稚实现的总速度差异:23.79 -> 0.23 和 180.07 -> 2.25,比原来的实现快大约 80 到 100 倍。

如果有人有兴趣,我会发表博客文章来描述程序描述所做的优化,包括此处未提及的优化(因为它位于不同的代码部分中)。

大辩论:

好的,我和 Unknown 正在进行一场大辩论,您可以在他的回答的评论中阅读。他声称,如果将其移植到 C 中,使用原始方法(使用 is_neighbor 而不是使用集合)会更快。我尝试了 2 个小时来获得我编写的 C 模块来构建和可链接,但在尝试后没有太大成功按照这个这个例子,看起来这个过程在Windows中有点不同?我不知道,但我放弃了。无论如何,这是程序的完整代码,文本文件来自12dict单词列表使用“2+2lemma.txt”文件。对不起,如果代码有点乱,这只是我一起破解的。此外,我忘记从单词列表中删除逗号,因此这实际上是一个错误,您可以保留它以进行相同的比较,或者通过在 cleanentries 的字符列表中添加逗号来修复它。

from itertools import izip
def unique(seq):  
    seen = {} 
    result = [] 
    for item in seq: 
        if item in seen:
            continue 
        seen[item] = 1 
        result.append(item) 
    return result
def cleanentries(li):
    pass
    return unique( [w.strip('[]') for w in li if w != "->"] )
def distance(word1, word2):
    difference = 0
    for x,y in izip (word1, word2):
        if x != y:
            difference += 1
    return difference
def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different
def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)
def AStar(start, goal, wordlist):
    import Queue
    closedset = []
    openset = [start]
    pqueue = Queue.PriorityQueue(0)
    g_score = {start:0}         #Distance from start along optimal path.
    h_score = {start:distance(start, goal)}
    f_score = {start:h_score[start]}
    pqueue.put((f_score[start], start))
    parent_dict = {}
    while len(openset) > 0:
        x = pqueue.get(False)[1]
        if x == goal:
            return reconstruct_path(parent_dict,goal)
        openset.remove(x)
        closedset.append(x)
        sortedOpen = [(f_score[w], w, g_score[w], h_score[w]) for w in openset]
        sortedOpen.sort()
        for y in getchildren(x, wordlist):
            if y in closedset:
                continue
            temp_g_score = g_score[x] + 1
            temp_is_better = False
            appended = False
            if (not y in openset): 
                openset.append(y)
                appended = True
                h_score[y] = distance(y, goal)
                temp_is_better = True
            elif temp_g_score < g_score[y] :
                temp_is_better = True
            else :
                pass
            if temp_is_better:
                parent_dict[y] = x
                g_score[y] = temp_g_score
                f_score[y] = g_score[y] + h_score[y]
                if appended :
                    pqueue.put((f_score[y], y))
    return None


def reconstruct_path(parent_dict,node):
     if node in parent_dict.keys():
         p = reconstruct_path(parent_dict,parent_dict[node])
         p.append(node)
         return p
     else:
         return []        

wordfile = open("2+2lemma.txt")
wordlist = cleanentries(wordfile.read().split())
wordfile.close()
words = []
while True:
    userentry = raw_input("Hello, enter the 2 words to play with separated by a space:\n ")
    words = [w.lower() for w in userentry.split()]
    if(len(words) == 2 and len(words[0]) == len(words[1])):
        break
print "You selected %s and %s as your words" % (words[0], words[1])
wordlist = [ w for w in wordlist if len(words[0]) == len(w)]
answer = AStar(words[0], words[1], wordlist)
if answer != None:
    print "Minimum number of steps is %s" % (len(answer))
    reply = raw_input("Would you like the answer(y/n)? ")
    if(reply.lower() == "y"):
        answer.insert(0, words[0])
        print "\n".join(answer)
    else:
        print "Good luck!"
else:
    print "Sorry, there's no answer to yours"
reply = raw_input("Press enter to exit")

我留下了 is_neighbors 方法,即使它没有被使用。这是建议移植到 C 的方法。要使用它,只需将 getchildren 替换为:

def getchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w))

至于让它作为 C 模块工作,我并没有那么远,但这就是我想出的:

#include "Python.h"

static PyObject *
py_is_neighbor(PyObject *self, Pyobject *args)
{
    int length;
    const char *word1, *word2;
    if (!PyArg_ParseTuple(args, "ss", &word1, &word2, &length))
        return NULL;

    int i;
    int different = 0;
    for (i =0; i < length; i++)
    {
        if (*(word1 + i) != *(word2 + i))
        {
            if (different)
            {
                return Py_BuildValue("i", different);
            }
            different = 1;
        }
    }
    return Py_BuildValue("i", different);
}

PyMethodDef methods[] = {
    {"isneighbor", py_is_neighbor, METH_VARARGS, "Returns whether words are neighbors"},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initIsNeighbor(void)
{
    Py_InitModule("isneighbor", methods);
}

我使用以下方法对此进行了分析:

python -m cProfile "Wordgame.py"

记录的时间是AStar方法调用的总时间。快速输入集是“诗歌诗人”,而长输入集是“诗人诗歌”。不同机器之间的时间显然会有所不同,因此如果有人最终尝试这样做,请按原样比较程序的结果,以及与 C 模块的比较。

4

12 回答 12

24

如果您的单词列表很长,从“单词”生成所有可能的 1 个字母差异,然后检查列表中的哪些可能会更有效?我不知道任何 Python,但应该有一个合适的数据结构用于 wordlist 允许日志时间查找。

我建议这样做是因为如果您的单词长度合理(约 10 个字母),那么您只会寻找 250 个潜在单词,如果您的单词列表大于几百个单词,这可能会更快。

于 2009-04-25T07:38:18.173 回答
10

distance当您真正只关心距离 = 1 时,您的函数正在计算总距离。大多数情况下,您会在几个字符内知道它 >1,因此您可以提前返回并节省大量时间。

除此之外,可能还有更好的算法,但我想不出。

编辑:另一个想法。

您可以制作 2 种情况,具体取决于第一个字符是否匹配。如果不匹配,则单词的其余部分必须完全匹配,您可以一次性测试。否则,请按照您所做的类似方式进行操作。您甚至可以递归地执行此操作,但我认为这不会更快。

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    same = True
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if same:
                same = False
            else:
                return False
    return not same

编辑2:我已经删除了检查字符串是否相同长度的检查,因为你说它是多余的。在我自己的代码和 MizardX提供的 is_neighbors 函数上运行Ryan 的测试,我得到以下信息:

  • 原始距离():3.7秒
  • My DifferentByOne():1.1 秒
  • MizardX 的 is_neighbors():3.7 秒

编辑3:(可能在这里进入社区维基领域,但是......)

尝试使用 izip 而不是 zip 对 is_neighbors() 的最终定义:2.9 秒。

这是我的最新版本,仍然是 1.1 秒:

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if different:
                return False
            different = True
    return different
于 2009-04-25T02:30:29.147 回答
6
from itertools import izip

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in izip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

或者也许内联izip代码:

def is_neighbors(word1,word2):
    different = False
    next1 = iter(word1).next
    next2 = iter(word2).next
    try:
        while 1:
            if next1() != next2():
                if different:
                    return False
                different = True
    except StopIteration:
        pass
    return different

并重写getchildren

def iterchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w) )
于 2009-04-25T02:41:49.370 回答
4

人们主要是通过尝试编写一个更快的函数来解决这个问题,但可能还有另一种方式..

“距离”被调用超过 500 万次

为什么是这样?也许更好的优化方法是尝试减少对 的调用次数distance,而不是减少几毫秒的distance's执行时间。没有看到完整的脚本就无法判断,但是优化特定功能通常是不必要的。

如果这是不可能的,也许你可以把它写成一个 C 模块?

于 2009-04-25T03:25:37.377 回答
3

使用相同参数调用距离函数的频率是多少?一个简单的实现优化是使用memoization

您可能还可以创建某种字典,其中包含冻结的字母集和相差一个的单词列表,并在其中查找值。该数据结构可以通过 pickle 存储和加载,也可以在启动时从头开始生成。

短路评估只会在您使用的单词很长时给您带来收益,因为您使用的汉明距离算法基本上是 O(n),其中 n 是单词长度。

我对 timeit 进行了一些实验,以获得一些可能具有说明性的替代方法。

时间结果

您的解决方案

d = """\
def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference
"""
t1 = timeit.Timer('distance("hello", "belko")', d)
print t1.timeit() # prints 6.502113536776391

一个班轮

d = """\
from itertools import izip
def hamdist(s1, s2):
    return sum(ch1 != ch2 for ch1, ch2 in izip(s1,s2))
"""
t2 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 10.985101179

捷径评估

d = """\
def distance_is_one(word1, word2):
    diff = 0
    for i in xrange(len(word1)):
        if word1[i] != word2[i]:
            diff += 1
        if diff > 1:
            return False
    return diff == 1
"""
t3 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 6.63337
于 2009-04-25T03:14:32.177 回答
2

好吧,如果差异为 2 或更多,您可以先中断循环。

你也可以改变

for i in range(len(word1)):

for i in xrange(len(word1)):

因为 xrange 按需生成序列,而不是一次生成整个数字范围。

您也可以尝试比较字长,这样会更快。另请注意,如果 word1 大于 word2,您的代码将不起作用

在那之后你可以在算法上做的事情不多,也就是说,通过将该部分移植到 C 中,你可能会发现更多的加速。

编辑 2

试图解释我对 Sumudu 算法的分析与逐个字符验证差异的比较。

当您有一个长度为 L 的单词时,您将生成的“相差一个”单词的数量将是 25L。我们从现代计算机上的集合实现中知道,搜索速度大约是log(n) base 2,其中 n 是要搜索的元素数。

看到你测试的 500 万个单词中的大部分都不在集合中,大多数时候,你会遍历整个集合,这意味着它真的变成了log(25L)而不仅仅是 log(25L)/2。(这是假设集合的最佳情况,逐个字符串比较等同于逐个字符比较)

现在我们来看看确定“逐个不同”的时间复杂度。如果我们假设您必须检查整个单词,那么每个单词的操作数变为L。我们知道大多数单词很快就会相差 2。并且知道大多数前缀只占单词的一小部分,我们可以从逻辑上假设您大部分时间会被L/2或一半单词打破(这是一个保守的估计)。

所以现在我们绘制两个搜索的时间复杂度,L/2 和 log(25L),并记住,这甚至考虑到字符串匹配与字符匹配相同的速度(非常有利于集合)。您有方程 log(25*L) > L/2,可以简化为 log(25) > L/2 - log(L)。从图中可以看出,使用 char 匹配算法应该更快,直到达到非常大的 L。

替代文字

另外,我不知道您是否指望优化中的差异为 2 或更多,但从马克的回答中,我已经打破了 2 或更多的差异,实际上,如果第一个字母的差异,它在第一个字母之后中断,即使进行了所有这些优化,更改为使用集合只是将它们从水中吹走。不过我有兴趣尝试你的想法

我是这个问题中第一个建议打破 2 或更多差异的人。问题是,Mark 对字符串切片的想法(如果 word1[0] != word2[0]: return word1[1:] == word2[1:])只是将我们正在做的事情放到 C 中。你怎么样认为word1[1:] == word2[1:]是计算出来的?就像我们正在做的一样。

我看了你的解释几次,但我不太明白,你介意再深入解释一下吗?此外,我对 C 不是很熟悉,过去几年我一直在使用高级语言(最接近的是 6 年前在高中学习 C++

至于制作C代码,我有点忙。我相信您将能够做到这一点,因为您以前用 C 编写过。您也可以尝试 C#,它可能具有类似的性能特征。

更多解释

这是对Davy8的更深入的解释

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)

您的 one_letter_off_strings 函数将创建一组 25L 字符串(其中 L 是字母数)。

从单词表中创建一个集合将创建一组 D 字符串(其中 D 是字典的长度)。通过从中创建一个交集,您必须遍历每个oneoff并查看它是否存在于wordlist中。

此操作的时间复杂度在上面详细说明。此操作的效率低于将您想要的单词与 wordlist 中的每个单词进行比较。Sumudu 的方法是 C 语言而不是算法的优化。

更多解释2

总共只有 4500 个单词(因为在传递给算法之前,单词列表已经预先过滤了 5 个字母的单词),与 125 个单字母单词相交。似乎您说的交集是 log(smaller) 或者换句话说 log(125, 2)。将此与再次假设您所说的进行比较,在比较 L/2 字母中的单词中断时,我会将其四舍五入为 2,即使对于 5 个字母的单词,它更有可能是 3。这种比较进行了 4500 次,所以 9000。log(125,2) 大约是 6.9,log(4500,2) 大约是 12。让我知道我是否误解了你的数字。

要创建 125 个单字母单词与 4500 个字典的交集,您需要进行 125 * 4500 次比较。这不是 log(125,2)。假设字典是预先排序的,最好是 125 * log(4500, 2)。集合没有神奇的捷径。您还在这里逐个字符串而不是逐个字符进行比较。

于 2009-04-25T02:27:44.787 回答
1

对于这样一个具有如此大的性能影响的简单函数,我可能会制作一个 C 库并使用ctypes调用它。reddit 的一位创始人声称,他们使用这种技术使网站的速度提高了 2 倍。

您也可以在此功能上使用psyco,但要注意它会占用大量内存。

于 2009-04-25T02:44:00.177 回答
0

我不知道它是否会显着影响您的速度,但您可以先将列表理解转换为生成器表达式。它仍然是可迭代的,所以在使用上应该没有太大的不同:

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

def getchildren(word, wordlist):
    return ( w for w in wordlist if distance(word, w) == 1 )

主要问题是列表推导会在内存中构建自身并占用相当多的空间,而生成器将动态创建您的列表,因此无需存储整个内容。

此外,根据未知的答案,这可能是一种更“pythonic”的写距离()的方式:

def distance(word1, word2):
    difference = 0
    for x,y in zip (word1, word2):
        if x == y:
            difference += 1
    return difference

但是当 len (word1) != len (word2) 时,它的意图令人困惑,在 zip 的情况下,它只会返回与最短单词一样多的字符。(这可能是一种优化......)

于 2009-04-25T02:29:48.227 回答
0

试试这个:

def distance(word1, word2):
  return sum([not c1 == c2 for c1, c2 in zip(word1,word2)])

另外,你的游戏有链接吗?我喜欢被文字游戏摧毁

于 2009-04-25T02:48:35.030 回答
0

我首先想到的是:

from operator import ne

def distance(word1, word2):
    return sum(map(ne, word1, word2))

它很有可能比人们发布的其他函数更快,因为它没有解释循环,只是调用 Python 原语。它足够短,您可以合理地将其内联到调用者中。

对于您的更高级别的问题,我会研究为度量空间中的相似性搜索而开发的数据结构,例如本文这本书,我都没有读过(他们是在搜索我读过的论文时出现的)但不记得了)。

于 2009-04-25T04:55:54.303 回答
0

对于这个片段:

for x,y in zip (word1, word2):
    if x != y:
        difference += 1
return difference

我会用这个:

return sum(1 for i in xrange(len(word1)) if word1[i] == word2[i])

提供的代码将遵循相同的模式...

于 2009-08-19T17:41:47.787 回答
0

其他人只关注显式距离计算,而没有做任何关于构建距离 1 候选的事情。您可以通过使用称为Trie的著名数据结构将隐式距离计算生成所有距离为 1 的相邻词的任务合并来进行改进。Trie 是一个链表,其中每个节点代表一个字母,“next”字段是一个最多包含 26 个条目的 dict,指向下一个节点。

这是伪代码:为给定的单词迭代遍历 Trie;在每个节点将所有距离为 0 和距离为 1 的邻居添加到结果中;保持距离计数器并减少它。您不需要递归,只需一个查找函数,它需要一个额外的 distance_so_far 整数参数。

通过为长度为 3、长度为 4、长度为 5 等单词构建单独的尝试,可以获得 O(N) 空间增加的额外速度的小权衡。

于 2016-10-14T20:54:28.553 回答