给定一个字典和一个字母列表,找到可以用字母构建的所有有效单词

时间:2014-08-14 00:16:03

标签: algorithm

蛮力方式可以解决O(n!)中的问题,基本上计算所有排列并检查字典中的结果。我正在寻找提高复杂性的方法。我可以考虑从字典中构建一个树,但仍然检查所有字母排列是O(n!)。有没有更好的方法来解决这个问题?

信件可以有重复。

该功能的api如下所示:

List<String> findValidWords(Dict dict, char letters[])

12 个答案:

答案 0 :(得分:16)

假设letters仅包含a到z的字母。

使用整数数组计算letters中字符的出现次数。

对于字典中的每个单词,检查单词中是否出现超出允许的单词,如果没有,请将此单词添加到result

    List<String> findValidWords(List<String> dict, char letters[]){
        int []avail = new int[26];
        for(char c : letters){
            int index = c - 'a';
            avail[index]++;
        }
        List<String> result = new ArrayList();
        for(String word: dict){
            int []count = new int[26];
            boolean ok = true;
            for(char c : word.toCharArray()){
                int index = c - 'a';
                count[index]++;
                if(count[index] > avail[index]){
                    ok = false;
                    break;
                }
            }
            if(ok){
                result.add(word);
            }
        }
        return result;
    }

所以我们可以看到时间复杂度是 O(m * k),m是字典中的单词数,k是单词中字符的最大总数

答案 1 :(得分:7)

您可以对字典中的每个单词进行排序,使字母与字母表中的单词顺序相同,然后从排序后的单词中构建一个单元格。 (其中每个节点包含可以用字母组成的所有单词的列表)。 (字典总字母长度的线性时间)然后,给定一组查询字母,以相同的方式对字母进行排序,并使用从左到右使用字母子集的所有可能方向中的深度优先搜索来继续使用特里。每当您到达包含单词的trie中的节点时,输出这些单词。您探索的每个路径都可以收费至字典中的至少一个单词,因此查找包含您可以制作单词的所有节点的最差情况复杂度为O(kn),其中n是字典中单词的数量,k是单词中的最大字母数。但是对于有些受限制的查询字母集,每个查询的运行时间应该快得多。

答案 2 :(得分:3)

更好的方法是循环遍历字典中的所有单词,看看是否可以使用数组中的字母构建单词。

答案 3 :(得分:2)

这是一种算法,可以找到O(1)中可由一组字母组成的所有单词。我们将用它们的光谱表示单词并将它们存储在前缀树(aka trie)中。

一般说明

单词W的频谱是大小为S的数组N,因此S(i)是出现次数(又名频率)A(i)字母中的W字母,其中A(i)是所选字母的i字母,N是其大小。

例如,在英文字母中,A(0)AA(1)B,......,A(25)Z 。单词aha的频谱为<2,0,0,0,0,0,0,1,0,...,0>

我们将字典存储在前缀trie中,使用频谱作为键。密钥的第一个标记是字母A的频率,第二个是字母B的频率,依此类推。 (从这里和下面我们将以英文字母为例)。

一旦形成,我们的字典将是一个高度为26的树,宽度随每个级别而变化,具体取决于字母的受欢迎程度。基本上,每个图层将具有多个子树,这些子树等于所提供字典中此字母的最大字​​频。

由于我们的任务不仅仅是决定我们是否可以从提供的字符集构建一个单词而且还要找到这些单词(搜索问题),那么我们需要将这些单词附加到它们的光谱上(因为光谱变换是不可逆,考虑单词readdear)的光谱。我们将在表示其频谱的每条路径的末尾附加一个字。

为了找出我们是否可以从提供的集合构建一个单词,我们将构建该集合的频谱,并找到前缀trie中的所有路径,其频率由该集合频谱的相应频率限定。 (注意,我们不强制使用集合中的所有字母,因此如果单词使用较少的字母,那么我们可以构建它。基本上,我们的要求是,对于单词中的所有字母,字母的频率应小于或等于所提供的集合中相同字母的频率)。

搜索过程的复杂性并不取决于字典的长度或所提供的集合的长度。平均而言,它等于字母平均频率的26倍。鉴于,英语词典,这是一个非常小的常数因素。对于其他词典,情况可能并非如此。

