检查一个词是不是是另一个词的子字谜(Java)

Posted

技术标签:

【中文标题】检查一个词是不是是另一个词的子字谜(Java)【英文标题】:Checking if a word is a sub-Anagram of another (Java)检查一个词是否是另一个词的子字谜(Java) 【发布时间】:2017-12-02 20:14:08 【问题描述】:

“unsold”和“silo”这两个词是“insidously”这个词的子变位词。也就是说,它们只能使用“insidously”中的字母拼写。显然还有更多,这个概念是“澳大利亚人”报纸上的文字游戏的基础。

我正在尝试编写一个程序,它接受两个参数 - 一个单词,另一个可能是这个单词的子变位词,如果是则返回 true。到目前为止,这就是我所拥有的:

public boolean isAnswer(String word, String base)
    ArrayList<Character> characters = new ArrayList<>();
    for(char x : base.toCharArray)
    
        characters.add(x)
    
    for(char y : word.toCharArray)
    
        if(characters.contains(x))
        
            characters.remove(x)
        
        else
        
            return false;
        
    return true;
    

它确实有效,但如果我遍历英语词典中的每个单词,这将非常耗费记忆。如何在不创建 ArrayList 局部变量的情况下做到这一点?

【问题讨论】:

“它确实有效”不,这是不可能的,缺少 ; ( ) 它不能编译所以它不能工作 【参考方案1】:

如果您想让现有程序更好地考虑使用 SET 而不是 LIST,因为它会

消除角色集合中的重复添加,节省空间。 在下一个循环中为您节省一些迭代,从而节省时间。

编辑

但是,这种优化可能不适用于其中一个 cmets 指出的条件。

EX - 当base 只有“ab”而word 有“aab”时

【讨论】:

使用集合可能没有帮助,因为重复字符与此任务相关。例如,word 'aab' 仅适用于 base,当它包含 'aab' 而不是仅包含 'ab' 时。【参考方案2】:

我建议您使用 java.util.Set 以避免不必要的迭代。请在下面找到代码:

private static boolean isSubAnagram() 
        String str  = "insidiously";
        String anagram = "siloy";

        Set<Character> set = new HashSet<Character>();
        for(int i = 0 ; i < str.length() ; ++i)
            set.add(new Character(str.charAt(i)));
        

        int count = 0;
        for(int i = 0 ; i < anagram.length() ; ++i)
            if(set.contains(anagram.charAt(i)))
                ++count;
            
        

        return count == anagram.length();

    

如果基本字符串中的字母数和所谓的子字谜需要相同,则选择:

private static boolean isSubAnagram() 
    String str  = "insidiously";
    String anagram = "siloyl";

    List<Character> list = new ArrayList<Character>();
    for(int i = 0 ; i < str.length() ; ++i)
        list.add(new Character(str.charAt(i)));
                   

    for(int i = 0 ; i < anagram.length() ; ++i)
        char curChar = anagram.charAt(i);
        if(list.contains(curChar))
            list.remove(new Character(curChar));
            continue;
        else
            return false;
        
    

    return true;

【讨论】:

套装不合适。如果您的基本词使用一个字符两次或更多次,则应允许您在子字谜中使用该字符相同的次数。 我不知道文字游戏。但如果是这种情况,则不应使用案例集。我会编辑这个。谢谢! @Michael 您从哪里获得该信息,这些字母最多可以出现在基本单词中的确切次数? OP没有给出这样的约束。 @KrzysztofCichocki 因为我知道字谜是什么。 :)【参考方案3】:

一种优化可能是首先确保单词不长于基数。

public boolean isAnswer(String word, String base)

    if (word.length() > base.length()) return false;
    //...

我怀疑单词的长度是否完全相同,there may be a faster way than comparing all of the characters:

public boolean isAnswer(String word, String base)

    if (word.length() > base.length()) 
        return false;
    
    else if (word.length() == base.length()) 
        return isFullAnagram(); // I'll leave the implementation of this up to you
    
    //...

优化这一点的下一步是确保您不会天真地尝试字典中的每个单词:

