LeetCode 14. 最长公共前缀:两种解法+优化思路全解析

0 阅读7分钟

在 LeetCode 简单题中,最长公共前缀(Longest Common Prefix)是一道经典的字符串处理题目。它看似简单,却能考察对字符串遍历、边界处理的基本功,同时衍生出多种优化算法。本文将从题目分析入手,拆解两种核心解法,补充易错点和场景适配建议,帮你彻底掌握这道题。

一、题目回顾

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""

示例

  • 输入:strs = ["flower","flow","flight"],输出:"fl"

  • 输入:strs = ["dog","racecar","car"],输出:""(无公共前缀)

  • 输入:strs = ["a"],输出:"a"(单元素数组直接返回自身)

核心考点

  1. 字符串遍历与字符对比;2. 边界条件处理(空数组、单元素数组、字符串长度不一致);3. 算法效率优化(减少无效对比次数)。

二、解法一:逐字符遍历法(暴力遍历优化版)

解法思路

以数组第一个字符串为基准,逐字符遍历其每个位置,同时检查其他所有字符串的对应位置是否相同:

  1. 遍历第一个字符串的每个字符(索引 j 从 0 开始);

  2. 对每个字符,检查数组中其他字符串的第 j 位是否与之相等,同时判断是否超出其他字符串的长度;

  3. 若所有字符串对应位置字符相同且无长度溢出,将该字符加入结果;若有任一不匹配或溢出,立即终止遍历并返回结果。

代码实现与注释


function longestCommonPrefix_1(strs: string[]): string {
  // 边界处理:空数组直接返回空字符串,避免访问 strs[0] 报错
  if (!strs || strs.length === 0) return "";
  // 单元素数组直接返回自身,无需遍历
  if (strs.length === 1) return strs[0];

  let res = "";
  // 以第一个字符串为基准,遍历每个字符
  for (let j = 0; j < strs[0].length; j++) {
    let isEqual = true; // 标记当前字符是否所有字符串都匹配
    let overLen = false; // 标记是否有字符串超出长度

    // 检查其他所有字符串的第 j 位
    for (let i = 1; i < strs.length; i++) {
      // 易错点:下标 j 等于字符串长度时已越界,需用 >= 而非 >
      if (j >= strs[i].length) {
        overLen = true;
        break;
      }
      // 字符不匹配,标记后终止内层循环
      if (strs[i][j] !== strs[0][j]) {
        isEqual = false;
        break;
      }
    }

    // 只有所有字符串匹配且无长度溢出,才追加字符
    if (isEqual && !overLen) {
      res += strs[0][j];
    } else {
      break; // 任一不满足,直接终止外层循环
    }
  }
  return res;
};

复杂度分析

  • 时间复杂度:O(mn),其中 m 是第一个字符串的长度,n 是字符串数组长度。最坏情况需遍历第一个字符串所有字符,且每个字符都要对比所有字符串。

  • 空间复杂度:O(1),仅用常数额外空间存储结果和标记变量(不计结果存储的空间)。

易错点修正

原代码中 j > strs[i].length 存在逻辑错误:字符串下标从 0 开始,当 j 等于字符串长度时,已超出字符串范围(如字符串长度为 3,下标最大为 2),需修正为 j >= strs[i].length,否则会出现越界报错。

解法二:二分查找法(高效优化版)

解法思路

最长公共前缀的长度必然在 [0, 最短字符串长度] 范围内(前缀不可能比最短字符串还长)。利用二分查找缩小这个长度范围,每次判断「当前长度的前缀是否为所有字符串的公共前缀」,逐步锁定最长有效长度:

  1. 先找到数组中最短字符串的长度,作为二分查找的右边界;

  2. 二分查找的左边界为 0,每次计算中间长度 mid

  3. 判断长度为 mid 的前缀是否为公共前缀:若是,尝试更长的前缀(左边界右移);若不是,尝试更短的前缀(右边界左移);

  4. 最终左边界(或右边界,二分结束后两者相等)即为最长公共前缀的长度,截取该长度的字符串返回。

