【LeetCode】最长公共前缀

216 阅读4分钟

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

题目链接

我的最初解答版本

思路是以第一个字符串为比较对象,从1个字符开始逐个累加字符、构成试探前缀(tryPrefix),然后判断其余待比较的字符串是否以tryPrefix为前缀,一轮比较下来如果都是的话则将此试探前缀视为已经确定的安全前缀(prefix),一旦过程中出现否定的结果则比较结束,返回当前最新的安全前缀。

复杂度O(mn),m为字符串的平均长度、n为字符串个数。startsWith操作在这里视为O(1)的。

实际执行时间10ms

function longestCommonPrefix(strs: string[]): string {
    let prefix = '';
    let tryPrefix = '';
    for (let i = 0; i < strs[0].length; i++) {
        let isCommon = true;
        tryPrefix = strs[0].slice(0, i + 1);
        for (let j = 1; j < strs.length; j++) {
            if (!strs[j].startsWith(tryPrefix)) {
                isCommon = false;
                break;
            }
        }
        if (!isCommon) {
            break;
        } else {
            prefix = tryPrefix;
        }
    }
    return prefix;
};

题解方法1:减而治之-横向扫描

这个方法的关键是找到如下的递推关系,先以3个字符串为例:

LCP(s1,s2,s3)=LCP(LCP(s1,s2),s3)LCP(s_1, s_2, s_3) = LCP(LCP(s_1, s_2), s_3)

推广到n,得到如下结果

LCP(s1,s2,s3...,sn)=LCP(LCP(LCP(s1,s2),s3),...,sn)LCP(s_1, s_2, s_3 ..., s_n) = LCP(LCP(LCP(s_1, s_2), s_3), ..., s_n)

有一点“结合律”的感觉,也有函数式编程中的“柯里化”和“复合”的意思。

柯里化的概念

You can call a function with fewer arguments than it expects. It returns a function that takes the remaining arguments. -- github.com/MostlyAdequ…

调用函数时不一次性将所有参数都传入,而是只传一部分,然后柯里化后的函数会返回一个新的函数,它将接收剩余函数。

复合(compose)的概念

const compose2 = (f, g) => x => f(g(x));

注意compose的方向是从右到左等于从内到外。

代码如下,复杂度O(mn),实际执行时间4ms

function longestCommonPrefix(strs: string[]): string {
    if (strs.length === 0) return '';
    let prefix = strs[0];
    let count = strs.length;
    for (let i = 0; i < count; i++) {
        // lcp(s1, s2, s3) = lcp(lcp(s1, s2), s3) 这个有点compose或者结合律的感觉
        // lcp(s1...sn, sn+1) = lcp(lcp(sn-1, sn), sn+1)
        prefix = lcp(prefix, strs[i]);
        if (!prefix) {
            break;
        }
    }
    return prefix;
};

function lcp(s1, s2) {
    const max = Math.min(s1.length, s2.length);
    let res = '';
    for (let i = 0; i < max; i++) {
        if (s1[i] === s2[i]) {
            res += s1[i];
        } else {
            break;
        }
    }
    return res;
}

方法2:纵向扫描法

这个方法比较直观,将所有字符串一行一行地排列下来,然后纵向检查每个位置上的字符是否相等。

复杂度也是O(mn),但是跑下来是9ms

function longestCommonPrefix(strs: string[]): string {
    if (strs.length === 0) return '';
    let prefix = '';
    for (let i = 0; i < strs[0].length; i++) {
        const char = strs[0][i];
        prefix = strs[0].slice(0, i);
        let isCommon = true;
        for (let j = 0; j < strs.length; j++) {
            if (strs[j][i] !== char) {
                isCommon = false;
                break;
            }
        }
        if (isCommon) {
            prefix = strs[0].slice(0, i + 1);
        } else {
            break;
        }
    }
    return prefix;
};

方法3:分而治之

这个方法和减而治之有点类似,只是它的递推公式变成了

LCP(s1,s2,s3,s4)=LCP(LCP(s1,s2),LCP(s3,s4))LCP(s_1, s_2, s_3, s_4) = LCP(LCP(s_1, s_2), LCP(s_3, s_4))

即每次分割时取的是重点而非端点。

复杂度一样是O(mn),但是实际执行时间是3ms

function longestCommonPrefix(strs: string[]): string {
    if (strs.length === 0) return '';
    return lcp(0, strs.length - 1, strs);
}

// 这里的区间是[start, mid), [mid, end]
function lcp(start, end, strs: string[]) {
    if (start === end) return strs[start];

    const mid = Math.floor((start + end) / 2);
    const left = lcp(start, mid, strs);
    const right = lcp(mid + 1, end, strs);
    const minLength = Math.min(left.length, right.length);
    for (let i = 0; i < minLength; i++) {
        if (left[i] !== right[i]) {
            return left.slice(0, i);
        }
    }
    return left.slice(0, minLength);
}

方法4:二分查找

这个方法的理论上的复杂度是O(mnlogm),比前面三个都大,所以这里强行加了个二分查找有点牵强。但是实际执行时间居然是0ms,奇怪,可能测试数据集具有某种偏好。

function longestCommonPrefix(strs: string[]): string {
    if (!strs.length) return '';

    const isCommonPrefix = (length) => {
        const str0 = strs[0].slice(0, length);
        const count = strs.length;
        for (let i = 1; i < count; i++) {
            if (!(strs[i].slice(0, length) === str0)) {
                return false;
            }
        }
        return true;
    }
    let minLength = strs[0].length;
    for (let i = 1; i < strs.length; i++) {
        minLength = Math.min(minLength, strs[i].length);
    }
    let low = 0;
    let high = minLength;
    while (low < high) {
        const mid = Math.floor((high - low + 1) / 2) + low;
        if (isCommonPrefix(mid)) {
            low = mid;
        } else {
            high = mid - 1;
        }
    }
    return strs[0].slice(0, low);
}

方法5:排序后求最短和最长的公共前缀

这个方法是在评论区看到的,其实这个方法是有条件的,即字符串之间是有序关系、可以排序的。

复杂度我认为是O(nlogn),实际运行时间0ms

function longestCommonPrefix(strs: string[]): string {
    if (!strs.length) return '';
    
    const sorted = strs.sort();
    const str0 = sorted[0];
    const strn = sorted[sorted.length - 1];
    let res = '';
    for (let i = 0; i < str0.length; i++) {
        if (str0[i] === strn[i]) {
            res += str0[i];
        } else {
            break;
        }
    }
    return res;
}

总结

我的方法和方法一、二属于较为容易想到的暴力方法,理论上的复杂度也已经达到了比较好的状态。官方题解中的方法三、四有点强行使用分治和二分查找的感觉,理论复杂度都有所增加,不过实际运行结果很好,所以现实应用中或许具有一定的价值。评论中大家给出的方法五是有一定的条件的,但是理论复杂度和实际运行时间都很好,在实现上也非常简明。

版权声明

本文为博主原创文章,首发于CSDN,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:blog.csdn.net/nameofacity…