【问题标题】:Faster alternative to for loop in for loopfor 循环中 for 循环的更快替代方案
【发布时间】:2015-12-06 06:00:51
【问题描述】:

我有一本包含约 150,000 个键的字典。没有重复的键。每个键长 127 个字符,每个键在 1-11 个位置处不同(大多数差异发生在键的末尾)。每个键的值是一个唯一的 ID 和一个空白列表 []。对于给定的键,我想找到恰好相差 1 个字符的所有其他键,然后将 ID 附加到给定的键空白列表中。最后,我想要一个键和它的值(一个 ID 和一个所有键的列表,其中一个字符不同)。

我的代码可以运行,但问题是它太慢了。双 for 循环是 150,000^2 = ~250 亿。在我的电脑上,我每分钟可以循环约 200 万次(每次都执行 match1 功能)。这需要大约 8 天才能完成。没有 match1 函数的循环运行速度快了约 7 倍,因此将在约 1 天内完成。

我想知道是否有人知道我可以如何提高这个速度?

# example dictionary
dict = {'key1' : ['1', []], 'key2' : ['2', []], ... , 'key150000' : ['150000', []]}


def match1(s1,s2,dict):    
    s = 0
    for c1, c2 in zip(reversed(s1), reversed(s2)):
        if s < 2:
            if c1 != c2:
                s = s + 1
        else:
            break
    if s == 1:
        dict1[s1][1].append(dict1[s2][0])


for s1 in dict:
    for s2 in dict:
        match1(s1,s2,dict)

【问题讨论】:

  • 不要将变量命名为“dict”。 dict1 取自哪里?
  • 你不需要第二个循环从头开始,因为你会多次比较相同的键
  • 另外,您不必颠倒字符串(此处为 s1 和 s2)。而是从 n-1 向后迭代到 0。这将减少一些时间,尽管在复杂性方面它仍然是相同的。由于两个反向的函数调用,将产生双重影响。

标签: python performance for-loop


【解决方案1】:

目前,您正在将每个键与其他所有键进行比较,总共进行了O(n^2) 比较。洞察力是我们只需要检查其他键的一小部分。

假设每个键的字符具有k 不同值的字母表。例如,如果您的密钥是由a-z0-9 组成的简单ASCII 字符串,那么k = 26 + 10 = 30

给定任何键,我们可以生成一个字符之外的所有可能键:有127 * k 这样的字符串。而在您将每个键与大约 150,000 个其他键进行比较之前,现在我们只需要与 127 * k 进行比较,对于 k = 30 的情况,它是 3810。这将整体时间复杂度从O(n^2) 降低到O(n * k),其中k 是一个独立于n 的常数。 是扩大规模时真正加速的地方n

这里有一些代码可以生成一个键的所有可能邻居:

def generate_neighbors(key, alphabet):
    for i in range(len(key)):
        left, right = key[:i], key[i+1:]
        for char in alphabet:
            if char != key[i]:
                yield left + char + right

所以,例如:

>>> set(generate_neighbors('ab', {'a', 'b', 'c', 'd'}))
{'aa', 'ac', 'ad', 'bb', 'cb', 'db'}

现在我们计算每个键的邻域:

def compute_neighborhoods(data, alphabet):
    keyset = set(data.keys())
    for key in data:
        possible_neighbors = set(generate_neighbors(key, alphabet))
        neighbors = possible_neighbors & keyset

        identifier = data[key][0]

        for neighbor in neighbors:
            data[neighbor][1].append(identifier)

现在举个例子。假设

data = {
 '0a': [4, []],
 '1f': [9, []],
 '27': [3, []],
 '32': [8, []],
 '3f': [6, []],
 '47': [1, []],
 '7c': [2, []],
 'a1': [0, []],
 'c8': [7, []],
 'e2': [5, []]
}

然后:

>>> alphabet = set('abcdef01234567890')
>>> compute_neighborhoods(data, alphabet)
>>> data
{'0a': [4, []],
 '1f': [9, [6]],
 '27': [3, [1]],
 '32': [8, [5, 6]],
 '3f': [6, [8, 9]],
 '47': [1, [3]],
 '7c': [2, []],
 'a1': [0, []],
 'c8': [7, []],
 'e2': [5, [8]]}