参考实施

我将在OCaml中提供算法的参考实现。

字典数据类型是递归的:

type t = {
  dict : t Int.Map.t;
  data : string list;
}

(注意:它不是最好的表示,可能最好表示它是一个和类型,例如type t = Dict of t Int.Map.t | Data of string list,但我发现使用上面的表示更容易实现它)。

我们可以通过频谱函数来概括算法,使用仿函数,或者只是将频谱函数存储在字典中,但为了简单起见,我们将只用ASCII表示法对英文字母进行硬编码,

let spectrum word =
  let index c = Char.(to_int (uppercase c) - to_int 'A') in
  let letters = Char.(to_int 'Z' - to_int 'A' + 1) in
  Array.init letters ~f:(fun i ->
      String.count word ~f:(fun c -> index c = i))

接下来,我们将定义类型add_word的{​​{1}}函数,它将为我们的字典添加一条新路径,通过将一个单词分解为其频谱,并添加每个成分。每次添加都需要完全dict -> string -> dict次迭代,不包括频谱计算。注意,该实现纯粹是功能性的,并没有使用任何命令性功能。每次函数26返回一个新的数据结构。

add_word

我们在let add_word dict word = let count = spectrum word in let rec add {dict; data} i = if i < Array.length count then { data; dict = Map.update dict count.(i) ~f:(function | None -> add empty (i+1) | Some sub -> add sub (i+1)) } else {empty with data = word :: data} in add dict 0 函数中使用empty值的以下定义:

add

现在让我们定义let empty = {dict = Int.Map.empty; data=[]} 类型的is_buildable函数,该函数将决定是否可以使用给定的字符集来构建字典中的任何单词。虽然我们可以通过搜索来表达它,但通过检查找到的集合的大小,我们仍然希望有一个专门的实现,因为它更有效,更容易理解。该功能的定义紧跟上面提供的一般描述。基本上,对于字母表中的每个字符,我们检查字典中是否存在频率小于或等于建筑物集中频率的条目。如果我们检查了所有字母,那么我们证明,我们可以用给定的集合构建至少一个单词。

dict -> string -> bool

现在,让我们找到所有单词的集合,可以从提供的集合中构建:

let is_buildable dict set =
  let count = spectrum set in
  let rec find {dict} i =
    i >= Array.length count ||
    Sequence.range 0 count.(i) ~stop:`inclusive |>
    Sequence.exists ~f:(fun cnt -> match Map.find dict cnt with
      | None -> false
      | Some dict -> find dict (i+1)) in
  find dict 0

我们基本上会遵循let build dict set = let count = spectrum set in let rec find {dict; data} i = if i < Array.length count then Sequence.range 0 count.(i) ~stop:`inclusive |> Sequence.concat_map ~f:(fun cnt -> match Map.find dict cnt with | None -> Sequence.empty | Some dict -> find dict (i+1)) else Sequence.of_list data in find dict 0 函数的结构,除了不是证明每个字母存在这样的频率,我们将通过到达路径的末尾并抓取所有的证据来收集所有证据。这个词附在它上面。

测试和示例

为了完整起见,我们将通过创建一个小程序来测试它,该程序将读取字典,每个单词在一个单独的行上,并通过询问一组并打印结果集来与用户交互。可以用它构建的单词。

is_buildable

以下是交互示例,它使用我的计算机上可用的module Test = struct let run () = let dict = In_channel.(with_file Sys.argv.(1) ~f:(fold_lines ~init:empty ~f:add_word)) in let prompt () = printf "Enter characters and hit enter (or Ctrl-D to stop): %!" in prompt (); In_channel.iter_lines stdin ~f:(fun set -> build dict set |> Sequence.iter ~f:print_endline; prompt ()) end 字典(Ubunty Trusty)。

/usr/share/dict/american-english

(是的,字典中包含的单词,如./scrabble.native /usr/share/doct/american-english Enter characters and hit enter (or Ctrl-D to stop): read r R e E re Re Er d D Rd Dr Ed red Red a A Ra Ar era ear are Rae ad read dear dare Dare Enter characters and hit enter (or Ctrl-D to stop): r,可能不是真正的英文单词。事实上,对于每个字母,字典都有一个单词,所以,我们基本上可以构建每个非空字母集中的一个单词)。

