找到给出集合的最长的单词

时间:2013-06-01 04:07:39

标签: java algorithm data-structures

这是一个谷歌面试问题,我在网上找到了大多数使用HashMap或类似数据结构的答案。我想尽可能找到使用Trie的解决方案。有人可以给我一些提示吗?

以下是问题: 您将获得一个字典,其形式为每行包含一个单词的文件。例如,

abacus 
deltoid 
gaff 
giraffe 
microphone 
reef 
qar 

您还会收到一系列信件。例如,

{a, e, f, f, g, i, r, q}. 

任务是找到字典中可以拼写的最长单词 字母。例如,上面示例值的正确答案是“长颈鹿”。 (注意 “reef”不是一个可能的答案,因为这组字母只包含一个“e”。)

首选Java实现。

9 个答案:

答案 0 :(得分:12)

没有Java代码。你可以自己解决这个问题。

假设我们需要做很多次,这就是我要做的事情:

  • 我首先为字典中包含26位的每个单词创建“签名”,如果单词包含一个(或多个)字母实例,则设置位[letter]。这些签名可以编码为Java int

  • 然后创建一个映射,将签名映射到具有该签名的单词列表。

使用预先计算的地图进行搜索:

  • 为要查找字词的字母组创建签名。

  • 然后遍历映射的键,查找(key & (~signature) == 0)所在的键。这为您提供了一个“可能”的简短列表,其中不包含任何不在所需字母集中的字母。

  • 在短名单中搜索每个所需字母的正确数字的单词,记录最长的匹配。


注意:

  1. 虽然主要搜索字典中的字数大致为O(N),但测试非常便宜。

  2. 这种方法的优点是需要相对较小的内存数据结构,(很可能)具有良好的局部性。这可能有助于加快搜索速度。


  3. 以下是加快上述O(N)搜索步骤的想法。

    从上面的签名地图开始,为包含特定对字母的所有单词创建(预计算)衍生地图;对于AC,BC,......和YZ,一个用于包含AB的单词。然后,如果您正在寻找包含(比如说)P和Q的单词,您可以只扫描PQ衍生图。这将大约O(N)步骤减少大约26^2 ...以额外地图的更多内存为代价。

    可以扩展到3个或更多字母,但缺点是内存使用量激增。

    另一个可能的调整是(以某种方式)将首字母对的选择偏向于不经常发生的字母/对。但是这会增加一个前期开销,这可能比搜索较短列表所获得的(平均)节省更多。

答案 1 :(得分:3)

我怀疑基于Trie的实现不会非常节省空间,但它可以很好地并行化,因为你可以并行地下降到树的所有分支中并收集从每个顶部可以到达的最深节点分支给定的一组字母。最后,您只需收集所有最深的节点并选择最长的节点。

我从这个算法开始(抱歉,只是伪代码),它不会尝试并行化,而只是使用普通的旧递归(和回溯)来找到最长的匹配:

TrieNode visitNode( TrieNode n, LetterCollection c )
{
    TreeNode deepestNode = n;
    for each Letter l in c:
        TrieNode childNode = n.getChildFor( l );

        if childNode:
            TreeNode deepestSubNode = visitNode( childNode, c.without( l ) );
            if deepestSubNode.stringLength > deepestNode.stringLength:
                deepestNode = deepestSubNode;
   return deepestNode;
}

即。这个函数应该从trie的根节点开始,带有整个给定的字母集合。对于集合中的每个字母,您尝试查找子节点。如果有,则递归并从集合中删除该字母。有一次,你的信件收集将是空的(最好的情况下,所有信件都会消耗 - 你可以立即拯救,而不必继续穿越特里)或者没有其他任何剩余字母的孩子 - 在这种情况下你会删除节点本身,因为这是你的“最长匹配”。

如果您更改递归步骤以便并行访问所有子项,收集结果 - 并选择最长的结果并返回该结果,则可以很好地并行化。

答案 2 :(得分:3)

首先,好问题。面试官想看看你是如何解决这个问题的。在这些问题中,您需要分析问题并仔细选择数据结构。

在这种情况下,我想到了两个数据结构:HashMapsTriesHashMaps不适合,因为您没有要查找的完整密钥(您可以使用基于地图的反向索引,但您说您已经找到了这些解决方案)。您只有部分 - Trie最适合的部分。

因此,尝试的想法是,您可以在遍历树时忽略字典中不存在的字符分支。

在你的情况下,树看起来像这样(我省略了非分支路径的分支):

*
   a
     bacus
   d 
     deltoid
   g
     a
       gaff
     i
       giraffe
   m 
     microphone
   r 
     reef
   q 
     qar