还有一些我没有在这里实现的优化。首先,您说键在它们后来的字符上大多不同,并且它们最多在 11 个位置上不同。这意味着我们可以更聪明地计算交叉点possible_neighbors &amp; keyset 和生成邻域。首先,我们修改generate_neighbors,先修改key的尾随字符。然后,我们不是一次生成整个邻居集,而是一次生成一个并检查是否包含在data 字典中。我们会记录我们找到的数量,如果找到 11 个,我们就会破坏。

我没有在我的回答中实现这一点的原因是我不确定它是否会导致显着的加速,实际上可能会更慢,因为这意味着删除优化的 Python带有纯 Python 循环的内置(设置交集)。

【讨论】:

  • 这是我需要的洞察力!虽然键有 127 个字符长,但只有 11 个位置可以更改,我知道这些位置可以是哪些位置,因此我可以生成一个新的更短的键进行比较(无论如何我真的应该这样做!)。
  • 另外,11个位置中的每一个只能更改1-6个其他字符。这应该使我的程序可用。谢谢
【解决方案2】:

这是未经测试的,因此可能只是闲散的猜测,但是...您可以减少字典查找的次数(更重要的是)通过将字典构建到列表中并仅比较剩余项目来消除一半的比较在列表中。

_dict = {'key1' : ['1', []], 'key2' : ['2', []], ... , 'key150000' : ['150000', []]}

# assuming python 3
itemlist = list(_dict.items())

while itemlist:
    key1, value1 = itemlist.pop()
    for key2, value2 in itemlist:
        # doesn't have early short circuit but may have fewer lookups to compensate
        if sum(c1 == c2 for c1, c2 in zip(key1, key2)) == 1:
            value1[1].append(key2)
            value2[1].append(key1)

【讨论】:

    【解决方案3】:

    试试这个代码:

    # example dictionary
    dict = {'key1' : ['1', []], 'key2' : ['2', []], ... , 'key150000' : ['150000', []]}
    
    
    def match1(s1,s2,dict):    
        s = 0
        #reverse and zip computations are avoided
        index = 127-1
        while (index>=0 && s<2):
            if(s1[index] == s2[index]):
                s = s + 1
    
        if (s == 1): 
            #we are modifying both s1 and s2 instead of only s1 to improve performance
            dict1[s1][1].append(dict1[s2][0])
            dict1[s2][1].append(dict1[s1][0])
    
    keys = dict.keys()
    #no of times match1 will be invoked is (n-1)*n/2 instead of n*n
    for i in range(0, len(keys)):
        for j in range(i+1, len(keys)):
            #if match1(s1,s2,dict) is invoked then no need to call match1(s2,s1,dict) because now match1 function will take care of it. So only either one needs to be called
            match1(keys[i],keys[j],dict)
    

    优化:

    • 避免了反向和压缩计算
    • 对于每个键,仅与在键列表中出现在该键之后的键进行比较。
    • match1() 同时修改 s1 和 s2 而不是仅修改 s1。这可以通过交换性来完成,即 s1 与 s2 相比,s2 与 s1 相比是相同的
    • keys 列表存储在一个变量中,并通过索引访问,因此 python 在执行过程中不会创建新的临时列表

    【讨论】:

    • 我想做这样的事情,但不确定使用 i+1 是否可行。
    【解决方案4】:

    对于键匹配部分,使用 Levenshtein 匹配进行非常快速的比较。 Python-Levenshtein 是一个基于 c 扩展的实现。使用它的hamming() 函数来确定不同字符的数量。

    使用 Git 链接安装它:

    pip install git+git://github.com/ztane/python-Levenshtein.git
    

    现在,将其插入@tdelaney 的答案,如下所示:

    import Levenshtein as lv
    
    itemlist = list(_dict.items())
    
    while itemlist:
       if lv.hamming(key1, key2) == 1:
           key1, value1 = itemlist.pop()
           for key2, value2 in itemlist:
               value1[1].append(key2)
               value2[1].append(key1)
    

    【讨论】:

      猜你喜欢
      • 2020-11-14
      • 2018-10-03
      • 1970-01-01
      • 2015-07-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多