【问题标题】:Performance of my function removing keys from a dictionary which are substrings of other keys我的函数从字典中删除键的性能,这些键是其他键的子字符串
【发布时间】:2025-12-15 22:40:01
【问题描述】:

我很好奇为什么在我的代码中删除一行会显着提高性能。该函数本身采用字典并删除作为其他键的子字符串的所有键。

使我的代码变慢的行是:

if sub in reduced_dict and sub2 in reduced_dict:

这是我的功能:

def reduced(dictionary):
    reduced_dict = dictionary.copy()
    len_dict = defaultdict(list)
    for key in dictionary:
        len_dict[len(key)].append(key)
    start_time = time.time()
    for key, subs in len_dict.items():
        for key2, subs2 in len_dict.items():
            if key2 > key:
                for sub in subs:
                    for sub2 in subs2:
                        if sub in reduced_dict and sub2 in reduced_dict: # Removing this line gives a significant performance boost
                            if sub in sub2:
                                reduced_dict.pop(sub, 0)
    print time.time() - start_time
    return reduced_dict

该函数多次检查 sub 是否在 sub2 中。我假设如果我检查了已经进行的比较,我会节省自己的时间。情况似乎并非如此。为什么在字典中查找的常数时间函数会减慢我的速度?

我是初学者,所以我对概念很感兴趣。

当我测试有问题的行是否返回 False 时,似乎确实如此。我已经用以下方法对此进行了测试

def reduced(dictionary):
    reduced_dict = dictionary.copy()
    len_dict = defaultdict(list)
    for key in dictionary:
        len_dict[len(key)].append(key)
    start_time = time.time()
    for key, subs in len_dict.items():
        for key2, subs2 in len_dict.items():
            if key2 > key:
                for sub in subs:
                    for sub2 in subs2:
                        if sub not in reduced_dict or sub2 not in reduced_dict:
                            print 'not present' # This line prints many thousands of times
                        if sub in sub2:
                            reduced_dict.pop(sub, 0)
    print time.time() - start_time
    return reduced_dict

对于函数输入字典中的 14,805 个键:

  • 19.6360001564 秒。没有线
  • 33.1449999809 秒。用线

这里有 3 个字典示例。 Biggest sample dictionary with 14805 keysmedium sample dictionarysmaller sample dictionary

对于最大示例字典中的前 14,000 个键,我绘制了以秒 (Y) 为单位的时间与以键数 (X) 为单位的输入大小的关系图。看来所有这些函数都具有指数复杂性。

  • John Zwinck answer 这个问题
  • 马特我的算法这个问题没有字典 比较
  • Matt exponential 是我第一次尝试解决这个问题。这花了 76 秒
  • Matt compare 是这个问题中的算法,带有 dict 比较线
  • tdelaney solution 这个问题。算法 1 和 2 按顺序排列
  • georg solution 来自我提出的一个相关问题

接受的答案在明显的线性时间内执行。

我很惊讶地发现输入大小存在魔法比率,其中 dict 查找的运行时间 == 字符串搜索。

【问题讨论】:

  • 你能包含你用来测试这个的示例字典吗?此外,使用time.time() 来衡量这一点通常不够准确。您应该改用timeit 模块。
  • 这里有 2 个字典示例。第一个较长justpaste.it/festival_dict,第二个较短justpaste.it/ATGC
  • 由于四重嵌套的“for”循环,您的代码可能比其他任何东西都慢。 :)
  • @John Zwinck 在尝试解决相同问题时,上面的代码运行速度比我之前的问题中的解决方案快得多。 *.com/a/25422647/3761932 我会尝试解决嵌套问题,但这是另一个问题。
  • 您描述了一种对键进行操作的算法,但您的代码对值进行了操作。这只是一个错字吗?

标签: python performance algorithm dictionary


【解决方案1】:

对于样本语料库,或任何大多数键都很小的语料库,测试所有可能的子键要快得多:

def reduced(dictionary):
    keys = set(dictionary.iterkeys())
    subkeys = set()
    for key in keys:
        for n in range(1, len(key)):
            for i in range(len(key) + 1 - n):
               subkey = key[i:i+n]
               if subkey in keys:
                   subkeys.add(subkey)

    return {k: v
            for (k, v) in dictionary.iteritems()
            if k not in subkeys}

