力扣解题-14. 最长公共前缀

5 阅读7分钟

力扣解题-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. 以第一个字符串为参照(比如示例1的"flower"),从第0位开始,逐个取出字符;
  2. 检查数组中其他所有字符串的相同位置,是否有这个字符;
  3. 如果所有字符串该位置都匹配,就把这个字符加入前缀;只要有一个不匹配/字符串长度不足,就停止验证,返回已匹配的前缀。
具体步骤
  1. 边界处理:若数组为空返回"",若只有1个字符串直接返回该字符串;
  2. 基准字符串:取第一个字符串firstStr作为比较基准;
  3. 逐字符验证
    • 遍历firstStr的每一个字符位置m(从0开始);
    • 截取firstStr的第m位字符(substring(m, m+1))作为待比较字符;
    • 遍历数组中其他字符串,统计“该位置字符与待比较字符匹配”的字符串数量(sameNum);
    • sameNum等于“其他字符串总数”(strs.length-1),说明该字符是公共前缀的一部分,拼接到maxPrefix
    • 若不匹配/某字符串长度不足,直接终止循环;
  4. 返回结果:返回拼接好的maxPrefix
性能损耗分析
  • 时间复杂度:O(m×n)(m为基准字符串长度,n为数组长度),每个字符都要遍历所有字符串;
  • 空间复杂度:O(m)(存储拼接的前缀字符串);
  • 核心损耗点:
    1. substring(m, m+1)会生成新的字符串对象,频繁创建小对象导致内存和时间开销;
    2. 用字符串拼接(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替代字符串拼接,大幅提升性能,是更优的逐位验证解法。

核心优化点拆解
  1. 字符获取优化
    • 第一次解答用substring(m, m+1)截取字符,会生成新字符串;
    • 第二次解答用charAt(m)直接获取字符(返回char类型),无对象创建开销,速度提升显著;
  2. 前缀拼接优化
    • 第一次解答用maxPrefix+compareStr拼接,每次拼接生成新字符串;
    • 第二次解答用StringBuilder.append(),仅在内部扩容数组,无频繁对象创建;
  3. 逻辑核心不变:仍以第一个字符串为基准,逐位验证其他字符串的匹配情况。
性能提升说明
  • 时间复杂度:仍为O(m×n),但charAtStringBuilder的常数因子大幅降低;
  • 空间复杂度: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. 初始前缀为第一个字符串(比如示例1的"flower");
  2. 拿这个前缀与第二个字符串比较,找到两者的公共前缀("flower"和"flow"的公共前缀是"flow");
  3. 再拿新的前缀与第三个字符串比较,找到公共前缀("flow"和"flight"的公共前缀是"fl");
  4. 若前缀缩短为空,直接返回"";遍历完所有字符串后,剩余前缀就是答案。
代码实现
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),但无效操作最少。

总结

  1. 第一次解答:逐字符截取比较,逻辑直观但substring和字符串拼接导致性能差;
  2. 第二次解答:优化字符获取和拼接方式,性能大幅提升,是新手易掌握的高效解法;
  3. 横向扫描法:逐字符串缩短前缀,适合字符串差异靠前的场景,能快速终止;
  4. 纵向扫描法(最优):提前找最短字符串,逐位验证无无效遍历,是本题的最优写法;
  5. 关键优化技巧:
    • 避免频繁创建字符串对象(用charAt替代substring,用StringBuilder替代拼接);
    • 提前处理边界(最短字符串、空数组等),减少无效遍历。
  6. 还可是使用分治,二分查找