【经典算法】LeetCode 1143:最长公共子序列Java/C/Python3实现含注释说明,Medium)

简介: 【经典算法】LeetCode 1143:最长公共子序列Java/C/Python3实现含注释说明,Medium)

题目

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

原题:Leetcode 1143:最长公共子序列

思路及实现

方式一:暴力递归(不推荐,但容易想到)

思路

暴力递归的解决思路是很容易想到的。

递归函数返回两个字符串 str1,str2 到 index1,index2 位置的最长公共子序列的长度。

递归终止的条件是两个串中任意一个串是空串,那么最长公共子序列的长度为 0。

到任意一个 index1,index2 位置有以下几种可能性

最长公共子序列的最后一个值和 str1[index1] 、str2[index2]都相同。

最长公共子序列的最后一个值和 str1[index1] 、str2[index2]都不相同。

最长公共子序列的最后一个值和 str1[index1] 、str2[index2]其中一个相同。

这样就把问题拆分成了子问题,使用递归求解。

代码实现

Java版本
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        return process(text1.toCharArray(), text1.length() - 1, text2.toCharArray(), text2.length() - 1);
    }
    public int process(char[] str1, int index1, char[] str2, int index2) {
        // 递归终止条件,当其中一个字符数组已经遍历完时,返回0
        if (index2 < 0 || index1 < 0) {
            return 0;
        }
        // 如果当前字符相等,则两个字符都向前移动一位,并且最长公共子序列长度加1
        if (str1[index1] == str2[index2]) {
            return process(str1, index1 - 1, str2, index2 - 1) + 1;
        } else {
            // 如果当前字符不相等,分别计算三种情况下的最长公共子序列长度,并返回最大值
            int p1 = process(str1, index1, str2, index2 - 1); // 在str2中减去当前字符
            int p2 = process(str1, index1 - 1, str2, index2); // 在str1中减去当前字符
            int p3 = process(str1, index1 - 1, str2, index2 - 1); // 在str1和str2中同时减去当前字符
            return Math.max(Math.max(p1, p2), p3);
        }
    }
}

说明:

在longestCommonSubsequence方法中,将输入字符串转换为字符数组,并调用process方法来求解最长公共子序列问题。

process方法递归地处理问题,根据当前字符是否相等进行判断:

如果当前字符相等,则最长公共子序列长度等于在两个字符串都减去当前字符的情况下的最长公共子序列长度加1。

如果当前字符不相等,分别计算在str2中减去当前字符、在str1中减去当前字符以及在str1和str2中同时减去当前字符的三种情况下的最长公共子序列长度,并返回最大值。

通过递归回溯的过程,依次处理所有字符的组合,最终得到最长公共子序列的长度。

需要注意的是,递归解法可能会导致重复计算,因此在问题规模较大的情况下,可能需要使用记忆化技术对递归过程进行优化,避免重复计算。

优化:缓存优化记忆化搜索

对于上述递归过程,其实会有很多重复的求解过程。

把重复的解放到一个缓存里面,每次直接从缓存中取值,这样就可以减少很多的重复过程。

新建 db 数组(当然 map 也可以),作为缓存数组。

class Solution {
    private int db[][];
    public int longestCommonSubsequence(String text1, String text2) {
        // 初始化二维数组db为-1
        db = new int[text1.length()][text2.length()];
        for (int i = 0; i < text1.length(); i++) {
            for (int j = 0; j < text2.length(); j++) {
                db[i][j] = -1;
            }
        }
        return process(text1.toCharArray(), text1.length() - 1, text2.toCharArray(), text2.length() - 1);
    }
    public int process(char[] str1, int index1, char[] str2, int index2) {
        // 递归终止条件,当其中一个字符数组已经遍历完时,返回0
        if (index2 < 0 || index1 < 0) {
            return 0;
        }
        // 如果当前字符已经计算过最长公共子序列长度,则直接返回结果
        if (db[index1][index2] != -1) {
            return db[index1][index2];
        }
        // 如果当前字符相等,则两个字符都向前移动一位,并且最长公共子序列长度加1
        if (str1[index1] == str2[index2]) {
            db[index1][index2] = process(str1, index1 - 1, str2, index2 - 1) + 1;
            return db[index1][index2];
        } else {
            // 如果当前字符不相等,分别计算三种情况下的最长公共子序列长度,并返回最大值
            int p1 = process(str1, index1, str2, index2 - 1); // 在str2中减去当前字符
            int p2 = process(str1, index1 - 1, str2, index2); // 在str1中减去当前字符
            int p3 = process(str1, index1 - 1, str2, index2 - 1); // 在str1和str2中同时减去当前字符
            db[index1][index2] = Math.max(Math.max(p1, p2), p3);
            return db[index1][index2];
        }
    }
}

