在 LeetCode 简单题中,最长公共前缀(Longest Common Prefix)是一道经典的字符串处理题目。它看似简单,却能考察对字符串遍历、边界处理的基本功,同时衍生出多种优化算法。本文将从题目分析入手,拆解两种核心解法,补充易错点和场景适配建议,帮你彻底掌握这道题。
一、题目回顾
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。
示例
-
输入:
strs = ["flower","flow","flight"],输出:"fl" -
输入:
strs = ["dog","racecar","car"],输出:""(无公共前缀) -
输入:
strs = ["a"],输出:"a"(单元素数组直接返回自身)
核心考点
- 字符串遍历与字符对比;2. 边界条件处理(空数组、单元素数组、字符串长度不一致);3. 算法效率优化(减少无效对比次数)。
二、解法一:逐字符遍历法(暴力遍历优化版)
解法思路
以数组第一个字符串为基准,逐字符遍历其每个位置,同时检查其他所有字符串的对应位置是否相同:
-
遍历第一个字符串的每个字符(索引
j从 0 开始); -
对每个字符,检查数组中其他字符串的第
j位是否与之相等,同时判断是否超出其他字符串的长度; -
若所有字符串对应位置字符相同且无长度溢出,将该字符加入结果;若有任一不匹配或溢出,立即终止遍历并返回结果。
代码实现与注释
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, 最短字符串长度] 范围内(前缀不可能比最短字符串还长)。利用二分查找缩小这个长度范围,每次判断「当前长度的前缀是否为所有字符串的公共前缀」,逐步锁定最长有效长度:
-
先找到数组中最短字符串的长度,作为二分查找的右边界;
-
二分查找的左边界为 0,每次计算中间长度
mid; -
判断长度为
mid的前缀是否为公共前缀:若是,尝试更长的前缀(左边界右移);若不是,尝试更短的前缀(右边界左移); -
最终左边界(或右边界,二分结束后两者相等)即为最长公共前缀的长度,截取该长度的字符串返回。
代码实现与注释
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,可视为常数级)。
关键优化点
-
二分查找减少无效对比:相比逐字符遍历,对于超长字符串(如长度 1000+),二分查找仅需 10 次左右的长度判断,大幅减少对比次数;
-
向上取整避免死循环:若采用
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 题的核心是「边界处理」和「减少无效对比」:
-
新手优先掌握逐字符遍历法,吃透边界处理(空数组、越界判断),夯实字符串操作基础;
-
追求高效可选用二分查找法,尤其适合超长字符串场景,理解「二分思想」在字符串问题中的应用;
-
实际开发中可根据字符串数组的规模和长度,选择最适配的解法(如多短字符串用排序法,长字符串用二分法)。
这道题的多种解法体现了「简单问题深挖掘」的特点,学好它能为后续更复杂的字符串处理题目打下基础。