可以在Gist

上找到完整的实施以及构建说明

答案 4 :(得分:1)

  1. 通过按顺序排序,“签署”可用的字母;那是O(m log m),其中m是字母数。

  2. 通过按顺序对单词的字母进行排序,对字典中的每个单词进行“签名”;那是O(k log k),其中k是单词的长度。

  3. 将字母签名与每个单词签名进行比较;那是O(min(m,k)* n),其中n是字典中的单词数。输出任何匹配的单词。

  4. 假设一个大约25万字的英文单词列表,并且不超过大约6个单词,那应该几乎是即时的。

答案 5 :(得分:1)

我最近在BankBazaar采访中被问到同样的问题。我可以选择(他以非常微妙的方式说)以我想要的任何方式预处理字典。

我的第一个想法是将字典排列在三叉或三元搜索树中,并根据给出的字母制作所有单词。在任何优化方式,这将需要n! + n-1! + n-2! N-3!在最坏的情况下,+ ..... + n字检查(n是字母数),这是不可接受的。

另一种方法是检查所有词典单词是否可以用给定的字母制作。再次以任何优化的方式,在最坏的情况下,将采用noOfDictionaryWords(m)*字典单词(k)的平均大小,这也是不可接受的。

现在我有了! + n-1! + n-2! + .... + N个单词,我必须在字典中查看,我不想全部检查,所以我必须检查它们的一部分,以及如何对它们进行分组的情况是什么

如果我只检查组合而不是排列,则结果为2 ^ n。

所以我必须以这样的方式预处理字典单词,如果我传递一个组合,那么所有的字谜都会打印出来。

像这样的ds:http://1.bp.blogspot.com/-9Usl9unQJpY/Vg6IIO3gpsI/AAAAAAAAAbM/oTuhRDWelhQ/s1600/hashmapArrayForthElement.png 由字母(不论其位置和排列)做出的哈希值,指向包含这些字母所有单词的列表,然后我们只需要检查该哈希值。

我给出了通过为所有字母分配素值来制作哈希值的答案,并在计算单词的哈希值时,将所有指定的值相乘。如果第26个素数为101,并且地图中的许多空值占用空间,这将产生具有非常大的哈希值的问题。我们可以稍微优化一下,而不是用a = 2,b = 3,c = 5,d = 7 ...... z = 101按字典顺序开始,我们搜索最常用的字母并为它们分配小值,如元音,'s','t'等 面试官接受了它,但没有期待答案,所以肯定有另一个答案,无论好坏,但有。

答案 6 :(得分:0)

以下是更有效的方式: -

1.Use count sort to count all letters appearing in the a word in dictionary.
2.Do count sort on the collection of letter that you are given. 
3.Compare if the counts are same then the word can be made.
4. Do this for all words in dictionary.

这对于多个此类查询效率低下,因此您可以执行以下操作: -

1. make a tupple for each word using count sort.
2. put the tupple in a Tree or hashmap with count entries.
3. When query is given do count sort and lookup tupple in hashmap

时间复杂度: -

上述方法为查询提供 O(1)时间复杂度,并为哈希表构造提供 O(N)时间复杂度,其中N不是字典中的单词< / p>

答案 7 :(得分:0)

