查看原文
其他

370,最长公共子串和子序列

山大王wld 数据结构和算法 2022-05-01

1,最长公共子串

假如有两个字符串,s1="people"和s2="eplm",我们要求他俩最长的公共子串。我们一眼就能看出他们的最长公共子串是"pl",长度是2。但如果字符串特别长的话就不容易那么观察了。


1,暴力求解:暴力求解对于字符串比较短的我们还可以接受,如果字符串太长实在是效率太低,所以这种我们就不再考虑


2,动态规划:我们用一个二维数组dp[i][j]表示第一个字符串前i个字符和第二个字符串前j个字符组成的最长公共字符串的长度。那么我们在计算dp[i][j]的时候,我们首先要判断s1.charAt(i)是否等于s2.charAt(j),如果不相等,说明当前字符无法构成公共子串,所以dp[i][j]=0。如果相等,说明可以构成公共子串,我们还要加上他们前一个字符构成的最长公共子串长度,也就是dp[i-1][j-1]。所以我们很容易找到递推公式

01最长公共子串的递推公式
1    if(s1.charAt(i) == s2.charAr(j))
2        dp[i][j] = dp[i-1][j-1] + 1;
3    else
4        dp[i][j] = 0;


02最长公共子串画图分析

我们看到在动态规划中,最大值不一定是在最后一个空格内,所以我们要使用一个临时变量在遍历的时候记录下最大值。代码如下

03最长公共子串代码
1public static int maxLong(String str1, String str2) {
2    if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
3        return 0;
4    int max = 0;
5    int[][] dp = new int[str1.length() + 1][str2.length() + 1];
6    for (int i = 1; i <= str1.length(); i++) {
7        for (int j = 1; j <= str2.length(); j++) {
8            if (str1.charAt(i - 1) == str2.charAt(j - 1))
9                dp[i][j] = dp[i - 1][j - 1] + 1;
10            else
11                dp[i][j] = 0;
12            max = Math.max(max, dp[i][j]);
13        }
14    }
15    Util.printTwoIntArrays(dp);//这一行是打印测试数据的,也可以去掉
16    return max;
17}
18

2-3行是一些边界的判断。


重点是在8-11行,就是我们上面提到的递推公式。


第12行是记录最大值,因为这里最大值不一定出现在数组的最后一个位置,所以要用一个临时变量记录下来。


第15行主要用于数据的测试打印,也可以去掉。


我们还用上面的数据来测试一下,看一下结果

1public static void main(String[] args) {
2    System.out.println(maxLong("eplm""people"));
3}

运行结果

结果和我们上面图中分析的完全一致。


我们发现上面的代码有个规律,就是在遍历的时候只使用了dp数组的上面一行,其他的都用不到,所以我们可以考虑把二维数组转化为一位数组,来看下代码

04最长公共子串代码优化
1public static int maxLong(String str1, String str2) {
2    if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
3        return 0;
4    int max = 0;
5    int[] dp = new int[str2.length() + 1];
6    for (int i = 1; i <= str1.length(); i++) {
7        for (int j = str2.length(); j >= 1; j--) {
8            if (str1.charAt(i - 1) == str2.charAt(j - 1))
9                dp[j] = dp[j - 1] + 1;
10            else
11                dp[j] = 0;
12            max = Math.max(max, dp[j]);
13        }
14        Util.printIntArrays(dp);//这一行和下面一行是打印测试数据的,也可以去掉
15        System.out.println();
16    }
17    return max;
18}

上面第7行的for循环我们使用的倒序的方式,这是因为dp数组后面的值会依赖前面的值,而前面的值不依赖后面的值,所以后面的值先修改对前面的没影响,但前面的值修改会对后面的值有影响,所以这里要使用倒序的方式。


我们还用上面的两个字符串来测试打印一下

我们看到结果和之前的完全一样。

2,最长公共子序列

上面我们讲了最长公共子串,子串是连续的。下面我们来讲一下最长公共子序列,而子序列不是连续的。我们还来看上面的两个字符串s1="people",s2="eplm",我们可以很明显看到他们的最长公共子序列是"epl",我们先来画个图再来找一下他的递推公式。

05最长公共子序列画图分析

我们通过上面的图分析发现,子序列不一定都是连续的,只要前面有相同的子序列,哪怕当前比较的字符不一样,那么当前字符串之前的子序列也不会为0。换句话说,如果当前字符不一样,我们只需要把第一个字符串往前退一个字符或者第二个字符串往前退一个字符然后记录最大值即可。


举个例子,比如图中第4行第4列(就是图中灰色部分),p和m不相等,如果字符串"eplm"退一步是"epl"再和"epop"对比我们发现有2个相同的子序列(也就是上面表格中数组(2,3)的位置)。如果字符串"peop"退一步是"peo"再和"eplm"对比我们发现只有1个相同的子序列(这里的pe和ep只能有一个相同,要么p相同,要么e相同,因为子序列的顺序不能变)(也就是上面表格中数组(3,2)的位置)。所以我们很容易找出递推公式

06最长公共子序列的递推公式
1    if(s1.charAt(i) == s2.charAr(j))
2        dp[i][j] = dp[i-1][j-1] + 1;
3    else
4        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);

有了上面的递推公式,代码就很容易写出来了,我们来看下

07最长公共子序列代码
1public static int maxLong(String str1, String str2) {
2    if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
3        return 0;
4    int[][] dp = new int[str1.length() + 1][str2.length() + 1];
5    for (int i = 1; i <= str1.length(); i++) {
6        for (int j = 1; j <= str2.length(); j++) {
7            if (str1.charAt(i - 1) == str2.charAt(j - 1))
8                dp[i][j] = dp[i - 1][j - 1] + 1;
9            else
10                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
11        }
12    }
13    Util.printTwoIntArrays(dp);//这一行是打印测试数据的,也可以去掉
14    return dp[str1.length()][str2.length()];
15}
16

我们发现他和最长公共子串的唯一区别就在第10行,我们还用图中分析的两个字符串测试一下,看一下结果

08最长公共子序列测试结果

我们看到打印的结果和上面图中分析的完全一致。上面在讲到最长公共子串的时候我们可以把二维数组变为一维数组来实现对代码性能的优化,这里我们也可以参照上面的代码来优化一下,但这里和上面稍微有点不同,如果当前字符相同的时候,他会依赖左上角的值,但这个值有可能会被上一步计算的时候就被替换掉了,所以我们必须要先保存下来,我们来看下代码

09最长公共子序列代码优化
1public static int maxLong(String str1, String str2) {
2    if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
3        return 0;
4    int[] dp = new int[str2.length() + 1];
5    int last = 0;
6    for (int i = 1; i <= str1.length(); i++) {
7        for (int j = 1; j <= str2.length(); j++) {
8            int temp = dp[j];//dp[j]这个值会被替换,所以替换之前要把他保存下来
9            if (str1.charAt(i - 1) == str2.charAt(j - 1))
10                dp[j] = last + 1;
11            else
12                dp[j] = Math.max(dp[j], dp[j - 1]);
13            last = temp;
14        }
15        Util.printIntArrays(dp);//这一行和下面一行是打印测试数据的,也可以去掉
16        System.out.println();
17    }
18    return dp[str2.length()];
19}

代码在第8行的时候先把要被替换的值保存下来,我们还是用上面的数据来测试一下,看一下打印结果

10最长公共子序列优化测试结果

我们看到结果和我们之前分析的完全一致。

长按上图,识别图中二维码之后即可关注。


如果喜欢这篇文章就点个"在看"吧

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存