java字符串排列组合查找

Posted

技术标签:

【中文标题】java字符串排列组合查找【英文标题】:java string permutations and combinations lookup 【发布时间】:2012-02-26 15:17:07 【问题描述】:

我正在编写一个 android 字应用程序。我的代码包含一个方法,该方法可以查找字符串的所有组合以及长度至少为 3 的 7 个字母字符串的子字符串。然后将所有可用组合与字典中的每个单词进行比较以查找所有有效单词。我正在使用递归方法。这是代码。

// 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);
    


【问题讨论】:

【参考方案1】:

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

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

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

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

在您实现这一点的方式上存在一些时间/空间权衡。在最简单(也是最快)的方法中,每个 Node 仅包含单词列表和子数组 Node[26]。这使您可以通过查看children[s.charAt(i)-'a'](其中s 是您的搜索字符串,i 是您当前在 trie 中的深度)来在恒定时间内找到您要寻找的孩子。

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

有了trie,你的整个排列函数就被一个查找替换了,把复杂度从O(N!log D)(其中D是字典的大小,N 字符串的大小)到 O(N log N)(因为您需要对字符进行排序;查找本身是 O (N))。

编辑:我已经汇总了这个结构的(未经测试的)实现:http://pastebin.com/Qfu93E80

【讨论】:

我得到了 239500800 个排列(不是 479001600)。你认为这两个 ts 是不同的吗?如果这个词是“aaa”,那么我认为只有 1 个排列,而不是 6 个。但除此之外,这是一个很好的答案,我 +1。 @Roger 是的,“aaa”只有一个不同的排列,但是询问者的代码不会查找重复项,因此它仍然会生成 6 个副本并为每个副本进行查找。 我花了一段时间才理解这个数据结构,但我终于明白了(我想)。所以这种树的节点将有 26 个分支。字母表的每个字符一个。每个节点也将有一个列表,其中包含该节点的相应单词。这是正确的吗? 听起来不错。我在答案中添加了几段关于实现细节的段落。 @NickBarnes 因此,如果您在上面的示例中有字符串“abcd”,那么您将如何找到“cab”、“bad”和“dab”的排列,因为树在“b”处分叉? 【参考方案2】:

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

答案中代码背后的思路如下:

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

【讨论】:

【参考方案3】:
  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;
  

【讨论】:

【参考方案4】:

我认为没有必要添加所有排列。您可以简单地将字符串封装成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 是一个字符串,但如果两个 PermutationStrings 具有相同的字符频率,则它们是相等的。因此new PermutationString("bad").equals(new PermutationString("dab"))。这也适用于.hashCode():如果字符串是彼此的排列,它们将生成相同的.hashCode()

现在您可以简单地HashMap&lt;PermutationString,ArrayList&lt;String&gt;&gt; 如下:

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作为key,如果key已经存在(这意味着已经有一个具有相同字符频率的单词),我们只需在其中添加我们自己的单词。否则,我们添加一个新的ArrayList&lt;String&gt; 与单个单词。

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

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

这将返回一个ArrayList&lt;String&gt;"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());

【讨论】:

【参考方案5】:

使用Trie

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

【讨论】:

【参考方案6】:

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

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

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

【讨论】:

查询不需要是 O(D)。您正在搜索特定的 letters[] 数组;如果您根据这些数组对字典进行排序,您可以在 O(logD) 中找到您要查找的字典。这几乎就是我上面的解决方案正在做的事情。

以上是关于java字符串排列组合查找的主要内容,如果未能解决你的问题,请参考以下文章

含有重复字符的字符串排列组合

获取字符串或组合的所有可能排列,包括 Java 中的重复字符

从给定字符串中查找长度为 k 的所有排列/组合

将两个数组 排列组合到一个数组集合 求java 代码

问个关于JAVA排列组合代码的问题

怎么用java实现输出a,b,c,d,e五个字符的所有可能的排列