更快地读取ArrayList

3

我正在编写一个用于解决填字游戏的求解器,它读取词典文件并根据模式返回符合该模式的所有单词列表。我已经实现了功能,但我需要让它更快地工作。我创建了一个HashMap,其中单词长度是键,单词列表是值。是否有一些方法可以更快地遍历ArrayList,或者有更好的数据结构可以使用?

import java.util.*;

public class CWSolution {

    //create the data structure that will store the dictionary
    private HashMap<Integer,ArrayList<String>> db;

    public CWSolution(List<String> allWords)
    {

    //construct the background structure

        //Create hashmap
        db = new HashMap<Integer,ArrayList<String>>();

        //go through each word
        for(String item : allWords ){
            //if the db does not contain a listing for this word length, create one
            if(!db.containsKey(item.length())){
                ArrayList<String> temp = new ArrayList<String>();
                temp.add(item);
                db.put(item.length(), temp);
            }
            //if it does contain a listing for this word length, add this word to it
            else{
                ArrayList<String> temp = db.get(item.length());
                temp.add(item);
                db.put(item.length(), temp);
            }
        }
    }

    public List<String> solutions(String pattern, int maxRequired)
    {

        //actually look for each pattern

        //create the structures we need
        List<String> answer = new ArrayList<String>();

        //get the relevant array list
        ArrayList<String> temp = db.get(pattern.length());

        //go through the array list word by word
        for(String item : temp ){
            //see if the pattern matches the word, if it does add it to the list, otherwise skip it
            if(matchPattern(pattern, item)){
                answer.add(item);
            }
            //if we reach the required size return it, otherwise keep going
            if(answer.size() == maxRequired){
                return answer;
            }
        }
        return answer;
    }

    private boolean matchPattern(String pattern, String word){
        //TODO implement this function
        //check the word against the pattern
        char star = "*".charAt(0);
        for(int i=0;i<pattern.length();i++){
            if(pattern.charAt(i) != star){
                if(pattern.charAt(i) != word.charAt(i)){
                    return false;
                }
            }
        }
        return true;
    }
}
编辑: 添加更多信息以使其更清晰。
一些评论在辩论这个问题,所以我想澄清一下:我是一位数据结构课程的学生,我只知道一些基本的数据结构,但我们即将结束本学期,所以我有一个基本的思路。
此外,我不太关心优化CWSolution()方法,而是关心优化solutions()方法。速度测试如下,我真正关心的是Time2。找到匹配单词所需的时间而不是创建结构所需的时间。
import java.util.Date;
import java.util.List;


public class CWSpeedTest {

    public static void main(String[] args){
    try{
        FileParser fp = new FileParser("TWL06.txt");
        List<String> solutions = null;
        //Change this to change the pattern
        String pattern = "*S**"; 
        //Change this to change the max solutions
        int maxSolns = 2000;

        List<String> dict = fp.getAllWords();

        Date d1 = new Date();
        CWSolution c = new CWSolution(dict);
        Date d2 = new Date();
        for (int i = 0; i < 1000; i++)
        solutions = c.solutions(pattern,maxSolns);
        Date d3 = new Date();
        System.out.println("Time 1: " + (d2.getTime() - d1.getTime()));
        System.out.println("Time 2: " + (d3.getTime() - d2.getTime()));
        System.out.println("For the pattern: " + pattern);
        System.out.println("With max solutions: " + maxSolns);
        System.out.println(solutions);

    }catch (Exception e){
        e.printStackTrace();
    }
    }
}