说明:

在longestCommonSubsequence方法中,创建了大小为text1.length() x

text2.length()的二维数组db,并将其初始化为-1。该数组将用于保存中间计算结果。

process方法递归地处理问题,增加了对中间计算结果是否已经保存的判断:

如果当前字符已经计算过最长公共子序列长度,则直接返回计算结果。

如果当前字符相等,则最长公共子序列长度等于在两个字符串都减去当前字符的情况下的最长公共子序列长度加1。

如果当前字符不相等,分别计算在str2中减去当前字符、在str1中减去当前字符以及在str1和str2中同时减去当前字符的三种情况下的最长公共子序列长度,并返回最大值。

通过这种记忆化的方式,可以避免重复计算,提高运行效率。

C语言版本
#include <stdio.h>
#include <string.h>
int db[1000][1000]; // 定义全局数组用于保存计算结果
int process(char* str1, int index1, char* str2, int index2) {
    // 递归终止条件,当其中一个字符数组已经遍历完时,返回0
    if (index2 < 0 || index1 < 0) {
        return 0;
    }
    // 如果当前字符已经计算过最长公共子序列长度,则直接返回结果
    if (db[index1][index2] != -1) {
        return db[index1][index2];
    }
    // 如果当前字符相等,则两个字符都向前移动一位,并且最长公共子序列长度加1
    if (str1[index1] == str2[index2]) {
        db[index1][index2] = process(str1, index1 - 1, str2, index2 - 1) + 1;
        return db[index1][index2];
    } else {
        // 如果当前字符不相等,分别计算三种情况下的最长公共子序列长度,并返回最大值
        int p1 = process(str1, index1, str2, index2 - 1); // 在str2中减去当前字符
        int p2 = process(str1, index1 - 1, str2, index2); // 在str1中减去当前字符
        int p3 = process(str1, index1 - 1, str2, index2 - 1); // 在str1和str2中同时减去当前字符
        db[index1][index2] = (p1 > p2) ? ((p1 > p3) ? p1 : p3) : ((p2 > p3) ? p2 : p3); // 取三者中的最大值
        return db[index1][index2];
    }
}
int longestCommonSubsequence(char* text1, char* text2) {
    memset(db, -1, sizeof(db)); // 初始化数组为-1
    return process(text1, strlen(text1) - 1, text2, strlen(text2) - 1);
}
/*
int main() {
    char text1[] = "abcde";
    char text2[] = "ace";
    int result = longestCommonSubsequence(text1, text2);
    printf("Longest Common Subsequence: %d\n", result);
    return 0;
}

说明

在process函数中,增加了对中间计算结果是否已经保存的判断:

如果当前字符已经计算过最长公共子序列长度,则直接返回计算结果。

如果当前字符相等,则最长公共子序列长度等于在两个字符串都减去当前字符的情况下的最长公共子序列长度加1。

如果当前字符不相等,分别计算在str2中减去当前字符、在str1中减去当前字符以及在str1和str2中同时减去当前字符的三种情况下的最长公共子序列长度,并返回最大值。

通过全局数组db的记忆化方式,可以避免重复计算,提高运行效率。在longestCommonSubsequence函数中,使用memset函数将db数组初始化为-1。

在main函数中,给定了两个示例字符串,并打印了最长公共子序列的长度。

Python3版本
def process(str1, index1, str2, index2, db):
    # 递归终止条件,当其中一个字符数组已经遍历完时,返回0
    if index2 < 0 or index1 < 0:
        return 0
    # 如果当前字符已经计算过最长公共子序列长度,则直接返回结果
    if db[index1][index2] != -1:
        return db[index1][index2]
    # 如果当前字符相等,则两个字符都向前移动一位,并且最长公共子序列长度加1
    if str1[index1] == str2[index2]:
        db[index1][index2] = process(str1, index1 - 1, str2, index2 - 1, db) + 1
        return db[index1][index2]
    else:
        # 如果当前字符不相等,分别计算三种情况下的最长公共子序列长度,并返回最大值
        p1 = process(str1, index1, str2, index2 - 1, db)  # 在str2中减去当前字符
        p2 = process(str1, index1 - 1, str2, index2, db)  # 在str1中减去当前字符
        p3 = process(str1, index1 - 1, str2, index2 - 1, db)  # 在str1和str2中同时减去当前字符
        db[index1][index2] = max(p1, p2, p3)  # 取三者中的最大值
        return db[index1][index2]
def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    db = [[-1] * n for _ in range(m)]  # 创建二维数组并初始化为-1
    return process(text1, m - 1, text2, n - 1, db)
# 示例用法
#text1 = "abcde"
#text2 = "ace"
#result = longestCommonSubsequence(text1, text2)
#print("Longest Common Subsequence:", result)

说明:

在process函数中,增加了对中间计算结果是否已经保存的判断:

如果当前字符已经计算过最长公共子序列长度,则直接返回计算结果。

如果当前字符相等,则最长公共子序列长度等于在两个字符串都减去当前字符的情况下的最长公共子序列长度加1。

如果当前字符不相等,分别计算在str2中减去当前字符、在str1中减去当前字符以及在str1和str2中同时减去当前字符的三种情况下的最长公共子序列长度,并返回最大值。

通过二维数组db的记忆化方式,可以避免重复计算,提高运行效率。

在longestCommonSubsequence函数中,创建了大小为m x n的二维数组db,并将其初始化为-1。

在示例用法中,给定了两个示例字符串,并打印了最长公共子序列的长度。

复杂度分析

  • 时间复杂度:O(m * n),其中m和n分别是text1和text2的长度。
  • 空间复杂度:O(m * n),主要用于存储中间计算结果的二维数组。
    通过对中间计算结果的记忆化优化,递归解法避免了重复计算,提高了效率。但在某些情况下,仍可能存在大量的计算和存储开销。因此,实际应用中,可以结合其他解法如动态规划来进一步优化时间和空间复杂度。

方式二:动态规划(推荐)

思路

最长公共子序列问题是典型的二维动态规划问题。 假设字符串 text 1 和 text 2 的长度分别为 m 和 n,创建 m+1 行 n+1 列的二维数组 dp,其中 dp[i][j] 表示 text 1 [0:i] text2 [0:j] 的最长公共子序列的长度。

上述表示中,text 1  [0:i] 表示 text 1  的长度为 i 的前缀,text 2  [0:j] 表示 text 2  的长度为 j 的前缀。

考虑动态规划的边界情况:

  1. 当 i=0 时,text 1  [0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是 0,因此对任意 0≤j≤n,有 dp[0][j]=0;
  2. 当 j=0 时,text 2  [0:j] 为空,同理可得,对任意 0≤i≤m,有 dp[i][0]=0。

因此动态规划的边界情况是:当 i=0 或 j=0 时,dp[i][j]=0。

当 i>0 且 j>0 时,考虑 dp[i][j] 的计算:

  1. 当 text 1  [i−1]=text 2  [j−1] 时,将这两个相同的字符称为公共字符,考虑 text 1  [0:i−1] 和 text 2  [0:j−1] 的最长公共子序列,再增加一个字符(即公共字符)即可得到 text 1  [0:i] 和 text 2  [0:j] 的最长公共子序列,因此 dp[i][j]=dp[i−1][j−1]+1。
  2. 当 text1[i−1]!=text2[j−1] 时,考虑以下两项:
  • 2.1.text1[0:i−1]和 text2[0:j] 的最长公共子序列;
  • 2.2.text1[0:i] 和 text2[0:j−1] 的最长公共子序列。
    要得到 text 1  [0:i] 和 text 2  [0:j] 的最长公共子序列,应取两项中的长度较大的一项,因此 dp[i][j]=max(dp[i−1][j],dp[i][j−1])。

由此可以得到如下状态转移方程:

最终计算得到 dp[m][n] 即为 text 1和 text2的最长公共子序列的长度。

代码实现

Java版本
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // dp, dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列
        int m = text1.length();
        int n = text2.length();
        int[][] dp = new int[m + 1][n + 1]; // 创建一个二维数组用于保存最长公共子序列的长度
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    // 如果当前字符相等,那么最长公共子序列的长度为前一个子序列长度加1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 如果当前字符不相等,那么最长公共子序列的长度为text1减去当前字符和text2的最长公共子序列长度,
                    // 与text2减去当前字符和text1的最长公共子序列长度的较大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n]; // 返回text1和text2的最长公共子序列的长度
    }
}

说明:

该代码实现了动态规划算法来求解两个字符串的最长公共子序列。其中,二维数组dp用于保存最长公共子序列的长

度。

通过两个嵌套的循环,遍历所有字符的组合。如果两个字符相等,则当前最长公共子序列的长度为前一个子序列长度加1;如果两个字符不相等,则最长公共子序列的长度为分别减去当前字符后的子序列的最大长度。

最终,返回dp[m][n],即text1和text2的最长公共子序列的长度。

希望这个补充的代码说明对您有帮助。

C语言版本
#include <stdio.h>
#include <string.h>
int longestCommonSubsequence(char* text1, char* text2) {
    int m = strlen(text1);
    int n = strlen(text2);
    int dp[m + 1][n + 1];
    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            if (i == 0 || j == 0) {
                dp[i][j] = 0;
            }
        }
    }
    // 填充二维数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1[i - 1] == text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = (dp[i - 1][j] > dp[i][j - 1]) ? dp[i - 1][j] : dp[i][j - 1];
            }
        }
    }
    return dp[m][n];
}
/**
int main() {
    char text1[] = "abcde";
    char text2[] = "ace";
    int result = longestCommonSubsequence(text1, text2);
    printf("Longest Common Subsequence: %d\n", result);
    return 0;
}
**/

