我怎样才能加快这个 Anagram 算法

Posted

技术标签:

【中文标题】我怎样才能加快这个 Anagram 算法【英文标题】:How Can I Speed Up This Anagram Algorithm 【发布时间】:2011-09-27 03:45:42 【问题描述】:

我正在制作一个移动应用程序来查找字谜和部分匹配项。移动设备很重要,因为计算能力不强,而效率是关键。

该算法采用任意数量的字母,包括重复字母,并找到由其字母组成的最长单词,每个字母仅使用一次。我也有兴趣快速找到最高的结果,只要满足 N,我并不真正关心底部(较短的)。例如:

STACK => stack, tacks, acts, cask, cast, cats…

我进行了一些谷歌搜索并找到了一些算法,我想出了一个我认为有效的算法,但没有我想要的那么有效。

我有一个预先制作的查找字典,它将排序的键映射到生成该键的真实单词。

"aelpp" => ["apple", "appel", "pepla"]

我已根据键的长度将每个字典进一步拆分为不同的字典。因此,5 个字母长的键在一个字典中,6 个键在另一个字典中。这些字典中的每一个都位于一个数组中,其中索引是在字典中找到的键的长度。

anagramArray[5] => dictionary5
dictionary5["aelpp"] => ["apple", "appel", "pepla"]

我的算法从输入单词“lappe”开始,然后对其进行排序:

"lappe" => "aelpp"

现在,对于每本最多包含 5 个字母的字典,我进行比较以将其提取出来。这是伪代码:

word = input.sort
for (i = word.length; i > 0; i--)
    dictionaryN = array[i]
    for (key in dictionaryN)
        if word matches key
            add to returnArray
        end
    end
    if returnArray count > N
      break
    end
end

returnArray.sort by longest word, alphabetize

该词典中只有大约 170,000 个单词,但对于 12 个字母的输入,搜索最多需要 20 秒。我的match 方法从键中生成了一个正则表达式:

"ackst" => /a.*c.*k.*s.*t.*/

这样,例如,一个 4 个字母的键,如 acst(行为),将匹配 ackst(堆栈),因为:

"ackst" matches /a.*c.*s.*t.*/

我看到其他应用程序在更短的时间内做同样的事情,我想知道我的方法是垃圾还是需要一些调整。

如何获得最大的计算效率以从一个单词中生成前 N 个字谜,按最大长度排序?

【问题讨论】:

CodeReview 上的相关问题:codereview.stackexchange.com/questions/1690/… 我设计了一个漂亮的字典结构优化,使用记录数组而不是树,数组内的相对偏移量,以及一些位打包。如果您有兴趣,我可以详细说明。 我实际上用自己的方法解决了这个问题,并得到了 mcdowella 的提示。它非常快,不使用字典树,而是使用 dfs 搜索原始单词。我不想再优化了,但如果你写了一篇博文,我会读的。 【参考方案1】:

如果您将字典想象(甚至可能表示)为一棵字母树,您可以避免查看大量节点。如果“堆栈”在字典中,那么将有一条从根到标记为 a-c-k-s-t 的叶的路径。如果输入单词是“attacks”,那么对它进行排序以获得 aackstt。您可以编写一个递归例程来跟踪从根开始的链接,同时使用来自 aackstt 的字母。当您到达 ack 时,您的字符串中会留下 stt,因此您可以按照 s 到达 ackst,但您可以排除按照 u 到达 acku 及其后代,v 到达 ackv 及其后代,等等。

事实上,通过这种方案,您可以只使用一棵树来保存任意数量字母的单词,这样可以节省您进行多次搜索,每个目标长度一次。

【讨论】:

问题是“边走边写”。如果我的输入中有 n 个字母,那么即使在对单词进行排序之后,树中是否还有 2^n 条路径要遵循?对于排序输入中的每个字母,我可以将其保留在搜索字符串中或将其删除,对吗?这意味着对于 16,我必须搜索 65536 条路径。我错了吗? 以上应该仍然有效。只需将您的搜索字符串 (S) 递归地分解为子字符串。然后搜索每个子字符串。 我知道它会起作用,但这并不能解决我对拥有 2^n 个子字符串以及因此需要搜索 2^n 个树路径的担忧。 我的回答和这个类似(但字数更多),所以+1给你。 在任何情况下,成本都是由您访问的节点数决定的。您最多访问每个节点一次,因此它受树中节点数的限制,即扫描整个字典的成本。事实上,因为您不会访问带有不在输入中的字母的节点的后代,所以您不会支付所有这些成本,除非您的输入字符串包含字母表中的每个字母。一旦你找到了一个好的解决方案,你就可以找到子树中没有任何东西可能导致更好的解决方案的情况,并且也停止在那里寻找。【参考方案2】:

生成正则表达式有点昂贵,因此您可能不希望在循环中这样做。

想到的一个选项(不一定是超级高效,但在这种特殊情况下似乎很有用)是,与其在字典中搜索所有单词,不如尝试删除各种组合的字母并检查结果是否字符串在您的字典中。这将在 2^n 次迭代时达到最大值(其中 n 是单词中的字母数),对于 n

