手把手教你实现最长公共子序列(LCS)算法及内存操作实战
一、前言
在算法与编程实践中,动态规划和内存操作是基本功。本文结合两个实战案例 —— 最长公共子序列(LCS)算法实现和 C 语言内存分配实战,带你深入理解核心逻辑、常见错误及优化思路。文末附完整代码和调试技巧,适合算法入门与 C 语言进阶学习者。
二、案例一:最长公共子序列(LCS)算法实现
2.1 问题描述
目标:给定两个字符串s1和s2,求它们的最长公共子序列(LCS)。子序列定义:不要求连续,但顺序必须一致(如 “abc” 是 “aebfc” 的子序列)。应用场景:文本相似度分析、版本控制(diff 工具)、生物信息学(DNA 序列比对)等。
2.2 动态规划解法核心思路
2.2.1 状态定义
dp[i][j]:表示s1前i个字符和s2前j个字符的最长公共子序列长度。
2.2.2 状态转移方程
字符相等:s1[i-1] == s2[j-1]时,dp[i][j] = dp[i-1][j-1] + 1(继承左上角状态并 + 1)。
字符不等:取上方或左方的最大值,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
2.2.3 代码实现(含详细注释)
#include <string.h>
#include <stdlib.h>
int min(int a, int b) { return a < b ? a : b; }
int max(int a, int b) { return a > b ? a : b; }
char* LCS(char* s1, char* s2) {
int m = strlen(s1), n = strlen(s2);
// 处理空输入:返回空字符串(根据题目要求调整,若需返回"-1"可修改此处)
if (m == 0 || n == 0) {
char* res = (char*)malloc(1); // 空字符串仅需’\0’
res[0] = ‘\0’;
return res;
}
// 动态规划表初始化
int dp[m+1][n+1];
memset(dp, 0, sizeof(dp));
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1; // 字符相等时,长度+1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]); // 取较大的子问题解
}
}
}
int len = dp[m][n];
if (len == 0) { // 非空输入但无公共子序列(如"111" vs "222")
char* res = (char*)malloc(3); // 存储"-1\0"
res[0] = '-'; res[1] = '1'; res[2] = '\0';
return res;
}
// 回溯构建LCS字符串(从后往前)
char* res = (char*)malloc(len + 1);
res[len] = '\0'; // 结尾符
int i = m, j = n, index = len - 1;
while (i > 0 && j > 0) {
if (s1[i-1] == s2[j-1]) { // 找到公共字符,加入结果
res[index--] = s1[i-1];
i--; j--;
} else if (dp[i-1][j] > dp[i][j-1]) { // 向较大子问题方向移动
i--;
} else {
j--;
}
}
return res;
}
2.3 常见错误与解决方案
错误类型
示例代码问题
解决方案
索引越界
if(s1[j] == s2[j])
修正为s1[i-1] == s2[j-1](字符串 0 索引,DP 表 1 索引)
空输入处理
返回 “-1” 而非空字符串
根据题目要求统一逻辑,如len == 0时返回 “-1”
结果逆序
直接存储顺序错误
回溯时从后往前填充,无需反转(如res[index–] = char)
三、案例二:C 语言内存分配实战 —— 批量字符串生成
3.1 代码功能
循环分配内存并初始化字符串"123",演示动态内存操作的基本用法。
3.2 完整代码(含内存泄漏风险提示)
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 新增:包含strcpy所需头文件
int main() {
char* str = “12345”;
printf(“固定字符串地址:%#p,内容:%s\n”, (void*)str, str);
for (int i = 0; i < 10000000; i++) {
char* stri = (char*)malloc(10 * sizeof(char)); // 分配10字节("123\0"需4字节,足够)
if (stri == NULL) { // 检查内存分配失败
fprintf(stderr, "内存分配失败!\n");
return 1;
}
strcpy(stri, "123"); // 复制字符串(含结尾符)
// 每10000次打印一次(避免控制台卡顿)
if (i % 10000 == 0) {
printf("第%d次分配:地址%#p,内容%s\n", i, (void*)stri, stri);
}
free(stri); // 关键:及时释放内存,避免泄漏!
}
return 0;
}
3.3 核心知识点解析
malloc用法:
分配n字节内存:malloc(n),返回void指针,需强制类型转换。
分配字符串内存时,需额外预留 1 字节给’\0’(如"123"需 4 字节:3 字符 + 1 结尾符)。
内存泄漏风险:
案例中每次malloc后必须调用free释放内存,否则循环次数过大会导致内存溢出。
最佳实践:malloc和free成对出现,释放后置指针为NULL(避免野指针)。
strcpy安全问题:
确保目标内存足够大,否则会越界(推荐使用strncpy并手动添加’\0’)。
四、扩展思考:从实战到优化
4.1 LCS 算法优化方向
空间复杂度优化:
原代码使用O(mn)空间,可优化为O(n)(滚动数组,仅保留前一行数据)。
处理多解情况:
当dp[i-1][j] == dp[i][j-1]时,可能存在多条 LCS,需记录所有路径(适合回溯法)。
4.2 内存操作最佳实践
初始化检查:malloc后立即检查返回值是否为NULL。
类型匹配:字符串操作使用char*,数值数组使用int*等,避免强制类型转换错误。
工具辅助:使用 Valgrind 等内存检测工具,定位泄漏和越界问题。
五、总结
5.1 核心收获
动态规划核心:状态定义、转移方程、边界条件缺一不可。
内存操作要点:分配与释放成对出现,避免越界和泄漏。
调试技巧:通过打印中间变量(如 DP 表、内存地址)定位逻辑错误。
5.2 代码调试建议
在 LCS 代码中添加printf打印dp[i][j],观察状态转移是否正确。
在内存操作代码中减少循环次数(如i < 100),逐步调试避免控制台过载。
5.3 文末福利
完整代码已上传至Gitee 仓库,包含测试用例和优化版本,欢迎 Star!
如果本文对你有帮助,欢迎点赞、收藏、评论三连,你的支持是我持续输出的最大动力!如有疑问,欢迎在评论区留言,我会第一时间回复~
六、参考资料
《算法导论》动态规划章节
C 语言官方文档(malloc/strcpy用法)
牛客网算法题解(LCS 专题)
作者:CodeXiaoBai发布时间:2023 年 X 月 X 日转载请注明出处,侵权必究。