说明:

使用二维数组dp保存最长公共子序列的长度。

代码中的longestCommonSubsequence函数接受两个字符串作为参数,并返回它们的最长公共子序列的长度。

首先,对dp数组进行边界条件的初始化,使其第一行和第一列的值为0。

然后,通过两个嵌套的循环遍历所有字符的组合。如果两个字符相等,则当前最长公共子序列的长度为前一个子序列长度加1;如果两个字符不相等,则最长公共子序列的长度为在text1中减去当前字符和在text2中的最长公共子序列长度,与在text2中减去当前字符和在text1中的最长公共子序列长度的较大值。

最后,返回dp[m][n],即text1和text2的最长公共子序列的长度。

在main函数中,给定了两个示例字符串,并打印了最长公共子序列的长度。

Python3版本
def longestCommonSubsequence(text1: str, text2: str) -> int:
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    # 动态规划填充二维数组
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                # 如果字符相等,则当前最长公共子序列的长度为前一个子序列长度加1
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                # 如果字符不相等,则最长公共子序列的长度为两个子序列的较大值
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]
# 示例用法
#text1 = "abcde"
#text2 = "ace"
#result = longestCommonSubsequence(text1, text2)
#print("Longest Common Subsequence:", result)

说明:

使用二维数组dp保存最长公共子序列的长度。