(参见anagram搜索,例如基于签名的方法using primes looks cleaner - 收集letters&#34;]的所有非等效&#34;子字符串
鉴于激励,我(预)订购Dict(构成每个单词的字符集,增加长度)并循环来自letters的子集,检查每个单词的有效性,直到长。
或者,从dict charletters "eeaspl"中找到一组单词可以被视为多维范围查询:letters指定字母,有效单词为零到两个&#34; e&#34;,a,s,p,l中的一个或者没有,根本没有其他字符 - 字长的边界(不超过dict,下限为品味)混合很好。
然后,像k-d-trees这样的数据结构很少有选择性维度。 (可能会发表评论:你没有提到字母基数,是否&#34;有效&#34;取决于大写或变音符号,&#34;复杂性&#34;包括程序员努力或dict的预处理 - 后者如果{{1}}是不可变的,可能很难分摊。)

答案 8 :(得分:0)

如果可以重复字母,那意味着一个单词可以无限长。你显然可以将它限制在字典中最长单词的长度,但仍有太多单词需要检查。就像nmore建议的那样,你宁愿迭代字典来做到这一点。

List<String> findAllValidWords(Set<String> dict, char[] letters) {
  List<String> result = new LinkedList<>();
  Set<Character> charSet = new HashSet<>();
  for (char letter : letters) {
    charSet.add(letter);
  }
  for (String word : dict) {
    if (isPossible(word, charSet)) {
      result.add(word);
    }
  }
  return result;
}

boolean isPossible(String word, Set<Character> charSet) {
  // A word is possible if all its letters are contained in the given letter set
  for (int i = 0; i < word.length(); i++) {
    if (!charSet.contains(word.charAt(i))) {
      return false;
    }
  }
  return true;
}

答案 9 :(得分:0)

Swift 3

 func findValidWords(wordsList: [String] , string: String) -> [String]{

    let charCountsDictInTextPassed = getCharactersCountIn(string: string)
    var wordsArrayResult: [String] = []

    for word in wordsList {

        var canBeProduced = true
        let currentWordCharsCount = getCharactersCountIn(string: word)

        for (char, count) in currentWordCharsCount {
            if let charCountInTextPassed = charCountsDictInTextPassed[char], charCountInTextPassed >= count {
                continue
            }else{
                canBeProduced = false
                break
            }
        }// end for

        if canBeProduced {
            wordsArrayResult.append(word)
        }//end if
    }//end for
        return wordsArrayResult
}


// Get the count of each character in the string
func getCharactersCountIn(string: String) -> [String: Int]{
    var charDictCount:[String: Int] = [:]
    for char in string.characters {
        if let count = charDictCount[String(char)] {
            charDictCount[String(char)] = count + 1
        }else{
            charDictCount[String(char)] = 1
        }
    }//end for
    return charDictCount
}

答案 10 :(得分:0)

雨燕4

func findValidWords(in dictionary: [String], with letters: [Character]) -> [String] {
    var validWords = [String]()

    for word in dictionary {
        var temp = word
        for char in letters {
            temp = temp.filter({ $0 != char })

            if temp.isEmpty {
                validWords.append(word)
                break
            }
        }
    }

    return validWords
}

print(findValidWords(in:[“ ape”,“ apples”,“ orange”,“ elapse”,“ lap”,“ soap”,“ bar”,“ sole”],其中:[“ a”, “ p”,“ l”,“ e”,“ s”,“ o”])))

输出=> [“猿”,“苹果”,“经过”,“圈”,“肥皂”,“唯一”]

答案 11 :(得分:0)

我的英语不好,所以请尝试理解。

我的方法是使用逐位提高速度。不过仍然蛮力。

第一步

我们只考虑每个单词中的不同字符并标记其存在。英语有26个字符,因此我们需要26位。整数是32位。够了。

现在将字典中的每个单词编码为整数。

abcdddffg -> 123444667 -> 123467 (only distinct characters) -> 1111011 (bits) -> 123 (decimal number)

因此2,000,000个单词将转换为2,000,000个整数。

现在让我们说你有这组字母:a,b,c,d,e

abcde -> 12345 -> 1111100 (bits)

进行“与”运算,我们有:

1111100 (abcde)
&
1111011 (abcdddffg, no e)
=
1111000 (result) => result != abcdddffg => word cannot be created

具有a,b,c,d,e,f,g,h的其他示例:

11111111 (abcdefgh)
&
11110110 (abcdddffg, no e and h)
= 
11110110 (result) => result == abcdddffg => word can be created

第二步

在将单词转换为数字时,还要存储字母数。如果我们在第一步中找到了匹配项,我们将继续检查字母数是否也足够。

根据要求,您可能不需要第二步。

复杂度

  • O(n)将单词转换为数字并存储字母计数。只需执行一次。
  • 每个搜索查询的
  • O(n)。