【问题标题】:Java Anagram running out of memoryJava Anagram 内存不足
【发布时间】:2017-07-19 17:42:35
【问题描述】:

我正在尝试解决古老的字谜问题。感谢那里的许多教程,我能够遍历一组字符串,递归地找到所有排列,然后将它们与英语单词列表进行比较。我发现的问题是,在大约三个词之后(通常是“变形”之类的词),我得到了 OutOfMemory 错误。我尝试将我的批次分成小组,因为它似乎是消耗我所有内存的递归部分。但即使只是“变形”也将其锁定...

在这里,我将文件中的单词读入列表

Scanner scanner = new Scanner(resource.getInputStream());
   while (scanner.hasNext()) {
       String s = scanner.nextLine();
        uniqueWords.add(s.toLowerCase());
   }

现在我将它们分成更小的集合并调用一个类来生成字谜:

List<List<String>> subSets = Lists.partition(new ArrayList(uniqueWords), SET_SIZE);

for (List<String> set: subSets) {
      // tried created as class attribute & injection, no difference 
      AnagramGenerator anagramGenerator = new AnagramGenerator();
      List<Word> anagrams = anagramGenerator.createWordList(set);
      wordsRepository.save(anagrams);
      LOGGER.info("Inserted {} records into the database", anagrams.size());
 }

最后是我的生成器:

public class AnagramGenerator {

private Map<String, List<String>> map = new Hashtable<>();
public List<Word> createWordList(List<String> dictionary) {

   buildAnagrams(dictionary);

   List<Word> words = new ArrayList<>();
   for (Map.Entry<String, List<String>> entry : map.entrySet()) {
       words.add(new Word(entry.getKey(), entry.getValue()));
   }
    return words;
   }

private Map<String, List<String>> buildAnagrams(List<String> dictionary) {

        for (String str : dictionary) {
            String key = sortString(str);
            if (map.get(key) != null) {
                map.get(key).add(str.toLowerCase());
            } else {
                if (str.length() < 2) {
                    map.put(key, new ArrayList<>());
                } else {
                    Set<String> permutations = permutations(str);
                    Set<String> anagramList = new HashSet<>();

                    for (String temp : permutations) {
                        if (dictionary.contains(temp) && !temp.equalsIgnoreCase(str)) {
                            anagramList.add(temp);
                        }
                    }
                    map.put(key, new ArrayList<>(anagramList));
                }
            }
        }
        return map;
    }

   private Set<String> permutations(String str) {    
        if (str.isEmpty()) {
            return Collections.singleton(str);
        } else {
            Set<String> set = new HashSet<>();
            for (int i = 0; i < str.length(); i++)
                for (String s : permutations(str.substring(0, i) + str.substring(i + 1)))
                    set.add(str.charAt(i) + s);
            return set;
        }
    }

编辑: 根据出色的反馈,我已将生成器从排列更改为工作查找:

public class AnagramGenerator {
private Map<String, Set<String>> groupedByAnagram = new HashMap<String, Set<String>>();

    private Set<String> dictionary;

    public AnagramGenerator(Set<String> dictionary) {

        this.dictionary = dictionary;
    }

 public List<Word> searchAlphabetically() {

        List<Word> words = new ArrayList<>();
        for (String word : dictionary) {
            String key = sortString(word);
            if (!groupedByAnagram.containsKey(key)) {
                groupedByAnagram.put(key, new HashSet<>());
            }
            if (!word.equalsIgnoreCase(key)) {
                groupedByAnagram.get(key).add(word);
            }
        }

        for (Map.Entry<String, Set<String>> entry : groupedByAnagram.entrySet()) {
            words.add(new Word(entry.getKey(), new ArrayList(entry.getValue())));
        }

        return words;
    }
 private String sortString(String goodString) {

        char[] letters = goodString.toLowerCase().toCharArray();
        Arrays.sort(letters);
        return new String(letters);
    }

它有更多的调整,所以我没有添加一个单词,因为它是自己的字谜,但除此之外,这似乎非常快。而且,代码更干净。谢谢大家!

【问题讨论】:

  • 你在哪里得到错误?堆栈跟踪?
  • 你在那里创造了一大堆集合..
  • 使用递归查找排列需要大量开销,并且通常涉及为您的程序增加分配的堆空间。我建议使用另一种方式来创建所有排列。
  • 我同意我正在使用递归创建大量 Set,但到目前为止,这是进行字符串操作的唯一方法。任何替代的想法?可以像 char[] 交换器一样简单吗?
  • 注意如果你有重复的字母,你会得到更少的组合。例如add 有 3 个字谜,但 the 有 6 个。

标签: java anagram


【解决方案1】:

正如更长的单词所指出的那样,排列的数量很快就会变得巨大。

/usr/share/dict/british-english 在 Debian 上有 99,156 行。有更长的单词列表,但我们以它为例。

九个字母单词的排列数是 9! = 362,880

因此,对于 9 个或更多字母的单词,尝试字典中的每个单词比尝试输入单词的每个排列所花费的计算量更少。

10! milliseconds = ~1 hour
12! milliseconds = ~5.54 days
15! milliseconds = ~41.44 years

如果您每毫秒处理一个排列,您会很幸运,因此您会发现您很快就会遇到许多完全不切实际的排列。对堆栈和堆的影响以相同的速度增加。

所以,试试算法(伪代码):

