给定一个字符串数组,找出所有字符串共有的最长前缀,没有公共前缀就返回空字符串。直接的做法是"竖着扫":以第一个字符串为基准,逐列比较所有字符串的同一个位置,一旦某个字符串已经到头,或者这一列有不同的字符,前缀就到此为止。这道题的实现很短,但有一个边界很容易漏:其他字符串比第一个字符串短时,如果不先检查长度就去访问下标,会越界。本文实测漏掉这个检查会出什么错,给出五种语言的实现,并把逐列比较的过程做成分步演示。
完整题目见官方页面(链接在文末)。前几组基准用例和结果由 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
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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 的返回值一致。前两列所有字符串都相同,第三列出现了不同的字符。