1
从我的经验来看,您可能需要的数据结构在通过长度筛选后是一个类似于 Map<Integer, Map<Character, Set<String>>> 的东西。其中的 Set 是所有包含给定 CharacterInteger 指定位置的单词。然后您可以遍历模式中的字符,从该映射中获取所有指定字符的 Set 并逐个对这些集合进行交集运算。 - millimoose
1
我可能会在这个 Map<Integer, Map<Integer, Map<Character, Set<String>>>> 之后使用一个薄包装器,因为这么多的尖括号让人感到痛苦。你真正需要的是一个多维矩阵。或者,你可以为映射键创建一个单独的类 DictionaryKey,其中包含字段 lengthpositioncharacter。让你的 IDE 使用这些字段生成一个 hashCode(),然后使用 Map<DictionaryKey, Set<String>> 使代码更易读。这样,你可以通过使 DictionaryKey 不可变并记忆哈希码来微调优化一下。 - millimoose
1
@BorisBrodski 这就是我的意思。假设 length -> position -> character -> words 的映射被称为 db。你会将单词 "puppy" 存储在长度为 5 的位置,并且使用 position, character0, 'p'1, 'u'2, 'p' 等存储。 (每个单词都将由单词中的每个字符进行索引,而不仅仅是起始单词。这正是键为 position, character 的原因。)然后,如果你正在寻找与 **ppy 匹配的单词,你将得到集合 db[2]['p']db[3]['p']db[4]['y']。我可以编写代码,但我现在很忙,如果这是作业,那么这将是一个很好的练习。;) - millimoose
1
至于为什么我的方法是一个重大的改进。如果我正确理解了OP的代码,它会迭代total_words/10个单词。(假设大多数单词的长度在3到13之间。实际上确切的长度分布并不重要,因为我的算法也通过单词长度进行过滤。)然后对这些单词中的每一个执行word_length个字符比较。如果字典很大,这可能是一个很大的数字。 - millimoose
1
总结一下,如果N是字典中单词的数量,m是当前单词的长度,则OP的方法为O((N * m) / 10)。我的方法为O(N / 260),并且有一些变化。考虑到m可能很小,它们属于相同的复杂度类,但我的方法至少快一个数量级。 - millimoose
显示剩余9条评论
2个回答

3

这里是使用所有位置和字符的索引来进行完整算法重写。首先,该算法查找包含指定字符在指定位置的最短单词列表,并计算与模式中每个非星号字符对应的其他单词列表的交集。

import java.util.*;

public class CWSolution {

    class FixLengthDB {
        // Index -> Letter -> All word with the Letter at Index
        HashMap<Integer, HashMap<Character, Set<String>>> indexLetterDb = new HashMap<>();

        public void storeWord(String word) {
            int l = word.length();
            for (int i = 0; i < l; i++) {
                HashMap<Character, Set<String>> letterDb = indexLetterDb.get(i);
                if (letterDb == null) {
                    letterDb = new HashMap<>();
                    indexLetterDb.put(i, letterDb);
                }

                Set<String> list = letterDb.get(word.charAt(i));
                if (list == null) {
                    list = new HashSet<>();
                    letterDb.put(word.charAt(i), list);
                }

                list.add(word);
            }
        }

        public Set<String> getList(int i, char c) {
            HashMap<Character, Set<String>> letterDb = indexLetterDb.get(i);
            if (letterDb == null) {
                return null;
            }
            return letterDb.get(c);
        }
    }

    //create the data structure that will store the dictionary
    private HashMap<Integer,FixLengthDB> db = new HashMap<>();
    private List<String> allWords;

    public CWSolution(List<String> allWords)
    {

        //construct the background structure

        this.allWords = allWords;
        //go through each word
        for(String item : allWords) {
            FixLengthDB fixLengthDB = db.get(item.length());

            if (fixLengthDB == null) {
                fixLengthDB = new FixLengthDB();
                db.put(item.length(), fixLengthDB);
            }

            fixLengthDB.storeWord(item);
        }
    }

    public List<String> solutions(String pattern, int maxRequired)
    {

        FixLengthDB fixLengthDB = db.get(pattern.length());
        if (fixLengthDB == null) {
            return new ArrayList<>();
        }

        Set<String> shortList = null;
        int shortListIndex = 0;
        int l = pattern.length();
        for (int i = 0; i < l; i++) {
            if (pattern.charAt(i) == '*') {
                continue;
            }
            Set<String> set = fixLengthDB.getList(i, pattern.charAt(i));
            if (set == null) {
                return new ArrayList<>();
            }

            if (shortList == null || shortList.size() > set.size()) {
                shortList = set;
                shortListIndex = i;
            }
        }

        if (shortList == null) {
            return allWords;
        }

        HashSet<String> result = new HashSet<>(shortList);
        for (int i = 0; i < l; i++) {
            if (i == shortListIndex || pattern.charAt(i) == '*') {
                continue;
            }
            Set<String> set = fixLengthDB.getList(i, pattern.charAt(i));
            result.retainAll(set);
        }

            // TODO truncate result list according to 'maxRequired' parameter
    return new ArrayList<>(result);
    }
}

