使用trie进行字符串分段 - 时间复杂度?

时间:2017-02-28 17:47:28

标签: string algorithm time-complexity trie substring

要解决的问题:

  

给定非空字符串s和包含列表的字符串数组wordArr   非空单词,确定s是否可以分割为a   空格分隔的一个或多个字典单词的序列。你可以   假设字典不包含重复的单词。

     

例如,给定s =“leetcode”,wordArr = [“leet”,“code”]。

     

返回true,因为“leetcode”可以被分割为“leet code”。

在上面的问题中,是否可以构建一个包含wordArr中每个字符串的trie。然后,对于给定字符串s中的每个字符,请按照特里结构进行操作。如果trie分支终止,则此子字符串完成,因此将剩余的字符串传递给根,并以递归方式执行完全相同的操作。

这应该是O(N)时间和O(N)空间是否正确?我问,因为我正在研究的问题是以最优的方式这将是O(N ^ 2)时间,我不确定我的方法有什么问题。

例如,如果s = "hello"wordArr = ["he", "ll", "ee", "zz", "o"],则"he"将在trie的第一个分支中完成,"llo"将以递归方式传递给根。然后,"ll"将完成,因此"o"会传递到特里的根。然后"o"完成,这是s的结束,所以返回true。如果s的结尾未完成,则返回false。

这是对的吗?

3 个答案:

答案 0 :(得分:1)

你的例子确实会提出线性时间复杂度,但请看这个例子:

 s = "hello" 
 wordArr = ["hell", "he", "e", "ll", "lo", "l", "h"]

现在,首先尝试“地狱”,但是在下一个递归周期中,没有找到解决方案(没有“o”),所以算法需要回溯并假设“地狱”不合适(双关语不是),所以你尝试“他”,并在下一级别找到“ll”,但然后它再次失败,因为没有“o”。再次需要回溯。现在从“h”开始,然后是“e”,然后再次出现故障:你尝试“ll”没有成功,所以回溯使用“l”代替:解决方案现在可用:“hel lo”。

所以,没有这个没有 O(n)时间复杂度。

答案 1 :(得分:0)

我怀疑这个问题是回溯的问题。如果单词基于特定字典不可分段,或者如果有多个可能的子字符串具有公共前缀,该怎么办?例如,假设字典包含hellenicllo。在trie的一个分支上失效将需要回溯,一些相应的时间复杂度增加。

这类似于正则表达式匹配问题:您提供的示例就像测试输入词一样

^(he|ll|ee|zz|o)+$

(任意数量的字典成员,任何顺序,没有别的)。我不知道正则表达式匹配的时间复杂性,但我知道回溯可以让你进入serious time trouble

我确实找到了this answer,其中说:

  

对字符串运行DFA编译的正则表达式确实是O(n),但最多可能需要O(2 ^ m)构造时间/空间(其中m =正则表达式大小)。

所以也许是O(n ^ 2),减少了施工工作量。

答案 2 :(得分:0)

让我们首先将trie转换为nfa。我们在根上创建一个接受节点,并添加一条边,该边从trie中字典的每个单词末尾移动到空char的根节点。

时间复杂度:由于trie中的每个步骤,我们只能移动到表示输入字符串和根中当前char的一个边。 T(n)= 2×T(n-1)+ c 这给了我们O(2 ^ n)

确实不是O(n),但你可以使用动态编程做得更好。

  • 我们将使用自上而下的方法。
  • 在我们解决任何字符串检查之前,如果我们已经解决了它。
  • 我们可以使用另一个HashMap来存储已经解决的字符串的结果。
  • 每当递归调用返回false时,将该字符串存储在HashMap中。

这个想法是只计算一次单词的每个后缀。我们只有n个后缀,最终会得到O(n ^ 2)。

代码格式algorithms.tutorialhorizo​​n.com:

Map<String, String> memoized;
Set<String> dict;

String SegmentString(String input) {
  if (dict.contains(input)) return input;
  if (memoized.containsKey(input) {
    return memoized.get(input);
  }
  int len = input.length();
  for (int i = 1; i < len; i++) {
    String prefix = input.substring(0, i);
    if (dict.contains(prefix)) {
      String suffix = input.substring(i, len);
      String segSuffix = SegmentString(suffix);
      if (segSuffix != null) {
        memoized.put(input, prefix + " " + segSuffix);
        return prefix + " " + segSuffix;
    }
}

你可以做得更好!

Map<String, String> memoized;
Trie<String> dict;

String SegmentString(String input) 
{
    if (dict.contains(input)) 
        return input;
    if (memoized.containsKey(input) 
        return memoized.get(input);

    int len = input.length();
    foreach (StringBuilder word in dict.GetAll(input)) 
    {
        String prefix = input.substring(0, word.length);
        String suffix = input.substring(word.length, len);
        String segSuffix = SegmentString(suffix);
        if (segSuffix != null) 
        {
            memoized.put(input, word.ToString()  + " " + segSuffix);
            return prefix + " " + segSuffix;
        }
    }
    retrun null;
}

使用Trieto只有在Trie到达单词结尾时才能找到递归调用,你会得到o(z×n),其中z是Trie的长度。