代码实现与注释


function longestCommonPrefix_2(strs: string[]): string {
  // 边界处理:空数组返回空字符串
  if (!strs || strs.length === 0) return "";
  if (strs.length === 1) return strs[0];

  // 步骤1:找到最短字符串长度,作为二分查找的右边界
  let minLen = Infinity;
  for (const str of strs) {
    minLen = Math.min(minLen, str.length);
  }

  // 步骤2:二分查找最长公共前缀长度
  let low = 0, high = minLen;
  while (low < high) {
    // 易错点:向上取整,避免 low = high - 1 时死循环
    const mid = Math.floor((low + high + 1) / 2);
    if (isCommonPrefix(strs, mid)) {
      low = mid; // 长度 mid 有效,尝试更长的前缀
    } else {
      high = mid - 1; // 长度 mid 无效,尝试更短的前缀
    }
  }

  // 步骤3:截取最长公共前缀
  return strs[0].substring(0, low);
}

// 辅助函数:检查所有字符串是否有长度为 len 的公共前缀
function isCommonPrefix(strs: string[], len: number): boolean {
  // 取第一个字符串的前 len 个字符作为基准前缀
  const prefix = strs[0].substring(0, len);
  // 检查其他所有字符串是否以该前缀开头
  for (let i = 1; i < strs.length; i++) {
    if (!strs[i].startsWith(prefix)) {
      return false;
    }
  }
  return true;
}

复杂度分析

  • 时间复杂度:O(mn log m),其中 m 是最短字符串长度,n 是字符串数组长度。二分查找的次数为 log m,每次检查前缀需遍历所有字符串,对比 m 个字符。

  • 空间复杂度:O(1),仅用常数额外空间存储边界变量和基准前缀(辅助函数的前缀为临时变量,长度不超过m,可视为常数级)。

关键优化点

  1. 二分查找减少无效对比:相比逐字符遍历,对于超长字符串(如长度 1000+),二分查找仅需 10 次左右的长度判断,大幅减少对比次数;

  2. 向上取整避免死循环:若采用 mid = Math.floor((low + high)/2),当 low = high - 1 时,可能出现无限循环(如 low=2, high=3,mid=2,若有效则 low 仍为 2,无法退出循环),向上取整可直接锁定最终长度。

三、两种解法对比与场景适配

解法优势劣势适用场景
逐字符遍历逻辑简单、实现成本低,提前不匹配时可立即终止超长字符串场景下,对比次数较多常规场景、字符串长度较短、大概率提前出现不匹配
二分查找超长字符串场景下效率更高,对比次数稳定逻辑稍复杂,需额外实现辅助函数字符串长度长、字符串数组规模大

四、进阶拓展:其他优化思路

除了上述两种解法,还有两种常用思路可进一步优化:

1. 横向扫描法(逐字符串对比)

以第一个字符串为初始前缀,依次与后续字符串对比,不断缩短前缀长度(取当前前缀与下一个字符串的公共前缀),若前缀为空则直接返回。时间复杂度同样为 O(mn),但提前空前缀时终止更快,适合前缀较短的场景。

2. 排序法(利用字典序特性)

对字符串数组排序后,最长公共前缀必然是第一个和最后一个字符串的公共前缀(排序后首尾字符串差异最大)。仅需对比首尾两个字符串,代码极简,适合字符串数量多但长度短的场景,时间复杂度为 O(nm log n)(排序占主导)。

五、总结

LeetCode 14 题的核心是「边界处理」和「减少无效对比」:

  • 新手优先掌握逐字符遍历法,吃透边界处理(空数组、越界判断),夯实字符串操作基础;

  • 追求高效可选用二分查找法,尤其适合超长字符串场景,理解「二分思想」在字符串问题中的应用;

  • 实际开发中可根据字符串数组的规模和长度,选择最适配的解法(如多短字符串用排序法,长字符串用二分法)。

这道题的多种解法体现了「简单问题深挖掘」的特点,学好它能为后续更复杂的字符串处理题目打下基础。