n字符串的最长公共前缀

时间:2011-12-20 16:07:56

标签: algorithm

给定n个最大长度为m的字符串。我们怎样才能找到其中至少有两个字符串共享的最长公共前缀?

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

答案:fl

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

8 个答案:

答案 0 :(得分:14)

使用trie解决此问题的O(|S|*n)。 [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表示法而言],在最坏的情况下,所有字符串都是相同的 - 你需要阅读所有字符串才能知道它。

答案 1 :(得分:14)

为什么要使用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 :(得分:5)

我会对它们进行排序,你可以在n lg n时间内完成它们。那么具有共同前缀的任何字符串都将紧挨着彼此。事实上,您应该能够保留一个指针,指出您当前正在查看哪个索引,然后按计划进行快速计算。

答案 3 :(得分:1)

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

您可以通过一次传递,根据其首字母对每个字符串进行存储。

使用另一个传递,您可以根据其后一秒对每个存储桶进行排序。 (这称为基数排序,O(n*m),每次传递都为O(n)。)这为您提供了2的基线前缀。

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

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

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

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

答案 4 :(得分:1)

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);

}

答案 5 :(得分:1)

恰好可以扩展corsiKa描述的桶排序(基数排序),使得所有字符串最终被单独放置在桶中,并且此时,已知用于这种孤独字符串的LCP。此外,每根弦的光泽也是已知的;它比LCP长一个。存储桶排序实际上是后缀数组的构造,但只是部分如此。那些未执行的比较(如corsiKa所述)确实代表后缀字符串中未添加到后缀数组的那些部分。最后,这种方法不仅可以确定LCP和闪烁,还可以轻松找到字符串中不存在的子序列。

答案 6 :(得分:0)

因为世界显然是在斯威夫特乞求答案,所以这里是我的;)

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数据类型,因此处理Grapheme Clusters类型所代表的Character
  3. 考虑到我需要首先编写的功能:

    /// 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])")
        }
    }
    

答案 7 :(得分:0)

也许是更直观的解决方案。将早先迭代中已经找到的前缀作为输入字符串引导到其余或下一个字符串输入。 [[[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;
}