java字符串排列和组合查找

时间:2012-02-04 03:47:01

标签: java algorithm permutation combinations

我正在写一个 Android 字应用。我的代码包含一个方法,可以找到字符串的所有组合和7字母字符串的子串,最小长度为3.然后将所有可用组合与字典中的每个单词进行比较,以找到所有有效单词。我正在使用递归方法。这是代码。

// Gets all the permutations of a string.
void permuteString(String beginningString, String endingString) {
    if (endingString.length() <= 1){
        if((Arrays.binarySearch(mDictionary, beginningString.toLowerCase() +   endingString.toLowerCase())) >= 0){
            mWordSet.add(beginningString + endingString);
        }
    }
    else
        for (int i = 0; i < endingString.length(); i++) {
            String newString = endingString.substring(0, i) + endingString.substring(i + 1);
            permuteString(beginningString + endingString.charAt(i), newString);
      }
}
// Get the combinations of the sub-strings. Minimum 3 letter combinations
void subStrings(String s){
    String newString = "";
    if(s.length() > 3){
        for(int x = 0; x < s.length(); x++){
            newString = removeCharAt(x, s);
            permuteString("", newString);
            subStrings(newString);
        }
    }
}

上面的代码运行正常,但是当我在Nexus上安装它时,我发现它运行得有点太慢了。完成需要几秒钟。大约3或4秒是不可接受的。 现在我在手机上玩了一些文字游戏,他们立即计算了一个字符串的所有组合,这让我相信我的算法效率不高而且可以改进。有人可以帮忙吗?


public class TrieNode {
TrieNode a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z;
TrieNode[] children = {a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z};
private ArrayList<String> words = new ArrayList<String>();

public void addWord(String word){
    words.add(word);
}
public ArrayList<String> getWords(){
    return words;
}
}

public class Trie {

static String myWord;
static String myLetters = "afinnrty";
static char[] myChars;
static Sort sort;
static TrieNode myNode = new TrieNode();
static TrieNode currentNode;
static int y = 0;
static ArrayList<String> availableWords = new ArrayList<String>();

public static void main(String[] args) {

    readWords();
    getPermutations();
}
public static void getPermutations(){
    currentNode = myNode;
    for(int x = 0; x < myLetters.length(); x++){
        if(currentNode.children[myLetters.charAt(x) - 'a'] != null){
            //availableWords.addAll(currentNode.getWords());
            currentNode = currentNode.children[myLetters.charAt(x) - 'a'];
            System.out.println(currentNode.getWords() + "" + myLetters.charAt(x));
        }
    }
    //System.out.println(availableWords);
}
public static void readWords(){
    try {
        BufferedReader in = new BufferedReader(new FileReader("c://scrabbledictionary.txt"));
        String str;
        while ((str = in.readLine()) != null) {
            myWord = str;
            myChars = str.toCharArray();
            sort = new Sort(myChars);
            insert(myNode, myChars, 0);
        }
        in.close();
    } catch (IOException e) {
    }
}
public static void insert(TrieNode node, char[] myChars, int x){    
    if(x >= myChars.length){
        node.addWord(myWord);
        //System.out.println(node.getWords()+""+y);
        y++;
        return;
    }
    if(node.children[myChars[x]-'a'] == null){
        insert(node.children[myChars[x]-'a'] = new TrieNode(), myChars, x=x+1);
    }else{
        insert(node.children[myChars[x]-'a'], myChars, x=x+1);
    }
}
}

6 个答案:

答案 0 :(得分:16)

在您当前的方法中,您正在查找每个子字符串的每个排列。因此,对于"abc",您需要查找"abc""acb""bac""bca""cab""cba"。如果你想找到“排列”的所有排列,你的查找次数几乎是 500,000,000 ,那就是在你查看其子串之前。但是我们可以通过预处理字典将其减少到 一个 查找,无论长度如何。

这个想法是将字典中的每个单词放入一些数据结构中,其中每个元素包含一组字符,以及包含(仅)这些字符的所有单词的列表。例如,您可以构建一个二叉树,其中包含一个包含(已排序)字符集"abd"和单词列表["bad", "dab"]的节点。现在,如果我们要查找"dba"的所有排列,我们会将其排序为"abd"并在树中查找以检索列表。

正如鲍曼所指出的,tries非常适合存储这类数据。 trie的美妙之处在于查找时间仅取决于搜索字符串的长度 - 它独立于字典的大小。由于你将存储相当多的单词,并且你的大多数搜索字符串都很小(大多数将是你递归的最低级别的3个字符的子字符串),这个结构是理想的。

在这种情况下,trie中的路径将反映字符集而不是单词本身。因此,如果您的整个字典都是["bad", "dab", "cab", "cable"],那么您的查找结构最终会如下所示:

Example trie

您实施此方法的方式有一些时间/空间权衡。在最简单(最快)的方法中,每个Node只包含单词列表和一个子类Node[26]。这样,您只需查看children[s.charAt(i)-'a'](其中s是您的搜索字符串,而i是您当前在线索中的深度),就可以定位您所关注的孩子。

缺点是大多数children数组大部分都是空的。如果空间是个问题,你可以使用更紧凑的表示,如链表,动态数组,哈希表等。但是,这些代价可能需要在每个节点上进行多次内存访问和比较,而不是简单的数组访问上面。但是如果浪费的空间超过整个字典的几兆,我会感到惊讶,所以基于阵列的方法可能是你最好的选择。

