最长公共前缀
1127 字
6 分钟
最长公共前缀
题目链接:14. 最长公共前缀 - 力扣(LeetCode)
- 难度:简单
- 标签:字典树、数组、字符串
题目描述
Note
原题说明:
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。
示例
- 输入:
strs = ["flower","flow","flight"] - 输出:
"fl"
提示
strs[i]仅由小写英文字母组成。
方案一:纵向扫描(我的初版)
核心思路: 从第一个字符串的第一个字符开始,依次与后面所有字符串的同一位置进行对比。一旦发现长度不够或者字符不匹配,立即返回。
源码实现
class Solution {public: string longestCommonPrefix(vector<string>& strs) { if(strs.empty()) return "";
for (int i = 0; i < strs[0].size(); ++i) { char c = strs[0][i]; for (int j = 1; j < strs.size(); ++j) { // 如果当前字符串长度不够,或者字符不匹配,就返回当前前缀 if (i >= strs[j].size() || strs[j][i] != c) { return strs[0].substr(0, i); } } } return strs[0]; }};复杂度分析
- 时间复杂度:。其中 是字符串数组中所有字符串的平均长度, 是字符串的数量。最坏情况下(所有字符串都完全相同),我们需要遍历所有字符。
- 空间复杂度:。只使用了常数级的额外空间。
方案二:横向扫描(逐步缩减)
核心思路: 假设第一个单词就是前缀,然后拿它不断去和后面的单词做匹配,不匹配就砍掉最后一个字符,直到匹配为止。
class Solution {public: string longestCommonPrefix(vector<string>& strs) { if (strs.empty()) return ""; string prefix = strs[0]; for (size_t i = 1; i < strs.size(); ++i) { // rfind(..., 0) == 0 表示 strs[i] 以 prefix 开头 while (strs[i].rfind(prefix, 0) != 0) { prefix.pop_back(); // 砍掉最后一个字符 if (prefix.empty()) return ""; } } return prefix; }};复杂度分析
- 时间复杂度:。 是所有字符串中字符数量的总和。
- 空间复杂度:。
方案三:排序比对(巧妙解法)
核心思路: 将数组按字典序排序。排序后,只需要对比第一个单词和最后一个单词的公共前缀即可。因为这两个单词是差异最大的,如果它们有共同点,那么中间的所有单词一定也包含这个前缀。
#include <algorithm>
class Solution {public: string longestCommonPrefix(vector<string>& strs) { if (strs.empty()) return ""; // 字典序排序 sort(strs.begin(), strs.end()); const string &first = strs.front(), &last = strs.back();
int i = 0; int n = min(first.size(), last.size()); while (i < n && first[i] == last[i]) ++i;
return first.substr(0, i); }};复杂度分析
- 时间复杂度:。其中 是单词数, 是平均长度。排序需要的时间开销。
- 空间复杂度:。用于存储排序相关的开销。
方案四:字典树(Trie Tree)
核心思路:
将所有单词存入字典树,记录每个节点的 pass(经过该节点的单词数)。从根节点开始向下找,只要当前节点满足“只有一个子节点”且“子节点的 pass == 总单词数”,它就是公共前缀的一部分。
源码实现
struct TrieNode { TrieNode* next[26] = {nullptr}; int pass = 0; // 经过该节点的单词数};
class Solution {public: string longestCommonPrefix(vector<string>& strs) { if (strs.empty()) return ""; TrieNode* root = new TrieNode(); const int n = strs.size();
// 1. 建树 for (auto &s : strs) { TrieNode* p = root; for (char c : s) { int idx = c - 'a'; if (!p->next[idx]) p->next[idx] = new TrieNode(); p = p->next[idx]; p->pass++; } }
// 2. 遍历找最长唯一链 string ans; TrieNode* p = root; while (true) { int cnt = 0, idx = -1; for (int i = 0; i < 26; ++i) { if (p->next[i]) { cnt++; idx = i; } } // 要求:只有一个分支,且该分支被所有单词经过 if (cnt != 1 || p->next[idx]->pass != n) break; ans += char('a' + idx); p = p->next[idx]; } return ans; }};复杂度分析
- 时间复杂度:。 是所有字符的总数。
- 空间复杂度:。 是字符集大小(26个字母)。
总结
- 简单题也有深意:纵向扫描是最容易想到的。
- 排序大法好:字符串排序比对是一种非常巧妙的思维跃迁。
- 字典树:虽然在这题中略显笨重(空间开销大),但在处理海量数据的各种前缀问题时它是真正的神器。
Tip
如果字符串数量不多但都很长,排序法非常高效;如果你对字典树这种高级数据结构感兴趣,这道题是一个极好的入门练习。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
最后更新于 2025-10-04,距今已过 207 天
部分内容可能已过时
相关文章 智能推荐
1
最后一个单词的长度
算法 力扣第58题:最后一个单词的长度。从反向遍历的直观思想到双指针的精准定位,掌握字符串处理的边界细节。
2
有效的括号
算法 力扣第20题:有效的括号。从“回文”误区到栈(Stack)的经典应用,深入理解括号匹配的 LIFO 逻辑。
3
罗马数字转整数
算法 力扣第13题:罗马数字转整数。探索如何处理特殊减法规则,并对比函数映射与哈希表两种实现方式。
4
回文数
算法 力扣第9题:回文数的判断。从暴力取余到“反转一半”的思路演进,以及字符串双指针解法。
5
删除排序链表中的重复元素
算法 力扣第83题:删除排序链表中的重复元素。深入理解链表指针操作,掌握“跳过”重复节点的优雅技巧。
随机文章 随机推荐