【问题标题】:Longest common prefix for n stringn 个字符串的最长公共前缀
【发布时间】:2012-01-24 14:06:28
【问题描述】:

给定 n 个最大长度为 m 的字符串。我们如何找到它们中至少两个字符串共享的最长公共前缀?

例子:['flower', 'flow', 'hello', 'fleet']

答案:fl

我正在考虑为所有字符串构建一个 Trie,然后检查分支到两个/更多子字符串(满足共性)的最深节点(满足最长)。这需要 O(n*m) 时间和空间。有没有更好的方法来做到这一点

【问题讨论】:

  • @Mark 我相信这个例子是flow。从提议的解决方案的上下文来看,它只需要至少对 2 个通用,而不是对所有人通用。我同意这里有必要 OP 进行一些澄清。
  • 一个字符串可能没有'fl'开头。 'hello' 被用来证明它可以是任何字符串,其中一个字符串不需要与其他字符串有任何公共前缀

标签: algorithm


【解决方案1】:

为什么要用trie(需要O(mn)时间和O(mn)空间,就用基本的蛮力方式。第一次循环,找到最短的字符串为minStr,耗时o(n),第二次循环,和这个minStr一一比较,并保留一个变量表示minStr的最右边的索引,这个循环需要O(mn),其中m是所有字符串中最短的长度。代码如下,

public String longestCommonPrefix(String[] strs) {
    if(strs.length==0) return "";
    String minStr=strs[0];

    for(int i=1;i<strs.length;i++){
        if(strs[i].length()<minStr.length())
            minStr=strs[i];
    }
    int end=minStr.length();
    for(int i=0;i<strs.length;i++){
        int j;
        for( j=0;j<end;j++){
            if(minStr.charAt(j)!=strs[i].charAt(j))
                break;
        }
        if(j<end)
            end=j;
    }
    return minStr.substring(0,end);
}

【讨论】:

    【解决方案2】:

    这个问题有一个O(|S|*n) 解决方案,使用trie。 [n是字符串的个数,S是最长的字符串]

    (1) put all strings in a trie
    (2) do a DFS in the trie, until you find the first vertex with more than 1 "edge".
    (3) the path from the root to the node you found at (2) is the longest common prefix.
    

    没有比它更快的解决方案 [在大 O 表示法方面],在最坏的情况下,您的所有字符串都是相同的 - 您需要阅读所有字符串才能知道它。

    【讨论】:

    • 这也是我在考虑并在问题中概述的方法。我同意时间的下限,因为我们必须读取每个字符串一次。空间复杂度还是O(n*m),能不能做得更好?
    • @shreyasva:刷新你的页面,我加了一句解释为什么这个问题是Omega(n*m),所以没有比O(n*m)更好的解决方案了
    • 我不了解 DFS 解决方案。能举个例子吗?
    • 我们不能从左到右检查位置 X 的字符是否对于所有字符串都相同,删除不满足的字符串并像这样迭代直到我们去最短字符串的结尾,或者我们的字符串数组中只有 1 个字符串。我们只需要记住最后一个最长的前缀。相同的时间复杂度,但不需要尝试。
    【解决方案3】:

    我会对它们进行排序,您可以在n lg n 时间完成。然后任何具有公共前缀的字符串都将彼此相邻。事实上,您应该能够保留您当前正在查看的索引的指针,并继续向下进行非常快速的计算。

    【讨论】:

    • 排序需要 nmlog(nm) 因为在最坏情况下的比较需要 O(m)
    • 实际上它是O(m*nlog(n)) 而不是O(nmlog(nm)),因为正如你所说:比较需要O(m),其中有O(nlogn),总共有O(mnlog(n))
    • 在最坏的情况下,是的,这是真的。然而,最坏的情况只适用于字符串本身非常非常相似的情况,这意味着交换会少得多。我还没有计算过,可以想象一个人为的外壳会导致它退化,但它似乎仍然是最好的选择,尤其是考虑到空间。
    • 另外,m 的概念仅在m 显着接近或大于n 时才重要。对于标准英语单词,典型的m 将是 12 或更少,如果我们考虑非创造的、非技术性的单词,我们会看到 33(反反建制主义)。因此,如果您的 n 比方说 100 或更少,它会产生影响,但如果是这种情况,您的整个操作很小并且 O 不适用。 O 符号用于渐近评估。
    【解决方案4】:

    作为与我的其他答案完全不同的答案...

    您可以一次性根据第一个字母对每个字符串进行存储。

    使用另一遍,您可以稍后根据第二个存储桶对每个存储桶进行排序。 (这称为基数排序,每次通过时为 O(n*m)O(n)。)这为您提供了基线前缀 2。

    您可以安全地从数据集中删除任何前缀不为 2 的元素。

    您可以继续基数排序,删除没有p 共享前缀的元素,因为p 接近m

    这将为您提供与 trie 方法相同的 O(n*m) 时间,但总是比 trie 更快,因为 trie 必须查看每个字符串中的每个字符(当它进入结构时),而这种方法是只保证每个字符串查看 2 个字符,此时它会剔除大部分数据集。

    最坏的情况仍然是每个字符串都是相同的,这就是为什么它共享相同的大 O 表示法,但在所有情况下都会更快,因为保证使用更少的比较,因为在任何“非最坏情况”上是永远不需要访问的角色。

    【讨论】:

      【解决方案5】:
      public String longestCommonPrefix(String[] strs) {
      
          if (strs == null || strs.length == 0)
              return "";
      
          char[] c_list = strs[0].toCharArray();
          int len = c_list.length;
          int j = 0;
          for (int i = 1; i < strs.length; i++) {
              for (j = 0; j < len && j < strs[i].length(); j++) 
                 if (c_list[j] != strs[i].charAt(j)) 
                  break;
              len = j;
          }
      
          return new String(c_list).substring(0, len);
      
      }
      

      【讨论】:

      • 很棒的解决方案 ;)
      【解决方案6】:

      corsiKa 所描述的桶排序(基数排序)可以扩展,使得所有字符串最终都单独放在一个桶中,此时,这样一个单独的字符串的 LCP 是已知的。此外,每个字符串的shustring也是已知的;它比 LCP 长一个。桶排序实际上是后缀数组的构造,但只是部分如此。那些未执行的比较(如 corsiKa 所述)确实代表了未添加到后缀数组的后缀字符串的那些部分。最后,这种方法不仅可以确定 LCP 和 shustrings,而且可以很容易地找到那些不存在于字符串中的子序列。

      【讨论】:

        【解决方案7】:

        既然世界显然都在乞求 Swift 的答案,这里是我的 ;)

        func longestCommonPrefix(strings:[String]) -> String {
        
            var commonPrefix = ""
            var indices = strings.map { $0.startIndex}
        
        outerLoop:
        
            while true {
                var toMatch: Character = "_"
        
                for (whichString, f) in strings.enumerate() {
        
                    let cursor = indices[whichString]
        
                    if cursor == f.endIndex {   break outerLoop     }
        
                    indices[whichString] = cursor.successor()
        
                    if whichString == 0     {   toMatch = f[cursor] }
                    if toMatch != f[cursor] {   break outerLoop     }
                }
        
                commonPrefix.append(toMatch)
            }
        
            return commonPrefix
        }
        

        Swift 3 更新:

        func longestCommonPrefix(strings:[String]) -> String {
        
            var commonPrefix = ""
            var indices = strings.map { $0.startIndex}
        
            outerLoop:
        
                while true {
                    var toMatch: Character = "_"
        
                    for (whichString, f) in strings.enumerated() {
        
                        let cursor = indices[whichString]
        
                        if cursor == f.endIndex {   break outerLoop     }
        
                        indices[whichString]  = f.characters.index(after: cursor)
        
                        if whichString == 0     {   toMatch = f[cursor] }
                        if toMatch != f[cursor] {   break outerLoop     }
                    }
        
                    commonPrefix.append(toMatch)
            }
        
            return commonPrefix
        }
        

        有趣的地方:

        1. 这在 O^2 或 O(n x m) 中运行,其中 n 是字符串数,m 是最短的长度。
        2. 这使用 String.Index 数据类型,因此处理Character 类型所代表的Grapheme Clusters

        考虑到我首先需要编写的函数:

        /// Takes an array of Strings representing file system objects absolute
        /// paths and turn it into a new array with the minimum number of common
        /// ancestors, possibly pushing the root of the tree as many level downwards
        /// as necessary
        ///
        /// In other words, we compute the longest common prefix and remove it
        
        func reify(fullPaths:[String]) -> [String] {
        
            let lcp = longestCommonPrefix(fullPaths)
        
            return fullPaths.map {
                return $0[lcp.endIndex ..< $0.endIndex]
            }
        }
        

        这是一个最小的单元测试:

        func testReifySimple() {
            let samplePaths:[String] = [
                "/root/some/file"
            ,   "/root/some/other/file"
            ,   "/root/another/file"
            ,   "/root/direct.file"
            ]
        
            let expectedPaths:[String] = [
                "some/file"
            ,   "some/other/file"
            ,   "another/file"
            ,   "direct.file"
            ]
        
            let reified = PathUtilities().reify(samplePaths)
        
            for (index, expected) in expectedPaths.enumerate(){
                XCTAssert(expected == reified[index], "failed match, \(expected) != \(reified[index])")
            }
        }
        

        【讨论】:

          【解决方案8】:

          也许是更直观的解决方案。将先前迭代中已找到的前缀作为输入字符串引导到剩余或下一个字符串输入。 [[[w1, w2], w3], w4]... so on],其中[] 应该是两个字符串的 LCP。

          public String findPrefixBetweenTwo(String A, String B){
              String ans = "";
              for (int i = 0, j = 0; i < A.length() && j < B.length(); i++, j++){
                  if (A.charAt(i) != B.charAt(j)){
                      return i > 0 ? A.substring(0, i) : "";
                  }
              }
              // Either of the string is prefix of another one OR they are same.
              return (A.length() > B.length()) ?  B.substring(0, B.length()) : A.substring(0, A.length());
          }
          public String longestCommonPrefix(ArrayList<String> A) {
              if (A.size() == 1) return A.get(0);
          
              String prefix = A.get(0);
              for (int i = 1; i < A.size(); i++){
                  prefix = findPrefixBetweenTwo(prefix, A.get(i)); // chain the earlier prefix 
              }
              return prefix;
          }
          

          【讨论】:

            猜你喜欢
            • 2011-10-21
            • 2018-05-07
            • 2019-05-03
            • 2017-03-19
            • 2021-09-11
            • 2020-12-12
            • 1970-01-01
            • 2021-02-14
            • 2020-11-13
            相关资源
            最近更新 更多