 sorted_input = sort_alphabetically(input_word)
 for each dictionary_word // probably a file readline()
     sorted_dictionary_word = sort_alphabetically(dictionary_word)
     if(sorted_dictionary_word = sorted_input)
         it's an anagram! Handle it
     end 
 end

同样,您可以相当快速地将所有字典单词算法写入查找数据结构。再次伪代码;在 Java 中,您可以使用来自 Apache Commons 或 Guava 的 Map&lt;String, List&lt;String&gt;&gt;MultiMap

  multimap = new MultiMap<String, String> // or whatever

  def build_dict:
      for each dictionary_word // probably a file readline()
          multimap.add(
               sort_alphabetically(dictionary_word), 
               dictionary_word)
      end
  end

  def lookup_anagrams(word):
      return multimap.get(sort_alphabetically(word))
  end 

这占用了适量的内存(整个字典,加上一些键和映射开销),但这意味着一旦创建了结构,您就可以非常便宜地一遍又一遍地查询。

如果你想找到两个单词的字谜,你需要一个更复杂和有趣的算法。但即便如此,避免暴力破解整个排列搜索空间对您的成功至关重要。

【讨论】:

  • 对每个单词中的字母进行排序的好技巧!我认为这是最好的答案。
【解决方案2】:

快速计算:“anamorphosis”有 12 个字母,等于 12! = 479,001,600 个排列。每个字符串至少占用 12 个字节(假设 UTF-8 仅包含 ASCII 字符),这意味着总大小为 12 * 479,001,600 字节,大约为 6 GB。

现在,据我所知,默认堆大小设置为 1GB 或(如果更小)可用内存的四分之一。这小于所需的 6GB。

有两种方法:

  • 在执行程序时增加堆大小,但它不适用于更长的单词,因为排列呈指数增长:再多一个字母,“完成”就需要 78GB。

  • 通过排列进行流式传输,而不是将它们具体化为一组字符串。具体来说,这意味着仍然使用递归,但不是存储每个递归生成的排列,而是立即处理它,然后在继续下一个时忘记。

现在,如果需要针对整个字典执行此操作,如果您可以访问集群,另一种方法可能是计算字典自身的笛卡尔积,将其存储在 HDFS 等分布式文件系统中(应该数量级为十亿个条目),然后使用 MapReduce 并行遍历所有对,并输出彼此是字谜的对。这是更多的努力,但复杂性从单词长度的指数下降到字典大小的二次。

【讨论】:

  • 注意:大多数 12 个字符的字符串将使用 ~64 字节的内存。
  • 是的,你说得对,彼得,还有额外的开销。我对我的下限持乐观态度,因为这足以说明这一点。它绝对使 12 个字母的字谜的具体化超出了商用计算机的范围:stackoverflow.com/questions/31206851/…
  • 我的楼梯下有一台旧电脑,有 128 GB ;) 我期待升级它。
  • 我无权访问集群,这是严格在我的笔记本电脑上运行的。我正在考虑使用并行流来减少 Set 创建数量。但我担心这会占用多少内存。
  • @PeterLawrey 哇,我没有意识到 HFT 需要这么多内存 :-) 我希望通过升级,您将设法获得 13 个字母的单词!
【解决方案3】:

这是一个将 slim 的方法与我的“伪 Java 代码”相结合的答案:

Map<String, Set<String>> groupedByAnagram = new HashMap<String, Set<String>>();

for(String word: dictionary)
{
  String footprint = sort_alphabetically(word);
  if(!groupedByAnagram.contains(footprint))
  {
    groupedByAnagram.put(footprint, new HashSet<String>>());
  }
  groupedByAnagram.get(footprint).insert(word); 
}

for(Set<String> anagram: groupedByAnagram.values())
{
  if(anagram.size() > 1)
  {
    System.out.println("Anagram found.");
    for (String word: anagram)
    {
      System.out.println(word);
    }
  } 
}

它首先通过“字谜指纹”(slim的想法)建立所有单词的索引,然后遍历它,只输出超过一个单词的条目。

【讨论】:

  • 不知道该给谁回答。 Slim 给出了很棒的想法,Ghislain 给出了很棒的实现。我希望这是正确的投票方式。
  • 谢谢 sonoerin,我很高兴它成功了。如果您仍然可以更改,请不要犹豫,尽管他的回答值得称赞,因为我只想提供一个有用的总结。我会很好,甚至更愿意让他获得声望点,这对我来说只是“正确”的感觉。 :-)
  • 我把它交给了 Slim,但看起来你们俩都解决了问题。我确实有一个后续问题-上述解决方案会将原始单词添加为字谜-您知道我该如何防止吗?
  • 谢谢!上述建议的解决方案没有原词。它所做的是将字典中的所有单词分组到包中,每个包中包含的单词是彼此的字谜。然后它会一一打印袋子,但袋子内的顺序是任意的。如果你需要的是一个指定的词的请求,你可以取它的字谜指纹,在groupedByAnagram中查找对应的包,并输出该包中除原词之外的所有词。我希望它有所帮助。
猜你喜欢
  • 1970-01-01
  • 2013-10-23
  • 2012-06-21
  • 2013-09-10
  • 2012-12-12
  • 1970-01-01
  • 1970-01-01
  • 2014-08-02
  • 2010-12-21
相关资源
最近更新 更多