【讨论】:

我希望大多数输入是 12 个字母,因此发现删除字母的所有排列都是 12! (12 阶乘为 4.79 亿)。这对我来说似乎没有效率, 呃,没有。一个字母是否存在 - 将每个字母视为具有两种可能状态(存在/不存在)的开关。请记住,移除了 'ck' 的 'stack' 与移除了 'kc' 是一样的。实现这一点的一种方法是在深入时仅删除您删除的最后一个字符右侧的字符。 没错,我昨晚很困惑。它只有 2^n 个排列【参考方案3】:

按如下方式构建您的字典:

 For each word W in the English language (or whatever word set you have)

     Sort the characters in W by alphabetical order (e.g. "apple" -> "aelpp") into a new string called W'

     Compute Hash H into W' using any fast hash algorithm (e.g CRC32.  You could likely invent anything yourself that has a low number of collisions)

     Store W and H as an element in the dictionary array
     That is:
        Word.original = W;
        Word.hash = Hash(W');
        Dictionary.append(Word);

  Sort the dictionary by hash values.

现在要查找所有字谜或搜索词 S

  Sort the characters in S by alphabetical order (e.g. "apple" -> "aelpp") into a new string called S'

  Compute Hash H of S' using the same fast hash algorithm above

  Now do a binary search on the dictionary for H.  The binary search should return an index F into Dictionary

  If the binary search fails to return an index into the Dictionary array, exit and return nothing

  I = F

  // Scan forward in the dictionary array looking for matches
  // a matching hash value is not a guarantee of an anagram match
  while (I < Dictionary.size) && (Dictionary[I].hash == H)
       if (IsAnagram(Dictonary[I], S)
           ResultSet.append(Dictionary[I].original)

  // Scan backwards in the dictionary array looking for matches
  I = F-1;
  while (I >= 0) && (Dictionary[I].hash == H)
       if (IsAnagram(Dictonary[I], S)
           ResultSet.append(Dictionary[I].original)


  return ResultSet     

现在我没有介绍如何处理“子字符串”搜索(搜索长度小于搜索词的字谜词。如果这是一个要求,我有点困惑。您的说明暗示该结果集字谜应该与搜索词具有完全相同的字符集。但是您可能可以枚举搜索字符串的所有子字符串,并通过上述搜索算法运行每个子字符串。

【讨论】:

我正在寻找所有子字符串。我认为问题的前 2 部分和示例解决了这个问题,但我可以回过头来更清楚地说明。【参考方案4】:

这只是一个想法,但也许这正是您要寻找的。您只有一个可以迭代的结构,所有大小的单词都在其中。在每个迭代步骤中,您都会多引入一个字母,并将搜索范围缩小到没有比已经引入的字母“更大”的字母。例如,如果你引入 M,你就不能再引入 N-Z 范围内的任何东西。

该结构可以是一个二叉树,其中一个字母的引入会进一步引导您进入几个树级别。每个节点都有一个字母,分支到其余的小字母,分支到其余的大字母,一个分支到下一个缩小搜索的根,以及一个指向完全用字母构建的单词列表的指针介绍到此为止。如果该搜索子空间中没有可能的单词,则分支可能为空,但您不能同时为 3 个分支设置空值,同时为指向单词列表的指针设置空值。 (你可以,作为一种优化,现在无关紧要)。除了指向单词列表的指针之外,您还可以使用一个标志来表示存在具有给定字母的单词,但这些单词可以存储在其他字典中。

假设我们有字母 ACKST。从结构的根开始,您在循环中搜索所有这些字母,但在 K 之后,您可能只能继续搜索 A 和 C(因为 S 和 T 在 K 之上)。因为我们对最大的单词最感兴趣,所以我们应该从最大的字母(在本例中为 T)开始搜索,然后继续搜索下一个最大的字母。对于 CAT 这个词,我们只能按特定顺序搜索字母 T、C、A。一旦我们到达那个 A,就会有一个指向以下单词列表的指针:ACT、CAT。

【讨论】:

【参考方案5】:

O(N) 时间和 O(1) 解决方案来检查 2 个字符串是否是字谜

bool Anagram( const  char *s1, const char *s2)

    unsigned int sum=0;

    if ( s1 == NULL || s2 == NULL)
        return false;

    while ( *s1 != '\0' && s2 != '\0')
    
                   sum ^= *s1;
                   sum ^= *s2;
                   s1++;
                   s2++;
    

    if ( s1 != '\0' || s2 != '\0')
        return false;

    if (sum) return false;

    return true;

如果你对两个相等的数进行异或运算..你的结果是 0。(因此算法)

【讨论】:

以上是关于我怎样才能加快这个 Anagram 算法的主要内容,如果未能解决你的问题,请参考以下文章

我怎样才能加快这个迭代?

SQLite3-我怎样才能加快这个 SELECT 查询?

我怎样才能加快这个汇总报价行行负载的视图?

我的子查询将执行时间增加了 20 秒。我怎样才能加快速度?

双连接查询需要 540 秒才能运行 - 我怎样才能加快速度?

java 读取大文件时怎么样才能加快速度?