bigywater.tech
算法与数据结构 · LeetCode 14

LeetCode 14. 最长公共前缀

给定一个字符串数组,找出所有字符串共有的最长前缀,没有公共前缀就返回空字符串。直接的做法是"竖着扫":以第一个字符串为基准,逐列比较所有字符串的同一个位置,一旦某个字符串已经到头,或者这一列有不同的字符,前缀就到此为止。这道题的实现很短,但有一个边界很容易漏:其他字符串比第一个字符串短时,如果不先检查长度就去访问下标,会越界。本文实测漏掉这个检查会出什么错,给出五种语言的实现,并把逐列比较的过程做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
4 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。前几组基准用例和结果由 Python 标准库的 os.path.commonprefix 给出(这是一个独立的实现)。一共 12 组,覆盖:只有一个字符串、空串、一个字符串是另一个的前缀、没有公共前缀、全部相同、长度 200 的字符串只在最后一位不同:

真实实测 · 本机执行输出
输入 [["flower","flow","flight"]]
输出 "fl"
输入 [["dog","racecar","car"]]
输出 ""
输入 [[""]]
输出 ""
输入 [["a"]]
输出 "a"
输入 [["ab","a"]]
输出 "a"
输入 [["","b"]]
输出 ""

陷阱:其他字符串比第一个短

以第一个字符串为基准逐列比较时,如果写成 s[i] != first[i],而 s 比 first 短,s[i] 在 Python 里会抛出 IndexError,在其他语言里则是越界访问。必须先判断 i == len(s)(这个字符串已经到头了)。下面是缺少这个检查的写法与正确写法的真实对比:

真实实测 · 本机执行输出
# 没有检查其他字符串是否已经到头的写法
['ab', 'a']                     : 缺检查写法 -> 抛出 IndexError: string index out of range    solve -> 'a'
['', 'b']                       : 缺检查写法 -> ''    solve -> ''
['flower', 'flow', 'flight']    : 缺检查写法 -> 'fl'    solve -> 'fl'
['abc', 'abcd', 'ab']           : 缺检查写法 -> 抛出 IndexError: string index out of range    solve -> 'ab'

["ab", "a"] 和 ["abc", "abcd", "ab"] 都因为有字符串比第一个字符串短而抛出 IndexError。["", "b"] 不出错,是因为第一个字符串是空串,循环根本不会执行;官方的示例 ["flower", "flow", "flight"] 里每个字符串在出现不同字符之前都够长,所以也不出错——这个漏洞不会被官方示例暴露。

竖着扫:五种语言的实现

外层循环遍历第一个字符串的每个下标 i,内层循环检查其余字符串:如果某个字符串的长度就是 i,或者它的第 i 个字符与第一个字符串不同,就返回第一个字符串的前 i 个字符;全部检查完没有出问题,说明整个第一个字符串都是公共前缀。Python、Java、Go、JavaScript、C++ 五种语言都是真实编译运行的。

Python · 解法代码def solve(strs): first = strs[0] for i in range(len(first)): # 竖着扫:逐列比较所有字符串的第 i 个字符 for s in strs[1:]: if i == len(s) or s[i] != first[i]: # 某个字符串已经到头,或者这一列不同:公共前缀到 i 为止 return first[:i] return first
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[["flower","flow","flight"]]  →  "fl"
[["dog","racecar","car"]]  →  ""
[[""]]  →  ""
[["a"]]  →  "a"
[["ab","a"]]  →  "a"
[["","b"]]  →  ""
Java · 解法代码class Solution { public String longestCommonPrefix(String[] strs) { String first = strs[0]; for (int i = 0; i < first.length(); i++) { // 竖着扫:逐列比较所有字符串的第 i 个字符 for (int k = 1; k < strs.length; k++) { if (i == strs[k].length() || strs[k].charAt(i) != first.charAt(i)) return first.substring(0, i); // 某个字符串已到头,或这一列不同 } } return first; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[["flower","flow","flight"]]  →  "fl"
[["dog","racecar","car"]]  →  ""
[[""]]  →  ""
[["a"]]  →  "a"
[["ab","a"]]  →  "a"
[["","b"]]  →  ""
Go · 解法代码func longestCommonPrefix(strs []string) string { first := strs[0] for i := 0; i < len(first); i++ { // 竖着扫:逐列比较所有字符串的第 i 个字符 for _, s := range strs[1:] { if i == len(s) || s[i] != first[i] { // 某个字符串已到头,或这一列不同 return first[:i] } } } return first }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[["flower","flow","flight"]]  →  "fl"
[["dog","racecar","car"]]  →  ""
[[""]]  →  ""
[["a"]]  →  "a"
[["ab","a"]]  →  "a"
[["","b"]]  →  ""
JavaScript · 解法代码function longestCommonPrefix(strs) { const first = strs[0]; for (let i = 0; i < first.length; i++) { // 竖着扫:逐列比较所有字符串的第 i 个字符 for (let k = 1; k < strs.length; k++) { if (i === strs[k].length || strs[k][i] !== first[i]) return first.slice(0, i); // 某个字符串已到头,或这一列不同 } } return first; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[["flower","flow","flight"]]  →  "fl"
[["dog","racecar","car"]]  →  ""
[[""]]  →  ""
[["a"]]  →  "a"
[["ab","a"]]  →  "a"
[["","b"]]  →  ""
C++ · 解法代码#include <string> #include <vector> using namespace std; class Solution { public: string longestCommonPrefix(vector<string>& strs) { const string& first = strs[0]; for (int i = 0; i < (int)first.size(); i++) { // 竖着扫:逐列比较所有字符串的第 i 个字符 for (size_t k = 1; k < strs.size(); k++) { if (i == (int)strs[k].size() || strs[k][i] != first[i]) return first.substr(0, i); // 某个字符串已到头,或这一列不同 } } return first; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[["flower","flow","flight"]]  →  "fl"
[["dog","racecar","car"]]  →  ""
[[""]]  →  ""
[["a"]]  →  "a"
[["ab","a"]]  →  "a"
[["","b"]]  →  ""
真实实测 · 本机执行输出
共 12 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 12/12
  Java        通过 12/12
  Go          通过 12/12
  JavaScript  通过 12/12
  C++         通过 12/12

随机对拍

Python 版本另外和标准库的 os.path.commonprefix 做了随机对拍:

真实实测 · 本机执行输出
随机对拍 20000 组(1 到 6 个长度 0 到 8 的字符串,字母表只有 a、b(或只有 a),公共前缀经常很长,也经常有字符串是另一个的前缀),solve 与 brute 的结果全部一致。

另外四种语言只在上面 12 组固定用例上验证过,其中包含一个字符串是另一个的前缀和空串,没有做大规模随机对拍。

分步演示:一列一列比较

下面的演示用 ["flower", "flow", "flight"]。每一行是一个字符串,格子里的 · 表示这个字符串在这个位置已经到头。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示最终得到的前缀与 solve 的返回值一致。前两列所有字符串都相同,第三列出现了不同的字符。

最长公共前缀:strs = ['flower', 'flow', 'flight']
正在比较的这一列已确认相同的列不同的位置

    参考与说明

    • 官方题目页:LeetCode 14. 最长公共前缀。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:先排序、只比较第一个和最后一个字符串的做法;用二分在前缀长度上查找的做法;Trie 做法。
    京ICP备2026067502号-1京公网安备11010502063487号