Python 求两个字符串的最长公共子串(长文解析)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在文本处理、自然语言处理(NLP)或生物信息学领域,寻找两个字符串的最长公共子串是一个常见需求。例如,比较两篇文档的相似性、检测代码的重复逻辑,或是分析 DNA 序列的相似片段时,都需要这一技术。本文将深入讲解如何用 Python 实现这一功能,并通过不同方法逐步优化性能,适合编程初学者和中级开发者循序渐进地理解。
什么是子串与最长公共子串?
子串与子序列的区别
在开始之前,我们需要明确几个关键概念:
- 子串(Substring):字符串中连续的一段字符。例如,字符串
"abcd"
的子串包括"a"
、"ab"
、"bcd"
等。 - 子序列(Subsequence):字符串中不连续的一段字符。例如,
"ac"
是"abcd"
的子序列,但不是子串。 - 最长公共子串(Longest Common Substring):两个字符串中完全相同的最长连续子串。例如,字符串
"abcdef"
和"zabcf"
的最长公共子串是"abc"
。
比喻:可以把子串想象成一本书中连续的几页,而子序列则是随机撕下几页后重新排列的组合。最长公共子串则类似于在两本书中找到最长的连续重复段落。
方法一:暴力破解法(Brute Force)
原理与实现
暴力破解法通过遍历所有可能的子串组合,逐一比较两个字符串的子串,找出最长的公共部分。虽然思路简单,但时间复杂度较高,适合小规模数据的演示。
步骤分解:
- 生成所有子串:对第一个字符串生成所有可能的子串。
- 检查子串是否存在于第二个字符串:遍历每个子串,判断其是否是第二个字符串的子串。
- 记录最长子串:维护当前最长子串的长度和内容。
代码示例:
def longest_common_substring(s1, s2):
max_len = 0
result = ""
# 遍历所有可能的起始和结束位置
for i in range(len(s1)):
for j in range(i + 1, len(s1) + 1):
substring = s1[i:j]
if substring in s2 and len(substring) > max_len:
max_len = len(substring)
result = substring
return result
s1 = "abcde"
s2 = "abfgde"
print(longest_common_substring(s1, s2)) # 输出 "ab" 或 "de"(取决于实现逻辑)
时间复杂度分析:
- 时间复杂度为 O(n^3),其中 n 是字符串的长度。
- 具体来说,生成子串需要 O(n^2) 时间,而
substring in s2
操作的时间复杂度为 O(n)。 - 因此,当字符串长度较大时,该方法效率较低,需要进一步优化。
方法二:动态规划(Dynamic Programming)
状态转移方程与优化
动态规划(DP)是解决最长公共子串问题的经典方法,通过记录子问题的解来减少重复计算,将时间复杂度降低到 O(n^2)。
核心思想:
- 定义一个二维数组
dp[i][j]
,表示以s1[i-1]
和s2[j-1]
结尾的最长公共后缀的长度。 - 状态转移方程:
if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = 0
- 最大值记录:遍历过程中维护当前最大值及对应的位置。
代码示例:
def lcs_dp(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
max_length = 0
end_pos = 0 # 记录最长子串在s1中的结束位置
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
if dp[i][j] > max_length:
max_length = dp[i][j]
end_pos = i
else:
dp[i][j] = 0
return s1[end_pos - max_length : end_pos]
s1 = "abcde"
s2 = "abfgde"
print(lcs_dp(s1, s2)) # 输出 "ab" 或 "de"
表格模拟动态规划过程
以下是一个简化示例的 dp
数组变化:
j=0 | j=1 (a) | j=2 (b) | j=3 (f) | j=4 (g) | j=5 (d) | j=6 (e) | |
---|---|---|---|---|---|---|---|
i=0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
i=1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
i=2 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |
i=3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
i=4 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
i=5 | 0 | 0 | 0 | 0 | 0 | 0 | 2 |
方法三:优化与扩展
后缀数组与滚动哈希
对于更长的字符串(如百万级长度),动态规划的 O(n^2) 时间复杂度可能仍不满足需求。此时可以考虑以下高级方法:
后缀数组(Suffix Array)
- 将两个字符串合并,构造后缀数组并排序,通过比较相邻后缀的公共前缀长度。
- 时间复杂度为 O(n log n),但实现较为复杂。
滚动哈希(Rolling Hash)
- 使用哈希函数快速比较子串的哈希值,通过二分法缩小查找范围。
- 时间复杂度为 O(n log n),适合大规模数据。
代码示例(滚动哈希简化版):
def longest_common_substring_rolling_hash(s1, s2):
def hash_val(s, size):
return hash(s[:size]) # 简化哈希实现,实际应使用更稳定的哈希算法
low, high = 0, min(len(s1), len(s2))
result = ""
while low <= high:
mid = (low + high) // 2
if find_substring(s1, s2, mid):
low = mid + 1
result = s1[start:start+mid]
else:
high = mid -1
return result
def find_substring(s1, s2, length):
# 实现哈希匹配逻辑,此处简化
pass
实际案例与对比分析
场景示例
假设我们有两个字符串:
s1 = "abcdeabc"
s2 = "xyzabcfde"
- 暴力法会遍历所有
s1
的子串,逐一检查是否在s2
中,最终找到"abc"
或"de"
。 - 动态规划法通过
dp
数组快速定位到最大值,直接返回"abc"
。 - 滚动哈希法则通过二分法和哈希加速,在大规模数据中表现更优。
性能对比表格
方法名称 | 时间复杂度 | 适用场景 | 代码复杂度 |
---|---|---|---|
暴力破解法 | O(n^3) | 小规模数据(n < 1000) | 简单 |
动态规划法 | O(n^2) | 中等规模数据(n < 1e4) | 中等 |
滚动哈希/后缀数组 | O(n log n) | 大规模数据(n > 1e5) | 复杂 |
结论
本文通过暴力法、动态规划法和优化方法,逐步讲解了如何用 Python 实现两个字符串的最长公共子串查找。对于编程初学者,建议从暴力法入手理解核心逻辑;对于中级开发者,则可以进一步探索动态规划和高级算法的优化技巧。
在实际应用中,需根据数据规模选择合适的方法:小数据用暴力或动态规划,大数据则需要滚动哈希或后缀数组。掌握这些方法不仅能解决具体问题,还能为后续学习算法优化和文本处理技术打下基础。
关键词布局检查:
- 核心关键词 “Python 求两个字符串的最长公共子串” 在标题、前言、方法部分均有自然出现。
- 其他相关术语如“动态规划”、“滚动哈希”等作为技术延伸点,符合 SEO 的长尾词策略。