LeetCode 14. 最长公共前缀
问题描述
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""
。
示例:
输入:strs = ["flower","flow","flight"]
输出:"fl"
输入:strs = ["dog","racecar","car"]
输出:""
解释:不存在公共前缀
算法思路
垂直扫描法:
- 边界处理:
- 空数组直接返回空字符串
- 单字符串数组返回该字符串本身
- 逐列比较:
- 以第一个字符串为基准,遍历其每个字符
- 对于每个字符位置,检查数组中其他字符串的相同位置
- 若出现字符不匹配或字符串长度不足,立即返回当前已匹配的前缀
- 提前终止:
- 当发现不匹配时立即终止扫描
- 若完全匹配第一个字符串,则返回该字符串
代码实现
class Solution {
/**
* 查找字符串数组的最长公共前缀
*
* @param strs 字符串数组
* @return 最长公共前缀
*/
public String longestCommonPrefix(String[] strs) {
// 空数组处理
if (strs == null || strs.length == 0) {
return "";
}
// 单字符串情况
if (strs.length == 1) {
return strs[0];
}
// 以第一个字符串为基准
String first = strs[0];
// 遍历第一个字符串的每个字符
for (int i = 0; i < first.length(); i++) {
char c = first.charAt(i);
// 检查后续字符串的相同位置
for (int j = 1; j < strs.length; j++) {
/*
* 终止条件:
* 1. 当前字符串长度不足
* 2. 字符不匹配
*/
if (i >= strs[j].length() || strs[j].charAt(i) != c) {
return first.substring(0, i);
}
}
}
// 完全匹配第一个字符串
return first;
}
}
算法分析
- 时间复杂度:O(S)
- S 是所有字符串字符总数
- 最坏情况:所有字符串完全相同 → 完整扫描第一个字符串
- 空间复杂度:O(1)
- 仅使用常数级别的额外空间
算法过程
输入:strs = ["flower","flow","flight"]
- 取第一个字符串
"flower"
为基准 - 第0列:
f
vsflow[0]=f
→ 匹配f
vsflight[0]=f
→ 匹配
- 第1列:
l
vsflow[1]=l
→ 匹配l
vsflight[1]=l
→ 匹配
- 第2列:
o
vsflow[2]=o
→ 匹配o
vsflight[2]=i
→ 不匹配
- 返回
"fl"
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
String[] strs1 = {"flower","flow","flight"};
System.out.println("Test 1: " + solution.longestCommonPrefix(strs1)); // "fl"
// 测试用例2:无公共前缀
String[] strs2 = {"dog","racecar","car"};
System.out.println("Test 2: " + solution.longestCommonPrefix(strs2)); // ""
// 测试用例3:完全匹配
String[] strs3 = {"apple","apple","apple"};
System.out.println("Test 3: " + solution.longestCommonPrefix(strs3)); // "apple"
// 测试用例4:部分匹配
String[] strs4 = {"abc","abd","ab"};
System.out.println("Test 4: " + solution.longestCommonPrefix(strs4)); // "ab"
// 测试用例5:空数组
String[] strs5 = {};
System.out.println("Test 5: " + solution.longestCommonPrefix(strs5)); // ""
// 测试用例6:包含空字符串
String[] strs6 = {"ab","","abc"};
System.out.println("Test 6: " + solution.longestCommonPrefix(strs6)); // ""
// 测试用例7:单元素数组
String[] strs7 = {"hello"};
System.out.println("Test 7: " + solution.longestCommonPrefix(strs7)); // "hello"
// 测试用例8:不同长度匹配
String[] strs8 = {"a","ab","abc"};
System.out.println("Test 8: " + solution.longestCommonPrefix(strs8)); // "a"
}
关键点
-
基准选择:
- 以第一个字符串为扫描基准
- 其他字符串只需与基准比较
-
终止条件:
i >= strs[j].length()
:当前字符串长度不足strs[j].charAt(i) != c
:字符不匹配
-
高效性:
- 发现不匹配立即终止扫描
- 避免不必要的后续比较
常见问题
-
为什么选择垂直扫描而不是水平扫描?
垂直扫描在发现不匹配时能立即终止,而水平扫描需要完整比较所有字符串,垂直扫描在平均情况下更高效。 -
如何处理不同长度的字符串?
通过i >= strs[j].length()
检查字符串长度,确保不越界访问。 -
算法是否依赖第一个字符串?
是,但这是合理的:- 公共前缀不可能比第一个字符串更长
- 若第一个字符串很短,扫描会提前终止
-
最坏情况下的时间复杂度?
当所有字符串完全相同时,需要扫描整个第一个字符串,时间复杂度为 O(n*m),其中 n 是数组长度,m 是第一个字符串长度。 -
能否用分治法或二分查找优化?
可以,但复杂度相同:// 二分查找实现 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()); int low = 1, high = minLen; while (low <= high) { int mid = (low + high) / 2; if (isCommonPrefix(strs, mid)) low = mid + 1; else high = mid - 1; } return strs[0].substring(0, (low + high) / 2); } private boolean isCommonPrefix(String[] strs, int len) { String prefix = strs[0].substring(0, len); for (int i = 1; i < strs.length; i++) if (!strs[i].startsWith(prefix)) return false; return true; }