做字谜组的更好方法

Posted

技术标签:

【中文标题】做字谜组的更好方法【英文标题】:better ways to do anagram group 【发布时间】:2017-05-23 04:58:26 【问题描述】:

解决以下分组字谜的问题。我目前的解决方案是按单个字符对每个单词进行排序,然后将相同的排序值映射到字典中。

想知道是否有更好的算法时间复杂度更低的想法?我正在考虑不进行排序的方法,例如散列,但散列也需要单词的顺序字符。

发布问题和我的代码,用 Python 2.7 编写。

问题

给出一个单词列表,如 [rats,star,arts,cie,ice],将相同的字谜分组到桶中并输出。 [老鼠,明星,艺术] [cie,冰]

源代码

from collections import defaultdict
def group_anagram(anagrams):
    result = defaultdict(list)
    for a in anagrams:
        result[''.join(sorted(list(a)))].append(a)
    return result

if __name__ == "__main__":
    anagrams = ['rats', 'star', 'arts', 'cie', 'ice']
    print group_anagram(anagrams)

【问题讨论】:

如果是工作代码,这个问题在 Code Review 上不是更好吗? @Tagc,代码审查是审查现有代码,但我的问题更多的是询问是否有更好(时间复杂度较低)的解决方案(这与我当前的代码无关,我展示了我当前的代码为更好的解决方案进行基准测试的目的)。 from itertools import groupby; k:list(v) for k,v in groupby(anagrams, key = lambda x: ''.join(sorted(x))) 是一个解决方案。将其与您已有的进行比较。 您的解决方案似乎很好。小点:''.join(sorted(list(a)))中使用list没有意义。字符串已经是可迭代的,sorted 可以直接应用于它。 我认为您的意思是return result.values() 而不仅仅是return result。代码相当快。在包含 264,097 个单词的 yawl 单词列表上运行大约需要 1 秒。代码将它们分成 235,485 个桶,这意味着大约有 30,000 个重要的字谜。 【参考方案1】:

素数分解是唯一的,乘法的顺序无关紧要。

您可以指定a = 2, b = 3, c = 5, d = 7 等。

然后 dab = 7 * 2 * 3 = 42 = 3 * 2 * 7 = 不好,所以你的哈希值是 42。

另一种选择是hash(frozenset(collections.Counter(word).items()))的有效实现

编辑:可能最快的是使用 26 位。对于单词中的每个字符,翻转对应的位。您可能会遇到一些冲突,在这种情况下,您可以在查找时进行重复数据删除

【讨论】:

2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103 如果单词超过 6 个字母,质数概念将不起作用(至少完全正确)。它将返回可以重复数据删除的冲突。好主意! @ElKamina 为什么它不适用于超过 6 个字母的单词?你有一个不起作用的例子吗?我相信它会一直有效 @bigballer 假设我们按顺序分配质数。那么z=101。所以 zzzzzz 大约是一万亿。使用无符号整数(4 个字节),您最多可以存储 40 亿个。您可以使用 long,但这会帮助您直到大约 12 字大小。简而言之,每个字母平均需要 5 位,更多的字母意味着我们需要更多的存储大小(无损)。 >>> 1016 1061520150601 >>> 264 18446744073709551616L 应该很容易适应 64 位整数。如果有人担心,总是可以将较小的素数分配给更常见的字母。并使用自适应整数大小(即每个整数大小都有一个哈希表,并根据哈希大小选择哈希表)【参考方案2】:

您当前的方法可能是最好的。为了测试事情,我使用了你的方法,@bigballer 的优秀答案中的方法和使用计数元组作为键的第三种方法。为了对这些方法进行压力测试,我在海量(264,097 字)单词列表yawl 上使用了它们,每个函数运行 100 次,并计算每种方法的平均时间:

from collections import defaultdict
import timeit

def group_anagram1(anagrams):
    result = defaultdict(list)
    for a in anagrams:
        result[''.join(sorted(a))].append(a)
    return result.values()

primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]

def group_anagram2(anagrams):
    result = defaultdict(list)
    for a in anagrams:
        n = 1
        for c in a:
            n *= primes[ord(c) - ord('a')]
        result[n].append(a)
    return result.values()

def group_anagram3(anagrams):
    result = defaultdict(list)
    for a in anagrams:
        counts = [0]*26
        for c in a:
            counts[ord(c) - ord('a')] += 1
        result[tuple(counts)].append(a)
    return result.values()



with open("yawl.txt") as f:
    words = f.readlines()
    words =[w.strip() for w in words]

print timeit.timeit("group_anagram1(words)", setup="from __main__ import group_anagram1,words",number = 100)/100.0
print timeit.timeit("group_anagram2(words)", setup="from __main__ import group_anagram2,words",number = 100)/100.0
print timeit.timeit("group_anagram3(words)", setup="from __main__ import group_anagram3,words",number = 100)/100.0

输出(在我的机器 YMMV 上):

0.486009083239
0.64333226691
0.797640375079

真的,考虑到yawl 的大小,所有方法都非常快,每个方法都需要不到一秒的时间来处理超过 25 万字。尽管如此,您的原始方法显然是赢家。此外,它不限于拉丁文'a''z' 字母表。至于为什么它是最好的——关键是直接由 Python 内置程序(运行优化的 C 代码)构造,但其他方法使用解释的 Python 代码。很难击败内置插件。

编辑时:我使用这个素数列表重新实现了第二种方法,并进行了排序,以便为更频繁的字母(在英语中)分配较小的素数:

primes = [5,71,37,29,2,53,59,19,11,83,79,31,43,13,7,67,97,23,17,3,41,73,47,89,61,101]

它缩短了几分之一秒的时间,但还不足以使其比第一种方法更快。

进一步编辑

我重新运行了上面的代码,并对第二种方法进行了以下调整(如 @bigballer 所建议的那样):

primes = [5,71,37,29,2,53,59,19,11,83,79,31,43,13,7,67,97,23,17,3,41,73,47,89,61,101]
primes = c:p for c,p in zip('abcdefghijklmnopqrstuvwxyz',primes)

def group_anagram2(anagrams):
    result = defaultdict(list)
    for a in anagrams:
        n = 1
        for c in a:
            n *= primes[c]
        result[n].append(a)
    return result.values()

在这个版本中,前两种方法几乎平分秋色,在我有限的测试中,基于素数的方法稍微快一些(快了大约 8%)。尽管如此,我仍然认为第一种方法更可取,因为它不依赖于固定的字母表。

【讨论】:

ord(c)-ord('a') 会很慢。相反,您应该设置 x = 'a':2, 'b': 3, c:'5'.. 并使用 x[c],它会快得多。即 n *= x[c] 对于 anagram3,你不应该使用 [0]*26,你应该使用 counts = 1 @bigballer 您对第二种方法的字典方法是正确的,但我不明白对anagram3 的拟议更改将如何避免冲突(需要处理)。跨度> 你可以修改第三种方法。如果有任何位重复,则 count += 1 @bigballer 这是一个有趣的想法。如果以后有时间我会尝试一下。快速检查发现yawl.txt 中 81.5% 的单词至少有一个重复的字母,所以我不会将需要记录计数 > 1 的情况称为“罕见”。

以上是关于做字谜组的更好方法的主要内容,如果未能解决你的问题,请参考以下文章

确定一个列表是不是由 Java 8 中的字谜元素组成

搜索字谜的最快方法是啥?

Javascript - 查找字谜的更好解决方案 - 时间复杂度 O (n log n)

在目标 c 中找到数组内所有字谜的快速方法是啥?

字谜检测方法c++。将字符串转换为 ascii 值的问题

有啥简单的方法可以判断单词列表是不是是彼此的字谜?