【问题标题】:Breaking a string apart into a sequence of words将字符串拆分为单词序列
【发布时间】:2011-11-03 23:41:47
【问题描述】:

我最近遇到了以下面试问题:

给定一个输入字符串和一个单词字典,实现一个方法,将输入字符串分解为一个由空格分隔的字典单词字符串,搜索引擎可能会使用该字符串来搜索“您的意思是什么?”例如,“applepie”的输入应该产生“apple pie”的输出。

就复杂性而言,我似乎无法找到最佳解决方案。有人对如何有效地做到这一点有任何建议吗?

【问题讨论】:

    标签: algorithm search search-engine complexity-theory


    【解决方案1】:

    一种选择是将所有有效的英语单词存储在 trie 中。完成此操作后,您可以开始按照字符串中的字母从根向下遍历 trie。每当您找到标记为单词的节点时,您有两种选择:

    1. 此时中断输入,或
    2. 继续扩展这个词。

    一旦您将输入分解为一组完全合法且没有剩余字符的单词,您就可以声称您找到了匹配项。由于在每个字母处,您要么有一个强制选项(要么您正在构建一个无效的单词并且应该停止 - 或者 - 您可以继续扩展单词)或两个选项(拆分或继续),因此您可以实现此功能使用穷举递归:

    PartitionWords(lettersLeft, wordSoFar, wordBreaks, trieNode):
        // If you walked off the trie, this path fails.
        if trieNode is null, return.
    
        // If this trie node is a word, consider what happens if you split
        // the word here.
        if trieNode.isWord:
            // If there is no input left, you're done and have a partition.
            if lettersLeft is empty, output wordBreaks + wordSoFar and return
    
            // Otherwise, try splitting here.
            PartitinWords(lettersLeft, "", wordBreaks + wordSoFar, trie root)
    
        // Otherwise, consume the next letter and continue:
        PartitionWords(lettersLeft.substring(1), wordSoFar + lettersLeft[0], 
                       wordBreaks, trieNode.child[lettersLeft[0])
    

    在最坏的情况下,这将列出字符串的所有分区,这些分区不能成指数增长。但是,只有当您可以以多种方式对字符串进行分区时才会发生这种情况,这些方式都以有效的英语单词开头,并且在实践中不太可能发生。但是,如果字符串有很多分区,我们可能会花费大量时间来查找它们。例如,考虑字符串“dotherdo”。我们可以通过多种方式拆分:

    do the redo
    do the red o
    doth ere do
    dot here do
    dot he red o
    dot he redo
    

    为避免这种情况,您可能希望限制您报告的答案数量,可能是两个或三个。

    由于我们在离开 trie 时切断了递归,所以如果我们尝试拆分时不会使字符串的其余部分保持有效,我们将很快检测到这一点。

    希望这会有所帮助!

    【讨论】:

      【解决方案2】:

      link 将此问题描述为一个完美的面试问题,并提供了几种解决方法。本质上它涉及recursive backtracking。在这个级别上,它将产生 O(2^n) 复杂度。使用记忆化的有效解决方案可以将这个问题降低到 O(n^2)。

      【讨论】:

      • 非常感谢,帮我获得这个美丽的链接!!.. wat 可以是一个完美的答案.. 欢呼这个对问题给予如此尊重的人,我在谷歌中被问到同样的问题面试一次!!
      • 我们有一个运行在字符串长度上的外循环(比如 i=1:length(s),其中 s 是输入字符串)和一个运行到当前前缀索引 i 的内循环(比如 j=1 :一世)。由于我们希望每个后缀仅在第一次时才在字典中查找(其余查找将在地图中),因此运行时间为 O(n^2)。这个推理正确吗?
      【解决方案3】:

      看起来这个问题正是我的面试问题,具体到我在 The Noisy Channel 的post 中使用的示例。很高兴你喜欢这个解决方案。我很确定你无法击败我描述的最坏情况下的 O(n^2) 动态编程/记忆解决方案。

      如果您的字典和输入不是病态的,您可以在实践中做得更好。例如,如果您可以在线性时间内识别输入字符串的子字符串在字典中(例如,使用 trie),并且如果此类子字符串的数量是恒定的,那么总时间将是线性的。当然,这是很多假设,但真实数据通常比病态的最坏情况要好得多。

      该问题还有一些有趣的变体使其变得更难,例如枚举所有有效的分段,根据最佳的某些定义输出最佳分段,处理太大而无法放入内存的字典,以及处理不精确的分段(例如,纠正拼写错误)。随时在我的博客上发表评论或以其他方式联系我以跟进。

      【讨论】:

      • 我知道这是一篇旧文章,但在阅读了您的精彩博文后我有一个问题。 O(2^n) 仍然让我对一般解决方案感到困惑,尽管直觉上它可能是有道理的。我尝试使用组合来解决它以及解决递归(T(n)= n * T(n-1)+ O(k)),但我只能得到一个涉及n乘积的界限!与 Gamma 函数。您是否也尝试解决递归以得出 O(2^n)?
      【解决方案4】:

      导入 java.util.*;

      class Position {
          int indexTest,no;
          Position(int indexTest,int no)
          {
              this.indexTest=indexTest;
              this.no=no;
          } } class RandomWordCombo {
          static boolean isCombo(String[] dict,String test)
          {
              HashMap<String,ArrayList<String>> dic=new HashMap<String,ArrayList<String>>();
              Stack<Position> pos=new Stack<Position>();
              for(String each:dict)
              {
                  if(dic.containsKey(""+each.charAt(0)))
                  {
                      //System.out.println("=========it is here");
                      ArrayList<String> temp=dic.get(""+each.charAt(0));
                      temp.add(each);
                      dic.put(""+each.charAt(0),temp);
                  }
                  else
                  {
                      ArrayList<String> temp=new ArrayList<String>();
                      temp.add(each);
                      dic.put(""+each.charAt(0),temp);
                  }
              }
              Iterator it = dic.entrySet().iterator();
          while (it.hasNext()) {
              Map.Entry pair = (Map.Entry)it.next();
              System.out.println("key: "+pair.getKey());
              for(String str:(ArrayList<String>)pair.getValue())
              {
                  System.out.print(str);
              }
          }
              pos.push(new Position(0,0));
              while(!pos.isEmpty())
              {
                  Position position=pos.pop();
                  System.out.println("position index: "+position.indexTest+" no: "+position.no);
                  if(dic.containsKey(""+test.charAt(position.indexTest)))
                  {
                      ArrayList<String> strings=dic.get(""+test.charAt(position.indexTest)); 
                      if(strings.size()>1&&position.no<strings.size()-1)
                           pos.push(new Position(position.indexTest,position.no+1));
                      String str=strings.get(position.no);
                      if(position.indexTest+str.length()==test.length())
                          return true;
                      pos.push(new Position(position.indexTest+str.length(),0));
                  }
              }
              return false;
          }
          public static void main(String[] st)
          {
              String[] dic={"world","hello","super","hell"};
              System.out.println("is 'hellworld' a combo: "+isCombo(dic,"superman"));
          } }
      

      我也做过类似的问题。如果给定的字符串是字典单词的组合,则此解决方案给出 true 或 false。它可以很容易地转换为以空格分隔的字符串。它的平均复杂度为 O(n),其中 n:给定字符串中字典单词的数量。

      【讨论】:

        【解决方案5】:

        使用python,我们可以写两个函数,第一个segment返回给定字典的一段连续文本的第一次分割,如果没有找到这样的分割,则返回None。另一个函数segment_all 返回找到的所有分段的列表。最坏情况复杂度为 O(n**2),其中 n 是以字符为单位的输入字符串长度。

        这里介绍的解决方案可以扩展到包括拼写更正和二元分析以确定最可能的分割。

        def memo(func):
            '''
            Applies simple memoization to a function
            '''
            cache = {}
            def closure(*args):
                if args in cache:
                    v = cache[args]
                else:
                    v = func(*args)
                    cache[args] = v
                return v
            return closure
        
        
        def segment(text, words):
            '''
            Return the first match that is the segmentation of 'text' into words
            '''
            @memo
            def _segment(text):
                if text in words: return text
                for i in xrange(1, len(text)):
                    prefix, suffix = text[:i], text[i:]
                    segmented_suffix = _segment(suffix)
                    if prefix in words and segmented_suffix:
                        return '%s %s' % (prefix, segmented_suffix)
                return None
            return _segment(text)
        
        
        def segment_all(text, words):
            '''
            Return a full list of matches that are the segmentation of 'text' into words
            '''
            @memo
            def _segment(text):
                matches = []
                if text in words: 
                    matches.append(text)
                for i in xrange(1, len(text)):
                    prefix, suffix = text[:i], text[i:]
                    segmented_suffix_matches = _segment(suffix)
                    if prefix in words and len(segmented_suffix_matches):
                        for match in segmented_suffix_matches:
                            matches.append('%s %s' % (prefix, match))
                return matches 
            return _segment(text)
        
        
        if __name__ == "__main__":    
            string = 'cargocultscience'
            words = set('car cargo go cult science'.split())
            print segment(string, words)
            # >>> car go cult science
            print segment_all(string, words)
            # >>> ['car go cult science', 'cargo cult science']
        

        【讨论】:

          猜你喜欢
          • 2011-06-12
          • 2019-03-12
          • 1970-01-01
          • 2014-06-09
          • 2023-01-22
          • 1970-01-01
          • 1970-01-01
          • 2022-01-18
          • 2011-10-23
          相关资源
          最近更新 更多