携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情
最长公共前缀
题目描述:
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例1:
输入: strs = ["flower","flow","flight"]
输出: "fl"
示例2:
输入: strs = ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
解题思路
思路一:粗暴循环比较法
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function (strs) {
if (strs.length <= 1) {
return strs[0];
}
var prev = strs[0];
var prevLen = prev.length,
len = strs.length;
for (var i = 1; i < len; i++) {
for (var j = 0; j < prevLen && j < strs[i].length; j++) {
if (prev[j] !== strs[i][j]) {
break;
}
}
prev = prev.substring(0, j);
if (prev === "") {
return "";
}
}
return prev;
};
时间复杂度: O(mn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次
空间复杂度: O(1)
思路二:仅需最大、最小字符串的最大公共前缀
获取数组中的最大值及最小值字符串(字符串比较是根据字符的 Unicode 编码大小逐位比较字符串大小, 可以看JavaScript中字符串的比较规则),最小值字符串与最大字符串的最长公共前缀也为其他字符串的公共前缀,即为字符串数组的最长公共前缀。
var longestCommonPrefix = function (strs) {
if (strs.length <= 1) {
return strs[0];
}
var min = 0,
max = 0;
for (var i = 1; i < strs.length; i++) {
if (strs[min] > strs[i]) min = i;
if (strs[max] < strs[i]) max = i;
}
for (var j = 0, len = strs[min].length; j < len; j++) {
if (strs[min][j] !== strs[max][j]) {
return strs[min].substring(0, j);
}
}
return strs[min];
};
时间复杂度: O(m + n) , 其中 m 是字符串数组中的最小字符串的长度,n 是数组长度。
空间复杂度: O(1)
思路三: 分治
可以将多个字符串的最长公共前缀分解成多个相似的子问题:求两个字符串的最长公共前缀;
LCP(S1, S2, …, Sn) = LCP(LCP(S1, Sk), LCP(Sk+1, Sn))
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function (strs) {
if (strs.length <= 1) {
return strs[0];
}
return longestCommonPrefix(strs);
};
// 分裂后数组长度为1,进行比较最长公共前缀
var longestCommonPrefix = function (arr) {
var len = arr.length;
if (len === 1) {
return arr[0];
}
var mid = Math.floor(len / 2);
var lcpLeft = arr.slice(0, mid);
var lcpRight = arr.slice(mid, len);
return commonPrefix(
longestCommonPrefix(lcpLeft),
longestCommonPrefix(lcpRight)
);
};
// 获取两字符串的最长公共前缀
var commonPrefix = function (lcpLeft, lcpRight) {
var minLength = Math.min(lcpLeft.length, lcpRight.length);
for (var i = 0; i < minLength; i++) {
if (lcpLeft[i] != lcpRight[i]) {
return lcpLeft.substring(0, i);
}
}
return lcpLeft.substring(0, minLength);
};
时间复杂度:O(mn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。时间复杂度的递推式是 T(n)=2 * T(n / 2) + O(m),通过计算可得 T(n)=O(mn)
空间复杂度:O(mlogn),空间复杂度主要取决于递归调用的层数,层数最大为 nlogn,每层需要 m 的空间存储返回结果
思路四: 利用Trie树(字典树)
Trie树, 字典树或前缀树,顾名思义,它是用来处理字符串匹配问题的数据结构,以及用来解决集合中查找固定前缀字符串的数据结构。
解题思路: 构建一个 Trie 树,字符串数组的最长公共序列就为从根节点开始遍历树,直到:
- 遍历节点存在超过一个子节点的节点
- 或遍历节点为一个字符串的结束字符
为止,走过的字符为字符串数组的最长公共前缀
var longestCommonPrefix = function (strs) {
if (strs.length <= 1) {
return strs[0];
}
// 初始化Trie树
var trie = new Trie();
// 构建Trie树
for (var i = 0; i< strs.length; i++) {
if (!trie.insert(strs[i])) {
return '';
}
}
return trie.searchLongestPrefix();
};
// Trie树
var Trie = function () {
this.root = new TrieNode();
}
var TrieNode = function () {
// next 放入当前节点的子节点
this.next = {};
// 当前是否是结束节点
this.isEnd = false;
}
Trie.prototype.insert = function (word) {
if (!word) {
return false;
}
var node = this.root;
for (var i = 0; i < word.length; i++) {
var key = word[i]
if (!node.next[key]) {
node.next[key] = new TrieNode();
}
node = node.next[key];
}
node.isEnd = true;
return true;
}
// 搜索最长公共前缀
Trie.prototype.searchLongestPrefix = function () {
var node = this.root, prefix = '';
while (node.next) {
var keys = Object.keys(node.next);
var key = keys[0];
if (keys.length !== 1) break; // 说明节点不一样了
if (node.next[key].isEnd) { // 到头了
prefix += key;
break;
}
prefix += key;
node = node.next[key];
}
return prefix;
}
时间复杂度:O(s+m),s 是所有字符串中字符数量的总和,m为字符串数组中最长字符的长度,构建 Trie 树需要 O(s) ,最长公共前缀查询操作的复杂度为 O(m)
空间复杂度:O(s),用于构建 Trie 树