因此,在此trie的每个级别,我们都会查看当前节点的子节点,并检查子节点的字符是否在我们的字典中。

如果是:我们在该树中深入并从字典中删除孩子的角色

直到你打了一片叶子(再也没有孩子)了,这里你知道这个词包含了这本词典中的所有字符。这是一个可能的候选人。现在我们想回到树中,直到我们找到另一个我们可以比较的匹配。 如果最新找到的匹配较小,则丢弃它,如果更长,这是我们现在可能的最佳匹配候选者。

有一天,重复将完成,你将得到所需的输出。

请注意,如果有一个最长的单词,则此方法有效,否则您必须返回候选人列表(这是面试的未知部分,您需要询问面试官希望看到的解决方案)

所以你需要Java代码,这里有一个简单的Trie和单个最长的单词版本:

public class LongestWord {

  class TrieNode {
    char value;
    List<TrieNode> children = new ArrayList<>();
    String word;

    public TrieNode() {
    }

    public TrieNode(char val) {
      this.value = val;
    }

    public void add(char[] array) {
      add(array, 0);
    }

    public void add(char[] array, int offset) {
      for (TrieNode child : children) {
        if (child.value == array[offset]) {
          child.add(array, offset + 1);
          return;
        }
      }
      TrieNode trieNode = new TrieNode(array[offset]);
      children.add(trieNode);
      if (offset < array.length - 1) {
        trieNode.add(array, offset + 1);
      } else {
        trieNode.word = new String(array);
      }
    }    
  }

  private TrieNode root = new TrieNode();

  public LongestWord() {
    List<String> asList = Arrays.asList("abacus", "deltoid", "gaff", "giraffe",
        "microphone", "reef", "qar");
    for (String word : asList) {
      root.add(word.toCharArray());
    }
  }

  public String search(char[] cs) {
    return visit(root, cs);
  }

  public String visit(TrieNode n, char[] allowedCharacters) {
    String bestMatch = null;
    if (n.children.isEmpty()) {
      // base case, leaf of the trie, use as a candidate
      bestMatch = n.word;
    }

    for (TrieNode child : n.children) {
      if (contains(allowedCharacters, child.value)) {
        // remove this child's value and descent into the trie
        String result = visit(child, remove(allowedCharacters, child.value));
        // if the result wasn't null, check length and set
        if (bestMatch == null || result != null
            && bestMatch.length() < result.length()) {
          bestMatch = result;
        }
      }
    }
    // always return the best known match thus far
    return bestMatch;
  }

  private char[] remove(char[] allowedCharacters, char value) {
    char[] newDict = new char[allowedCharacters.length - 1];
    int index = 0;
    for (char x : allowedCharacters) {
      if (x != value) {
        newDict[index++] = x;
      } else {
        // we removed the first hit, now copy the rest
        break;
      }
    }
    System.arraycopy(allowedCharacters, index + 1, newDict, index,
        allowedCharacters.length - (index + 1));

    return newDict;
  }

  private boolean contains(char[] allowedCharacters, char value) {
    for (char x : allowedCharacters) {
      if (value == x) {
        return true;
      }
    }
    return false;
  }

  public static void main(String[] args) {
    LongestWord lw = new LongestWord();
    String longestWord = lw.search(new char[] { 'a', 'e', 'f', 'f', 'g', 'i',
        'r', 'q' });
    // yields giraffe
    System.out.println(longestWord);
  }

}

我也只能建议阅读这本书Cracking the Coding Interview: 150 Programming Questions and Solutions,它会引导您完成决策并构建专门针对面试问题的算法。

答案 3 :(得分:-1)

免责声明:这不是一个特里解决方案,但我仍然认为这是一个值得探索的想法。

创建某种哈希函数,只考虑单词中的字母而不是它们的顺序(除了排列外,不应该发生冲突)。例如,ABCDDCBA都生成相同的哈希值(但ABCDD没有)。生成这样一个包含字典中每个单词的哈希表,使用链接来链接冲突(另一方面,除非你有严格的要求找到“所有”最长的单词而不只是一个,你可以只删除冲突,这只是排列,并放弃整个链接。)

现在,如果您的搜索集长度为4个字符,例如A, B, C, D,那么您可以检查以下哈希值以查看它们是否已包含在词典中:

hash(A), hash(B), hash(C), hash(D) // 1-combinations
hash(AB), hash(AC), hash(AD), hash(BC), hash(BD), hash(CD) // 2-combinations
hash(ABC), hash(ABD), hash(ACD), hash(BCD) // 3-combinations
hash(ABCD) // 4-combinations

