LeetCode_14 最长公共前缀(Longest Common Prefix) 暴力*2+分治+二分查找

原题

在一组给定的字符串中找到它们最长的公共前缀,如果没有公共前缀,返回空字符串。

来源:LeetCode
链接:https://leetcode.com/problems/longest-common-prefix/
 
 

解析

这题看似比较简单,解法却有很多,都比较有意义。

解法一:暴力纵向搜索

这题目乍一看的思路就很直接,暴力从第一个字符串的第一个字符开始,检查它是否在所有字符串中。也就是纵向的搜索。
代码如下:

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if(strs.size() == 0) //边界特殊情况都要想想到
            return "";
        string ans = "";
        char tmp;
        int j;
        for(int i = 0; i < strs[0].length(); ++i){
            tmp = strs[0][i];
            for(j = 1; j < strs.size(); ++j){
                if(strs[j].length() == i || strs[j][i] != tmp) //前面的条件要判出界
                    break;
            }
            if(j != strs.size())
                return ans;
            ans += tmp;
        }
        return ans;
    }
};

时间复杂度的估计:最坏情况下,所有字符串都是相同的,那么程序会把所有字母都扫一遍,否则在找到不是公共前缀的字母的时候就会停止。因此时间复杂度为O(S)(S是所有字符串中字母的总个数)。
考虑空间复杂度的话,其实上面的代码还不是最优,其实不需要另外再设置ans字符串来存放答案,可以在找到公共前缀的最后一个字符的下标之后,直接取原字符串的子串即可。这样的话,空间复杂度就是O(1)的。

运行情况:
Runtime: 4 ms
Memory Usage: 8.8 MB

 

解法二:暴力横向搜索

还有一种暴力的方式,就是横向搜索。先找到两个字符串之间的公共前缀,再求这个公共前缀和下一个字符串之间的公共前缀。

代码如下:

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if(strs.size() == 0)
            return "";
        string ans = strs[0];
        char tmp;
        int j;
        for(int i = 1; i < strs.size(); ++i){
            for(j = 0; j < ans.length(); ++j){
                if(strs[i].length() == j || strs[i][j] != ans[j]){
                    ans = ans.substr(0,j);
                    break;
                }
            }
        }
        return ans;
    }
};

时间复杂的的估计:最差情况下,算法依旧需要访问所有字符串中的所有字符,但是在平均情况下,因为它是横向扫描,所有纵向扫描会遍历的字符它都会遍历,还会扫到其它纵向不会扫到的字符。时间复杂度为O(S),和纵向一样,但是平均下来更花时间。
空间复杂度的话,这边其实同样可以做到O(1)的空间,只需每次都记录一个最后的下标。

运行情况:
Runtime: 8 ms
Memory Usage: 8.8 MB

 

解法三:分治

将字符串集分成两个大小几乎相等的子集,分别对两个子集求最长公共前缀,然后再求最终的答案。

代码如下:

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if(strs.size() == 0)
            return "";
        return divide_conquer(strs,0,strs.size()-1);
    }
    
    string divide_conquer(vector<string>& strs, int l, int r){
        if(l == r)
            return strs[l];
        else if(l == r-1){
            for(int i = 0; i < strs[l].length(); ++i){
                if(strs[r].length() == i || strs[r][i] != strs[l][i])
                    return strs[l].substr(0,i);
            }
            return strs[l];
        }
        int mid = (l+r)/2;
        string re1 = divide_conquer(strs,l,mid);
        string re2 = divide_conquer(strs,mid+1,r); //分治得到两个子集各自的最长公共前缀
        for(int i = 0; i < re1.length(); ++i){
            if(re2.length() == i || re2[i] != re1[i])
                return re1.substr(0,i);
        }
        return re1;
    }
};

时间复杂度分析:对于分治的问题,可以列递推方程式。
T ( n ) = 2 T ( n 2 ) + O ( l e n ) . T(n)=2T(\frac{n}{2})+O(len). T(n)=2T(2n)+O(len).
其中len表示最长的字符串长度。
可以解得 T ( n ) = O ( n ∗ l e n ) . T(n)=O(n*len). T(n)=O(nlen).因此,时间复杂度也为O(S)。

空间复杂度:同一时间,最多可能有 O ( l o g n ) O(logn) O(logn)个递归函数没有返回。每个递归函数需要的空间都是 O ( l e n ) O(len) O(len)。所以,空间复杂度为 O ( l e n ∗ l o g n ) . O(len*logn). O(lenlogn).

运行情况为:
Runtime: 8 ms
Memory Usage: 9.5 MB

可见,运行时间与横向的扫描差不多,而空间复杂度则较大。

 

解法四:二分查找(binary search)

二分查找是我觉得比较巧妙的一个思路,它的核心思想就是每次把搜索区间的长度减半。搜索的是什么呢?是最长公共前缀的最后一个字符的下标(也即结束处)。例如一开始的搜索区间为 [ l , r ] [l,r] [l,r],取 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r,如果所有字符串的子串 [ l . . . m i d ] [l...mid] [l...mid]是公共的,则接下来的搜索区间就减半成 [ m i d + 1 , r ] [mid+1,r] [mid+1,r];如果所有字符串的子串 [ l . . . m i d ] [l...mid] [l...mid]不是完全公共的,则接下来的搜索区间就减半成 [ l . . . m i d ] [l...mid] [l...mid]

代码如下:

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if(strs.size() == 0)
            return "";
        int l = 0;
        int r = strs[0].length()-1;
        int mid;
        string tmp;
        for(int i = 1; i < strs.size(); ++i){
            if(strs[i].length()-1 < r)
                r = strs[i].length()-1;
        } //得到最短的字符串长度,作为搜索区间的右边界
        int i;
        while(l <= r){ //以下是常见的二分查找的while循环
            mid = (l+r)/2;
            tmp = strs[0].substr(l,mid-l+1);
            for(i = 1; i < strs.size(); ++i){
                if(strs[i].substr(l,mid-l+1) != tmp)
                    break;
            }
            if(i != strs.size())
                r = mid-1;
            else
                l = mid+1;
        }
        return strs[0].substr(0,l);
    }
};

时间复杂度分析:与分治相同,也可以列出递推方程式。
T ( n ) = T ( n 2 ) + O ( N ∗ n 2 ) . T(n)=T(\frac{n}{2})+O(N*\frac{n}{2}). T(n)=T(2n)+O(N2n).
可得 T ( n ) = O ( N ∗ n ) T(n)=O(N*n) T(n)=O(Nn),其中 N N N是字符串的个数, n n n是最短字符串的长度。
也即时间复杂度为 O ( S ) O(S) O(S),事实上,一般比S要小。
(在leetcode的官方solution中,时间复杂度是 O ( S ∗ l o g n ) O(S*logn) O(Slogn),这个估计应该是太粗略了?)

空间复杂度也可以做到 O ( 1 ) . O(1). O(1).

运行情况为:
Runtime: 4 ms
Memory Usage: 9 MB

如有错误及不足,欢迎交流指正~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值