这在我的系统(i7-3720QM 2.6GHz)上大约需要 0.2 秒。

【讨论】:

  • 这是最快的算法。我正在绘制超过 103,000 个键。我会将此图添加到问题中。 dict 查找 == 字符串搜索时,输入大小似乎有一个神奇的比率。
【解决方案2】:

我会做一些不同的事情。这是一个生成器函数,它只为您提供“好”键。这避免了创建一个可能会被大量逐个键破坏的字典。我也只有两个级别的“for”循环和一些简单的优化,以尝试更快地找到匹配项并避免搜索不可能的匹配项。

def reduced_keys(dictionary):
    keys = dictionary.keys()
    keys.sort(key=len, reverse=True) # longest first for max hit chance                                                                                                     
    for key1 in keys:
        found_in_key2 = False
        for key2 in keys:
            if len(key2) <= len(key1): # no more keys are long enough to match                                                                                              
                break
            if key1 in key2:
                found_in_key2 = True
                break
        if not found_in_key2:
            yield key1

如果你想用这个来做一个实际的字典,你可以:

{ key: d[key] for key in reduced_keys(d) }

【讨论】:

  • 对于我的 14,805 键字典 justpaste.it/14805keys yours = 27.7s 与我的函数相同的 7086 键。我的 = 17.7 秒。我会尝试以不同的方式计时。您的发电机最昂贵的部分是什么?是长度比较还是填充字典?
  • 有趣。我们可以在内部循环之外缓存 key1 的长度。如果你想要更高的速度(但仍然可以从 Python 中使用),这个算法应该很容易移植到 C 中。如果您以非反向顺序排序并完全检查密钥长度,我很想知道它有多快。
【解决方案3】:

您创建了 len_dict,但即使它对大小相等的键进行分组,您仍然必须多次遍历所有内容才能进行比较。你的基本计划是正确的——按尺寸排序,只比较相同或更大的尺寸,但还有其他方法可以做到这一点。下面,我刚刚创建了一个按键大小排序的常规列表,然后向后迭代,以便我可以随时修剪字典。我很好奇它的执行时间与你的相比如何。它在 0.049 秒内完成了您的小 dict 示例。

(我希望它确实有效!)

def myfilter(d):
    items = d.items()
    items.sort(key=lambda x: len(x[0]))
    for i in range(len(items)-2,-1,-1):
        k = items[i][0]
        for k_fwd,v_fwd in items[i+1:]:
            if k in k_fwd:
                del items[i]
                break
    return dict(items)

编辑

通过不解包 k_fwd,v_fwd 显着提高了速度(在运行了几次之后,这并不是真正的加速。其他东西一定在我的电脑上消耗了一段时间)。

def myfilter(d):
    items = d.items()
    items.sort(key=lambda x: len(x[0]))
    for i in range(len(items)-2,-1,-1):
        k = items[i][0]
        for kv_fwd in items[i+1:]:
            if k in kv_fwd[0]:
                del items[i]
                break
    return dict(items)

【讨论】:

  • 你的函数更快。对于 14,805 个键,我没有问题行的函数在 17.75 秒内运行。你的 12.3139998913 秒。您的函数返回 7087 个键和我的 7086 个键,这很有趣。我喜欢你的方法。您知道为什么字典查找会延长我的函数的运行时间吗?
  • @mattkaeo 字典查找不是免费,它们只是(伪)恒定时间。由于代码的结构,您需要进行数百万次查找。
  • 1.您的 len_dict.items() 的 2 个 for 循环意味着您在第二个循环 len(len_dict) 次中重建项目,并在之后的 if 中将大部分项目丢弃。与列表中的索引查找相比,剩余的 dict 查找相当昂贵。事实上,我以为我会比你打败更多。现在我很困惑……
  • @tdelaney 这很有趣。我不知道字典查找可能比列表中的索引查找更昂贵。这是我用于实验的更大的 14805 密钥长度字典。 justpaste.it/14805keys
  • 哎呀,我从我生成的带有 22056 个项目的字典中读取了结果。你的 14805 设置是 4.00 秒。有趣的是,随着更大的集合增加时间 - 不是指数增长!