如果按顺序搜索哈希值,则找到的最后一个匹配项将是最长的匹配项。

这最终会产生一个运行时间,该运行时间取决于搜索集的长度而不是字典的长度。如果M是搜索集中的字符数,那么哈希查找的数量是总和M choose 1 + M choose 2 + M choose 3 + ... + M choose M,它也是搜索集的powerset的大小,因此它是O(2^M) 。乍一看这听起来真的很糟糕,因为它是指数级的,但是从透视的角度来看,如果你的搜索集大小为10,那么只有大约1000次查找,这可能比实际现实场景中的字典大小要小得多。在M = 15时,我们得到32000次查找,真的,有多少英文单词超过15个字符?

我可以考虑两种(替代)方式来优化它:

1)首先搜索更长的匹配项,例如M组合然后(M-1) - 组合等。一旦找到匹配,你就可以停止!您可能只会覆盖搜索空间的一小部分,可能是最差的一半。

2)首先搜索较短的匹配(1个组合,2个组合等)。假设您在第2级遇到错过(例如,您的词典中没有字符串仅由AB组成)。使用辅助数据结构(可能是位图),您可以检查字典中的任何单词是否由AB组成部分(与您的主要内容相反)检查完整组合的哈希表。如果您也错过了辅助位图,那么您就知道可以跳过所有更高级别的组合,包括AB(即您可以跳过hash(ABC)hash(ABD)hash(ABCD),因为没有单词包含AB)。这利用了 Apriori 原则,并且随着M的增长和未命中变得更频繁,将大大减少搜索空间。编辑:我意识到我抽象出与“辅助数据结构”有关的细节是重要的。当我更多地考虑这个想法时,我意识到它倾向于完整的字典扫描作为一个子程序,这打破了整个方法的要点。不过,似乎应该有一种方法可以在这里使用 Apriori 原则。

答案 4 :(得分:-1)

我认为上述答案错过了关键点。我们有一个27维的空间,第一个是长度,其他是每个字母的坐标。在那个空间里,我们有点,这是单词。单词的第一个坐标是他的长度。对于每个字母维度,其他坐标是该字母中该字母的出现次数。例如, abacus deltoid gaff giraffe 麦克风,< em>礁石, qar abcdefghijklmnopqrstuvwxyz 有坐标