代码中的longestCommonSubsequence函数接受两个字符串作为参数,并返回它们的最长公共子序列的长度。

首先,创建一个大小为(m+1)x(n+1)的二维数组dp,其中m和n分别是两个字符串的长度。

然后,通过两个嵌套的循环遍历所有字符的组合。如果两个字符相等,则当前最长公共子序列的长度为前一个子序列长度加1;如果两个字符不相等,则最长公共子序列的长度为两个子序列的较大值。

最后,返回dp[m][n],即text1和text2的最长公共子序列的长度。

在示例用法中,给定了两个示例字符串,并打印了最长公共子序列的长度。

希望这个带有注释说明的Python代码对您有所帮助。

复杂度分析

  • 时间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1  和 text 2  的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。
  • 空间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1  和 text 2  的长度。创建了 m+1 行 n+1 列的二维数组 dp。

总结

解法 时间复杂度 空间复杂度 备注
动态规划 O(m * n) O(m * n) 使用二维数组保存中间计算结果
递归优化 O(m * n) O(m * n) 使用二维数组保存中间计算结果

相似题目

相似题目 力扣链接 难度评级
最长递增子序列 链接 Medium
编辑距离 链接 Hard
最长重复子数组 链接 Medium
最长回文子序列 链接 Medium
最长连续递增序列 链接 Easy
相关文章
|
4月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
4月前
|
人工智能 算法 NoSQL
LRU算法的Java实现
LRU(Least Recently Used)算法用于淘汰最近最少使用的数据,常应用于内存管理策略中。在Redis中,通过`maxmemory-policy`配置实现不同淘汰策略,如`allkeys-lru`和`volatile-lru`等,采用采样方式近似LRU以优化性能。Java中可通过`LinkedHashMap`轻松实现LRUCache,利用其`accessOrder`特性和`removeEldestEntry`方法完成缓存淘汰逻辑,代码简洁高效。
182 0
|
2月前
|
算法 数据可视化 数据挖掘
基于EM期望最大化算法的GMM参数估计与三维数据分类系统python源码
本内容展示了基于EM算法的高斯混合模型(GMM)聚类实现,包含完整Python代码、运行效果图及理论解析。程序使用三维数据进行演示,涵盖误差计算、模型参数更新、结果可视化等关键步骤,并附有详细注释与操作视频,适合学习EM算法与GMM模型的原理及应用。
|
2月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
62 1
|
2月前
|
存储 监控 算法
基于 Python 跳表算法的局域网网络监控软件动态数据索引优化策略研究
局域网网络监控软件需高效处理终端行为数据,跳表作为一种基于概率平衡的动态数据结构,具备高效的插入、删除与查询性能(平均时间复杂度为O(log n)),适用于高频数据写入和随机查询场景。本文深入解析跳表原理,探讨其在局域网监控中的适配性,并提供基于Python的完整实现方案,优化终端会话管理,提升系统响应性能。
72 4
|
8月前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的眼疾识别系统实现~人工智能+卷积网络算法
眼疾识别系统,本系统使用Python作为主要开发语言,基于TensorFlow搭建卷积神经网络算法,并收集了4种常见的眼疾图像数据集(白内障、糖尿病性视网膜病变、青光眼和正常眼睛) 再使用通过搭建的算法模型对数据集进行训练得到一个识别精度较高的模型,然后保存为为本地h5格式文件。最后使用Django框架搭建了一个Web网页平台可视化操作界面,实现用户上传一张眼疾图片识别其名称。
479 5
基于Python深度学习的眼疾识别系统实现~人工智能+卷积网络算法
|
3月前
|
PyTorch 算法框架/工具 C++
人工智能算法python程序运行环境安装步骤整理
本教程详细介绍Python与AI开发环境的配置步骤,涵盖软件下载、VS2017安装、Anaconda配置、PyCharm设置及组件安装等内容,适用于Windows系统,助你快速搭建开发环境。
|
8月前
|
存储 算法 安全
探究‘公司禁用 U 盘’背后的哈希表算法与 Java 实现
在数字化办公时代,信息安全至关重要。许多公司采取“禁用U盘”策略,利用哈希表算法高效管理外接设备的接入权限。哈希表通过哈希函数将设备标识映射到数组索引,快速判断U盘是否授权。例如,公司预先将允许的U盘标识存入哈希表,新设备接入时迅速验证,未授权则禁止传输并报警。这有效防止恶意软件和数据泄露,保障企业信息安全。 代码示例展示了如何用Java实现简单的哈希表,模拟公司U盘管控场景。哈希表不仅用于设备管理,还在文件索引、用户权限等多方面助力信息安全防线的构建,为企业数字化进程保驾护航。
|
4月前
|
算法 Python
Apriori算法的Python实例演示
经过运行,你会看到一些集合出现,每个集合的支持度也会给出。这些集合就是你想要的,经常一起被购买的商品组合。不要忘记,`min_support`参数将决定频繁项集的数量和大小,你可以根据自己的需要进行更改。
161 18

热门文章

最新文章