随着trie的到位,你的整个排列函数被一次查找替换,从 O(N!log D)(其中 D )的复杂性降低字典的大小, N 字符串的大小)到 O(N log N)(因为你需要对字符进行排序;查找本身是 O (N))。

编辑:我把这个结构的(未经测试的)实现放在一起:http://pastebin.com/Qfu93E80

答案 1 :(得分:1)

见这里:How to find list of possible words from a letter matrix [Boggle Solver]

答案中代码背后的想法如下:

  • 遍历每个单词词典。
  • 迭代单词中的每个字母,将其添加到字符串中,每次都将字符串添加到前缀数组中。
  • 创建字符串组合时,请在进一步分支之前测试它们是否存在于前缀数组中。

答案 2 :(得分:1)

  static List<String> permutations(String a) {
    List<String> result=new LinkedList<String>();
    int len = a.length();
    if (len<=1){
      result.add(a);
    }else{
      for (int i=0;i<len; i++){
        for (String it:permutations(a.substring(0, i)+a.substring(i+1))){
          result.add(a.charAt(i)+it);
        }
      }
    }
    return result;
  }

答案 3 :(得分:1)

我不认为添加所有排列是必要的。您可以简单地将字符串封装到PermutationString

public class PermutationString {

    private final String innerString;

    public PermutationString(String innerString) {
        this.innerString = innerString;
    }

    @Override
    public int hashCode() {
        int hash = 0x00;
        String s1 = this.innerString;
        for(int i = 0; i < s1.length(); i++) {
            hash += s1.charAt(i);
        }
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final PermutationString other = (PermutationString) obj;
        int nChars = 26;
        int[] chars = new int[nChars];
        String s1 = this.innerString;
        String s2 = other.innerString;
        if(s1.length() != s2.length()) {
            return false;
        }
        for(int i = 0; i < s1.length(); i++) {
            chars[s1.charAt(i)-'a']++;
        }
        for(int i = 0; i < s2.length(); i++) {
            chars[s2.charAt(i)-'a']--;
        }
        for(int i = 0; i < nChars; i++) {
            if(chars[i] != 0x00) {
                return false;
            }
        }
        return true;
    }

}

PermutationString是一个字符串,但如果两个PermutationString具有相同的字符频率,则它们相等。因此new PermutationString("bad").equals(new PermutationString("dab"))。这也适用于.hashCode():如果字符串是彼此的排列,它们将生成相同的.hashCode()

现在您只需HashMap<PermutationString,ArrayList<String>>,如下所示:

HashMap<PermutationString,ArrayList<String>> hm = new HashMap<PermutationString,ArrayList<String>>();
String[] dictionary = new String[] {"foo","bar","oof"};
ArrayList<String> items;
for(String s : dictionary) {
    PermutationString ps = new PermutationString(s);
    if(hm.containsKey(ps)) {
        items = hm.get(ps);
        items.add(s);
    } else {
        items = new ArrayList<String>();
        items.add(s);
        hm.put(ps,items);
    }
}

现在我们迭代字典中所有可能的单词,构建一个PermutationString作为,如果已经存在(这意味着那里)已经是一个具有相同字符频率的单词),我们只需添加自己的单词即可。否则,我们会添加一个包含单个单词的新ArrayList<String>

现在我们已经填充了hm所有排列(但没有那么多),您可以查询:

hm.get(new PermutationString("ofo"));

这将返回ArrayList<String> "foo""oof"

<强>测试用例

HashMap<PermutationString, ArrayList<String>> hm = new HashMap<PermutationString, ArrayList<String>>();
String[] dictionary = new String[]{"foo", "bar", "oof"};
ArrayList<String> items;
for (String s : dictionary) {
    PermutationString ps = new PermutationString(s);
    if (hm.containsKey(ps)) {
        items = hm.get(ps);
        items.add(s);
    } else {
        items = new ArrayList<String>();
        items.add(s);
        hm.put(ps, items);
    }
}
Assert.assertNull(hm.get(new PermutationString("baa")));
Assert.assertNull(hm.get(new PermutationString("brr")));
Assert.assertNotNull(hm.get(new PermutationString("bar")));
Assert.assertEquals(1,hm.get(new PermutationString("bar")).size());
Assert.assertNotNull(hm.get(new PermutationString("rab")));
Assert.assertEquals(1,hm.get(new PermutationString("rab")).size());
Assert.assertNotNull(hm.get(new PermutationString("foo")));
Assert.assertEquals(2,hm.get(new PermutationString("foo")).size());
Assert.assertNotNull(hm.get(new PermutationString("ofo")));
Assert.assertEquals(2,hm.get(new PermutationString("ofo")).size());
Assert.assertNotNull(hm.get(new PermutationString("oof")));
Assert.assertEquals(2,hm.get(new PermutationString("oof")).size());

答案 4 :(得分:0)

使用Trie

而不是测试所有N!可能性,您只需遵循导致结果的前缀树。这将显着减少您要检查的字符串数量。

答案 5 :(得分:0)

好吧,您可以使用数组letters[]扩展您的字典实体,其中letters[i]停留的时间是该单词中使用的第i个字母。这需要一些额外的记忆,不会比现在使用的多。

然后,对于要检查的排列的每个单词,您还需要计算不同字母的数量,然后通过简单的比较程序遍历分类。如果对于字典中所有字母的所有字母数量少于或等于字词我们正在检查 - 是的,这个字可以表示为子字符串的排列,否则 - 否。

复杂性:预先计算需要O(D * maxLen),每个查询需要O(max(N,D))。