[3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[6, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]
[7, 0, 0, 0, 2, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
[4, 1, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[7, 1, 0, 0, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[10, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[4, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[26, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

具有坐标的集合的良好结构是R-treeR*-Tree。鉴于你的集合[x0,x1,...,x26],你必须要求每个字母包含最多包含xi字母的所有单词。您的搜索位于O(日志N),其中N是字典中的单词数。但是,您不希望查看与您的查询匹配的所有单词中的最大单词。这就是第一个维度很重要的原因。

你知道最大单词的长度在0到X之间,其中X = sum(x_i,i = 1..26)。您可以从X迭代搜索到1,但您也可以查询binary search algorithm的查询长度。您使用数组的第一个维度作为查询。你从a = X开始到b = X / 2。如果它们至少匹配,则从a搜索到(a + b)/ 2,否则从b搜索到b-(a-b)/ 2 =(3b-a)/ 2。你这样做直到你有b-a = 1。你现在拥有最大的长度和所有匹配的长度。

该算法渐近地比上述算法更有效。时间复杂度为O(ln(N)×ln(X))。实现取决于您使用的R树库。

答案 5 :(得分:-2)

Groovy(几乎是Java):

def letters = ['a', 'e', 'f', 'f', 'g', 'i', 'r', 'q']
def dictionary = ['abacus', 'deltoid', 'gaff', 'giraffe', 'microphone', 'reef', 'qar']
println dictionary
    .findAll{ it.toList().intersect(letters).size() == it.size() }
    .sort{ -it.size() }.head()

保存字典的集合类型的选择与算法无关。如果你应该实现一个特里,这是一回事。否则,只需从适当的库中创建一个来保存数据。 Java和Groovy在我的标准库中都没有我所知道的。

答案 6 :(得分:-2)

我试图在C ++中编码这个问题..我创建了自己的哈希键,并完成了与给定字符的所有组合。

完成从最大长度到1

的这些输入字符的所有组合

这是我的解决方案

#include "iostream"
#include <string>

using namespace std;

int hash_f(string s){
        int key=0;
        for(unsigned int i=0;i<s.size();i++){
           key += s[i];
        }
        return key;
}

class collection{

int key[100];
string str[10000];

public: 
collection(){
    str[hash_f( "abacus")] = "abacus"; 
    str[hash_f( "deltoid")] = "deltoid"; 
    str[hash_f( "gaff")] = "gaff"; 
    str[hash_f( "giraffe")] = "giraffe"; 
    str[hash_f( "microphone")] = "microphone"; 
    str[hash_f( "reef")] = "reef"; 
    str[hash_f( "qar")] = "qar"; 
}

string  find(int _key){
    return str[_key];
}
};

string sub_str(string s,int* indexes,int n ){
    char c[20];
    int i=0;
    for(;i<n;i++){
        c[i] = s[indexes[i]];
    }
    c[i] = 0;
    return string(c);
}

string* combination_m_n(string str , int m,int n , int& num){

    string* result = new string[100];
    int index = 0;

    int * indexes = (int*)malloc(sizeof(int)*n);

    for(int i=0;i<n;i++){
        indexes[i] = i; 
    }

    while(1){
            result[index++] = sub_str(str , indexes,n);
            bool reset = true;
            for(int i=n-1;i>0;i--)
            {
                if( ((i==n-1)&&indexes[i]<m-1) ||  (indexes[i]<indexes[i+1]-1))
                {
                    indexes[i]++;
                    for(int j=i+1;j<n;j++) 
                        indexes[j] = indexes[j-1] + 1;
                    reset = false;
                    break;
                }
            }
            if(reset){
                indexes[0]++;
                if(indexes[0] + n > m) 
                    break;
                for(int i=1;i<n;i++)
                    indexes[i] = indexes[0]+i;
            }
    }
    num = index;
    return result;
}


int main(int argc, char* argv[])
{
    string str = "aeffgirq";
    string* r;
    int num;

    collection c;
    for(int i=8;i>0;i--){
        r = combination_m_n(str, str.size(),i ,num);
        for(int i=0;i<num;i++){
            int key = hash_f(r[i]);
             string temp = c.find(key);
            if(  temp != "" ){
                  cout << temp ;
            }
        }
    }
}

答案 7 :(得分:-2)

假设一个大字典和一个少于10或11个成员的字母集(例如给出的例子),最快的方法是构建一个包含字母可以生成的单词的树,然后将单词列表与树匹配。换句话说,你的字母树的根有七个子节点:{a,e,f,g,i,r,q}。 “a”的分支有六个子节点{e,f,g,i,r,q}等。树因此包含可以用这些字母制作的每个可能的单词。

浏览列表中的每个单词并将其与树匹配。如果匹配是最大长度(使用所有字母),则完成。如果单词小于max,但比任何先前匹配的单词长,请记住它,这是“迄今为止最长的单词”(LWSF)。忽略任何长度等于LWSF的单词。另外,请忽略任何长于字母列表长度的单词。

这是一个线性时间算法,一旦构造了字母树,所以只要单词列表明显大于字母树,它就是最快的方法。

答案 8 :(得分:-2)

首先要注意的是,您可以完全忽略字母顺序。

有一个特里(好吧,一种特里)如下:

  • 从根目录开始,有26个孩子(最多),每个字母一个。
  • 从每个非根节点开始,子节点等于大于或等于节点字母的字母数。
  • 让每个节点存储所有可以使用(确切地)来自根路径中的字母的单词。

像这样构建trie:

对于每个单词,对该单词的字母进行排序,并将排序后的字母插入到trie中(通过从根创建这些字母的路径),随时创建所有必需的节点。并将单词存储在最终节点。

如何查询:

对于给定的一组字母,查找所有字母的子集(其中大多数希望不存在),并在遇到的每个节点输出单词。

<强>复杂度:

O(k!),其中k是提供的字母数。伊克!但是可笑的是,特里的话语越少,路径就越少,所需的时间就越少。 k 提供的字母数(应该相对较小),而不是trie中的字数。

实际上它可能更符合O(min(k!,n)),看起来好多了。请注意,如果你给了足够的字母,你将不得不查找所有单词,因此你必须在最坏的情况下O(n)工作,所以,就最坏的情况而言,你不能做得更好。

示例:

输入:

aba
b
ad
da
la
ma

排序:

aab
b
ad
ad
al
am

Trie :(只显示非空儿童)

     root
     /  \
    a    b
 /-/|\-\
a b d l m
|
b

查找adb

  • 从根...
  • 转到孩子a
    • 转到孩子b
      • 没有孩子,返回
    • 转到孩子d
      • 在节点 - adda
      • 输出字词
      • 没有孩子,返回
    • 处理完所有字母,返回
  • 转到孩子b
    • 在节点输出字 - b
    • 不寻找a孩子,因为只有孩子&gt; = b存在
    • 没有d孩子,返回
  • 没有d孩子,请停止