力扣解题-14. 最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
提示:
1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] 如果非空,则仅由小写英文字母组成
Related Topics
字典树、数组、字符串
第一次解答
解题思路
核心方法:逐字符截取比较法,以第一个字符串为基准,逐个截取其字符,与数组中其他字符串对应位置的字符逐一比较,统计匹配的字符数,最终拼接出最长公共前缀。逻辑直观但存在性能损耗,适合理解核心思路。
核心逻辑拆解(通俗版)
要找所有字符串的公共前缀,核心是“逐位验证”:
- 以第一个字符串为参照(比如示例1的"flower"),从第0位开始,逐个取出字符;
- 检查数组中其他所有字符串的相同位置,是否有这个字符;
- 如果所有字符串该位置都匹配,就把这个字符加入前缀;只要有一个不匹配/字符串长度不足,就停止验证,返回已匹配的前缀。
具体步骤
- 边界处理:若数组为空返回"",若只有1个字符串直接返回该字符串;
- 基准字符串:取第一个字符串
firstStr作为比较基准; - 逐字符验证:
- 遍历
firstStr的每一个字符位置m(从0开始); - 截取
firstStr的第m位字符(substring(m, m+1))作为待比较字符; - 遍历数组中其他字符串,统计“该位置字符与待比较字符匹配”的字符串数量(
sameNum); - 若
sameNum等于“其他字符串总数”(strs.length-1),说明该字符是公共前缀的一部分,拼接到maxPrefix; - 若不匹配/某字符串长度不足,直接终止循环;
- 遍历
- 返回结果:返回拼接好的
maxPrefix。
性能损耗分析
- 时间复杂度:O(m×n)(m为基准字符串长度,n为数组长度),每个字符都要遍历所有字符串;
- 空间复杂度:O(m)(存储拼接的前缀字符串);
- 核心损耗点:
substring(m, m+1)会生成新的字符串对象,频繁创建小对象导致内存和时间开销;- 用字符串拼接(
maxPrefix+compareStr)会不断生成新字符串,效率低;
- 性能表现:耗时3ms仅击败12.38%用户,内存44.1MB击败5.00%用户,正是上述损耗导致。
public String longestCommonPrefix(String[] strs) {
if(strs.length<1){
return "";
}
if(strs.length==1){
return strs[0];
}
String firstStr=strs[0];
String maxPrefix="";
for(int m=0;m<firstStr.length();m++){
String compareStr=firstStr.substring(m,m+1);
int sameNum=0;
for (int i = 1; i < strs.length; i++) {
String str=strs[i];
if(str.length()==0 || m>=str.length()){
break;
}
if(compareStr.equals(str.substring(m,m+1))){
sameNum++;
}
}
if(sameNum==strs.length-1){
maxPrefix=maxPrefix+compareStr;
}else {
break;
}
}
return maxPrefix;
}
第二次解答
解题思路
核心方法:逐字符直接比较法,在第一次解答的基础上优化两个核心损耗点——用charAt替代substring、用StringBuilder替代字符串拼接,大幅提升性能,是更优的逐位验证解法。
核心优化点拆解
- 字符获取优化:
- 第一次解答用
substring(m, m+1)截取字符,会生成新字符串; - 第二次解答用
charAt(m)直接获取字符(返回char类型),无对象创建开销,速度提升显著;
- 第一次解答用
- 前缀拼接优化:
- 第一次解答用
maxPrefix+compareStr拼接,每次拼接生成新字符串; - 第二次解答用
StringBuilder.append(),仅在内部扩容数组,无频繁对象创建;
- 第一次解答用
- 逻辑核心不变:仍以第一个字符串为基准,逐位验证其他字符串的匹配情况。
性能提升说明
- 时间复杂度:仍为O(m×n),但
charAt和StringBuilder的常数因子大幅降低; - 空间复杂度:O(m)(
StringBuilder的底层数组),但内存使用更高效; - 性能表现:耗时1ms击败77.04%用户,内存42.7MB击败28.40%用户,优化效果显著。
public String longestCommonPrefix(String[] strs) {
if(strs.length<1){
return "";
}
if(strs.length==1){
return strs[0];
}
String firstStr=strs[0];
StringBuilder maxPrefix=new StringBuilder();
for(int m=0;m<firstStr.length();m++){
char compareStr=firstStr.charAt(m);
int sameNum=0;
for (int i = 1; i < strs.length; i++) {
String str=strs[i];
if(str.length()==0 || m>=str.length()){
break;
}
if(compareStr==str.charAt(m)){
sameNum++;
}
}
if(sameNum==strs.length-1){
maxPrefix=maxPrefix.append(compareStr);
}else {
break;
}
}
return maxPrefix.toString();
}
示例解答
解题思路
解法1:横向扫描法(更高效的逐字符串比较)
核心方法:以当前公共前缀为基准,逐字符串缩短前缀,先取第一个字符串为初始前缀,再依次与后续字符串比较,不断缩短前缀至所有字符串都匹配,时间复杂度仍为O(m×n),但实际遍历次数更少。
核心逻辑拆解(通俗版)
横向扫描的核心是“逐步缩小前缀范围”:
- 初始前缀为第一个字符串(比如示例1的"flower");
- 拿这个前缀与第二个字符串比较,找到两者的公共前缀("flower"和"flow"的公共前缀是"flow");
- 再拿新的前缀与第三个字符串比较,找到公共前缀("flow"和"flight"的公共前缀是"fl");
- 若前缀缩短为空,直接返回"";遍历完所有字符串后,剩余前缀就是答案。
代码实现
public String longestCommonPrefix(String[] strs) {
// 边界处理
if (strs == null || strs.length == 0) {
return "";
}
// 初始前缀为第一个字符串
String prefix = strs[0];
// 遍历后续字符串
for (int i = 1; i < strs.length; i++) {
// 找到当前前缀与第i个字符串的公共前缀
while (strs[i].indexOf(prefix) != 0) {
// 缩短前缀(去掉最后一个字符)
prefix = prefix.substring(0, prefix.length() - 1);
// 前缀为空,直接返回
if (prefix.isEmpty()) {
return "";
}
}
}
return prefix;
}
优势说明
- 实际遍历次数更少:若某一步前缀缩短为空,可直接终止,无需遍历所有字符;
- 逻辑更简洁:利用
indexOf(prefix) == 0判断前缀是否匹配(若前缀是strs[i]的开头,返回0); - 性能更稳定:在字符串差异出现在前几位的场景下(如示例2),能快速返回空字符串。
解法2:纵向扫描法(最优逐位验证,代码简化版)
核心方法:优化版逐位验证,提前找到最短字符串(公共前缀长度不可能超过最短字符串),减少无效遍历,是逐位验证的最优写法。
代码实现
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}
// 找到最短字符串的长度(公共前缀最长不超过该长度)
int minLen = Integer.MAX_VALUE;
for (String str : strs) {
minLen = Math.min(minLen, str.length());
}
// 逐位验证
for (int i = 0; i < minLen; i++) {
char c = strs[0].charAt(i);
for (int j = 1; j < strs.length; j++) {
if (strs[j].charAt(i) != c) {
// 不匹配,返回前i个字符
return strs[0].substring(0, i);
}
}
}
// 所有位都匹配,返回最短字符串的前minLen位
return strs[0].substring(0, minLen);
}
优势说明
- 减少无效遍历:提前找到最短字符串长度,避免遍历到超出最短字符串的位置;
- 代码更简洁:无需统计匹配数,只要有一个字符不匹配就立即返回前缀;
- 性能最优:逐位验证的极致写法,时间复杂度O(m×n),但无效操作最少。
总结
- 第一次解答:逐字符截取比较,逻辑直观但
substring和字符串拼接导致性能差; - 第二次解答:优化字符获取和拼接方式,性能大幅提升,是新手易掌握的高效解法;
- 横向扫描法:逐字符串缩短前缀,适合字符串差异靠前的场景,能快速终止;
- 纵向扫描法(最优):提前找最短字符串,逐位验证无无效遍历,是本题的最优写法;
- 关键优化技巧:
- 避免频繁创建字符串对象(用
charAt替代substring,用StringBuilder替代拼接); - 提前处理边界(最短字符串、空数组等),减少无效遍历。
- 避免频繁创建字符串对象(用
- 还可是使用分治,二分查找