原题
在一组给定的字符串中找到它们最长的公共前缀,如果没有公共前缀,返回空字符串。
来源: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(n∗len).因此,时间复杂度也为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(len∗logn).
运行情况为:
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(N∗2n).
可得
T
(
n
)
=
O
(
N
∗
n
)
T(n)=O(N*n)
T(n)=O(N∗n),其中
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(S∗logn),这个估计应该是太粗略了?)
空间复杂度也可以做到 O ( 1 ) . O(1). O(1).
运行情况为:
Runtime: 4 ms
Memory Usage: 9 MB
如有错误及不足,欢迎交流指正~