// Don't do this
public static void main(String... args)

    String base = "something";
    for (final String word : dictionary)
    
        if (isAnswer(word, base)) // do something
    

// Don't do this

您有一个很大的优势,即任何有价值的字典文本文件都将被预先排序。一个基本的优化是将你的字典分成 26 个文件 - 一个用于以每个字母开头的单词 - 并跳过任何不可能匹配的文件。

public static void main(String... args)

    String base = "something";
    Set<Characters> characters = // populate with chars from base

    for (final Section section : dictionary)
    
        if (characters.contains(section.getChar())
        
            for (final String word : section)
            
                if (isAnswer(word, base)) // do something
            
        
    

接下来我要做的是查看并行化这个过程。一种基本方法是在自己的线程上运行每个部分(因此您最多可以查看大约 12 个线程来查找最常见的英语单词)。

public static void main(String... args)

    String base = "something";
    Set<Characters> characters = // populate with chars from base

    for (final Section section : dictionary)
    
        if (characters.contains(section.getChar())
        
            startMyThread(section, base);
        
    

你可以让线程返回一个Future,你可以在最后检查。我会把这个细节留给你。

像CUDA 这样的库允许您通过将计算推送到GPU 来使用非常高的并发性。您可以同时运行数百个线程。我不确定在这种情况下一个好的策略会是什么样子。


我正在假设您只需要处理罗马字母表中的 26 个字母。我在报纸上看到的每一个这样的游戏都避免使用带有变音符号的词:cafe、fiancée、naïve 等。

【讨论】:

【参考方案4】:

我相信这将是运行速度快且占用内存最少的解决方案:

public class Snippet 

public static void main(String[] args) 

    System.out.println(isAnswer("unsold", "insidiously"));
    System.out.println(isAnswer("silo", "insidiously"));
    System.out.println(isAnswer("silk", "insidiously"));


public static boolean isAnswer(String word, String base) 
    char[] baseCharArr = base.toCharArray();
    for (int wi = 0; wi < word.length(); wi++) 
        boolean contains = false;
        char wchar = word.charAt(wi);
        for (int bi = 0; bi < baseCharArr.length; bi++) 
            if (baseCharArr[bi]==wchar) 
                baseCharArr[bi]='_'; // to not use this letter anymore we delete it using some sign that is non valid to from a word.
                contains=true;
                break;
            
        
        if (!contains) 
            return false;
        
    
    return true;

【讨论】:

好吧,这很有趣,你刚刚使用了 baseCharArr[bi] = '_',而我使用了 characters.remove(x)。除了循环将遍历每个字符,即使它找到一个与基数不匹配的字符。所以如果我们纠正它,它可以变得更快。 看看 ar:if (!contains) return false; - 如果缺少任何必需的字母,它显然会返回 false【参考方案5】:

你的代码漏掉了很多,;,(),不能清晰的编译和工作^^,我改了“if”的顺序以及如何添加所有base

public boolean isAnswer(String word, String base) 
      ArrayList<Character> characters = new ArrayList<>();
      characters.addAll(Arrays.asList(base.toCharArray()));
      for (char y : word.toCharArray()) 
          if (!characters.contains(y)) 
              return false;
          
          characters.remove(y);
      
      return true;

【讨论】:

X 使用时也超出范围 @Michael 是的,实际上是 'y' ^^ 并且在 toCharArray 和其他更正处错过了 '()'【参考方案6】:

您可以直接替换base。这不是很有效,并且会创建很多 String 对象,但很容易阅读:

public boolean isAnswer(String word, String base)

  for (char ch : word.toCharArray())
  
    base = base.replaceFirst("" + ch, "");
  
  return base.trim().length() == 0;

【讨论】:

但是每次迭代都会产生新的String,这显然不是很有效的解决方案。 保存一些代码的好主意,但不适用于 sub-anagrams。 dofood 的子变位词,但该方法将返回false。还要注意字母多次出现的情况。 replace 替换所有匹配项。 你是对的。我不知道字符的 replace() 方法会替换所有出现的字符。修改上面的例子以使用字符串。 感谢 Florian,这是一个有趣的方法。 @Socow【参考方案7】:

当前方法的问题/其他答案

有很多答案,但没有一个是非常有效的。

对于子字谜候选中的每个字母,我们搜索列表并删除字母。一次搜索需要线性时间。由于我们必须对每个字母进行搜索,我们最终会得到二次时间复杂度。

有些人建议使用集合而不是列表。在集合中搜索需要恒定的时间,所以我们最终会得到线性时间。但是,当同一个字母多次出现时,set 方法会失败。

由于恒速因素,所提出的解决方案也很慢。当我们使用List&lt;Character&gt;Set&lt;Character&gt; 时,String 的chars 必须装箱在Character 对象中。创建和处理这些对象比使用基本的char 类型要慢得多。

解决方案

多组

我们可以使用multiset(也称为bag)来表示单词中的字母。对于每个单词,我们创建其字母的多重集,并检查该多重集是否是基本单词的字母多重集的子集。

示例

基本词"Food" 有多重集合f, o, o, d。 Word "do" 具有多重集 d, o。 Word "dod" 有多重集合d, d, o

d, of, o, o, d 的子集 ==> dofood 的子字谜。d, o, d 不是 f, o, o, d 的子集==> dod 不是food 的子字谜。

存储多集

由于我们知道只有字符'a''z' 出现,我们使用int 数组来表示一个多重集。 array[0]的值为'a's的个数; array[1] 的值为'b's 的数量,以此类推。 array[1]也可以写成array['b' - 'a']

示例

带有多重集f, o, o, d 的单词"Food" 由数组表示

// Entry for:     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
int[] multiSet = 0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0;

子集检查

ab 的子集当且仅当a[i] &lt;= b[i] 对于所有i

当我们在计算多重集a 时进行子集测试时,我们不必检查所有 26 个数组条目,而只需检查设置为大于零值的条目。

重用工作

我们要检查很多词中的一个基本词。我们可以将多重集重新用于基本词,而不必一遍又一遍地计算它。 我们不是编写返回 truefalse 的方法,而是编写一个返回给定基本词和给定字典(要检查的词列表)的所有子字谜列表的方法。

小优化

如果一个词比基本词长,它就不能是一个子字谜。在这种情况下,我们不必为那个词计算多重集。

实施

public static List<String> subAnagrams(String base, List<String> dictionary) 
    char[] usableChars = new char['z' - 'a'];
    base = base.toLowerCase();
    for (int i = 0; i < base.length(); ++i) 
        ++usableChars[base.charAt(i) - 'a'];
    

    List<String> subAnagrams = new ArrayList<>();
    for (String candidate : dictionary) 
        boolean isSubAnagram = candidate.length() <= base.length();
        candidate = candidate.toLowerCase();
        char[] usedChars = new char['z' - 'a'];
        for (int i = 0; isSubAnagram && i < candidate.length(); ++i) 
            int charIndex = candidate.charAt(i) - 'a';
            isSubAnagram = ++usedChars[charIndex] <= usableChars[charIndex];
        
        if (isSubAnagram) 
            subAnagrams.add(candidate);
        
    
    return subAnagrams;

示例用法

public static void main(String[] args) 
    List<String> dict = new ArrayList<>();
    dict.add("Do");
    dict.add("Odd");
    dict.add("Good");
    dict.add("World");
    dict.add("Foo");
    System.out.println(subAnagrams("Food", dict));  

打印[do, foo]

【讨论】:

以上是关于检查一个词是不是是另一个词的子字谜(Java)的主要内容,如果未能解决你的问题,请参考以下文章

查找给定单词的字谜

如何在不使用大型结果集的情况下检查一个字符串是不是是另一个字符串的子字符串?

我如何在 Python (2.72) 上打开一个文本文件中的行

有啥简单的方法可以判断单词列表是不是是彼此的字谜?

查找字符串中的所有字谜如何优化

需要帮助来理解字谜代码的乐趣