说明: 该算法分为两步进行

  • 构建索引(在构造函数中)
  • 使用索引查找匹配的单词(solutions(...)

构建索引: 索引维护每个单词长度/字符索引/字符的字符串集。

下面是如何将单词添加到索引中:

Add word: fun
          |||
          ||\--- (length: 3, position 3, character 'n') -> set{"fun"})
          |\---- (length: 3, position 2, character 'u') -> set{"fun"})
          \----- (length: 3, position 1, character 'f') -> set{"fun"})

Add word: run
          |||
          ||\--- (length: 3, position 3, character 'n') -> set{"fun, run"})
          |\---- (length: 3, position 2, character 'u') -> set{"fun, run"})
          \----- (length: 3, position 1, character 'r') -> set{"run"})

Add word: raw
          |||
          ||\--- (length: 3, position 3, character 'w') -> set{"raw"})
          |\---- (length: 3, position 2, character 'a') -> set{"raw"})
          \----- (length: 3, position 1, character 'r') -> set{"run, raw"})

Add word: rar
          |||
          ||\--- (length: 3, position 3, character 'r') -> set{"rar"})
          |\---- (length: 3, position 2, character 'a') -> set{"raw, rar"})
          \----- (length: 3, position 1, character 'r') -> set{"run, raw, rar"})

添加四个单词(fun, run, raw, rar)后的数据库如下:
(length: 3, position 1, character 'f') -> set{"fun"})
(length: 3, position 1, character 'r') -> set{"run, raw, rar"})
(length: 3, position 2, character 'u') -> set{"fun, run"})
(length: 3, position 2, character 'a') -> set{"raw, rar"})
(length: 3, position 3, character 'w') -> set{"raw"})
(length: 3, position 3, character 'r') -> set{"rar"})
(length: 3, position 3, character 'n') -> set{"fun, run"})

使用索引:如何匹配模式ru*

首先让我们在索引中找到最小匹配集合。我们只有2个非星号字符,所以我们只检查两个集合。

1: (length: 3, position 1, character 'r') -> set{"run, raw, rar"})
2: (length: 3, position 2, character 'u') -> set{"fun, run"})

最小的集合是 #2 {"fun, run"}。现在我们遍历所有其他匹配的集合(在我们的例子中是集合 #1),并计算它们的交集:
{"fun, run"} cross {"run, raw, rar"} => {"run"}

结果是 {"run"}


你能否解释一下你在solutions()方法中实现的算法?我有些难以理解它所做的一切。 - Lesha
根据要求添加了算法解释。 - Boris Brodski

2
如果你想优化查找速度,最理想的解决方案是将你所知道的所有内容(关于模式的一切),并仅提供与之匹配的一组单词。现实情况是,你会使用你所知道的一些信息来缩小到一个可接受大小的集合。
在原始代码中,你只使用了一个已知项(即长度)作为键。Millimoose的评论提供了正确的答案:创建一个更具有区分性的键。例如,假设你有一个两字段键:(长度,包含字符)...即1A,1B,1C,... 1Z,2A...如果每个指向一个集合,那么每个集合都会更小。然后,你可以使用长度和模式中的任何字母来获取该集合。
更进一步,你可以像Millimoose所说的那样,使用三字段键(长度、位置、字符)。这样,你可以使用模式中的任何字母,并使用这三个属性将其缩小到更小的列表中。[正如Millimoose指出的那样,减慢速度的是字符串比较。]
理论上,你可以走到底,对于每个可能的模式都有一个集合。例如,单词“man”适合模式“m **”,“ma *”,“m * n”,“* a *”,“ma *”,“* an”,“** n”,“m * n”和“* an”。每个模式都可以是指向一个映射的键,该映射指向包含单词“man”的列表(值)。例如,“m **”将指向一个包含“man”,“mob”,“map”,“mid”等的列表。
在这样做时,你可能会在构造数据结构时花费太多时间。此外,你可能没有足够的空间来保存该数据结构。这些都是权衡。
总之,从你当前的键开始考虑添加更多的信息到键中,同时权衡这样做的成本。

为每个可能的模式创建一个集合将会浪费很多内存。这种方法与我的方法等效 - 从 "m**" 到单词列表的映射和从 (0, 'm') 到相同列表的映射之间没有区别。前者将使用 2 ^ total_characters_in_dictionary 个索引条目。而后者只使用 total_characters_in_dictionary 个。 - millimoose
当然,你的方法是完美的。 - Darius X.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接