C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化

C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化

最长公共子串(连续)求解是字符串处理中的经典问题,常出现在算法面试与数据处理场景中。这份代码基于动态规划思想,不仅实现了最长公共子串长度的计算,还能提取出具体的公共子串内容,同时封装了换行符处理函数保证输入的规范性。本文将逐模块拆解代码逻辑,指出潜在问题并提供修复后的可运行版本,帮你吃透动态规划解决字符串问题的核心思路。

一、代码整体功能与核心亮点

1. 整体功能

  1. 接收用户输入的两个字符串,处理fgets读取带来的换行符;
  2. 利用动态规划算法计算两个字符串的最长公共连续子串长度;
  3. 提取并输出最长公共子串的具体内容,无公共子串时给出明确提示;
  4. 全程保证字符串操作的安全性,避免乱码与数组越界问题。

2. 核心亮点

  • 采用动态规划思想,规避暴力枚举的低效问题,逻辑清晰且可扩展;
  • 封装Remove_Newline函数,统一处理换行符,符合模块化编程规范;
  • 不仅返回最长长度,还提取具体子串,实用性更强;
  • 手动添加字符串结束符'\0',避免输出乱码,细节处理到位。

二、核心模块逐句解析

1. 辅助函数:Remove_Newline(换行符处理)

c

运行

python 复制代码
void Remove_Newline(char *str)
{
    if(str == NULL) // 健壮性判断:避免传入空指针导致崩溃
    {
        return;
    }
    int len = strlen(str);
    if(len > 0 && str[len-1] == '\n') // 判断末尾是否为换行符
    {
        str[len-1] = '\0'; // 替换为字符串结束符,截断换行符
    }
}
  • 核心作用:处理fgets的 "遗留问题"------fgets会将用户按下的Enter键(\n)读取到字符串中,若不处理,会导致后续字符串对比时出现 "隐形差异";
  • 健壮性设计:先判断str是否为NULL,避免解引用空指针触发程序崩溃;
  • 注意点:仅修改字符串末尾的\n,不影响其他有效字符,保证输入字符串的完整性。

2. 核心算法:Substring(动态规划求解最长公共子串)

这是整个程序的核心,实现了 "计算长度" 和 "提取子串" 两个核心功能,我们分步骤拆解。

步骤 1:变量与二维数组初始化

c

运行

ini 复制代码
int m = strlen(a);
int n = strlen(b);
int i, j;
int sub[m+1][n+1]; // 动态规划二维数组(状态表)
int max_len = 0; // 记录最长公共子串长度
int end_index=0; // 记录最长公共子串在a中的结束索引

// 初始化动态规划数组所有元素为0(边界条件)
for(i = 0; i <= m; i++)
{
    for(j = 0; j <= n; j++)
    {
        sub[i][j] = 0; // 空字符串与任意字符串的公共子串长度为0
    }
}
  • 动态规划数组sub[m+1][n+1]sub[i][j]表示 "以字符串a的第i-1个字符、字符串b的第j-1个字符结尾的最长公共子串长度";
  • 边界初始化:所有元素置 0,对应 "空字符串与任意字符串无有效公共子串" 的边界条件,为后续计算铺垫。
步骤 2:动态规划填充数组(核心逻辑)

c

运行

perl 复制代码
for(i = 1; i <= m; i++)
{
    for(j = 1; j <= n; j++)
    {
        if(a[i-1] == b[j-1]) // 两个字符匹配
        {
            // 状态转移方程:当前长度 = 前一个位置长度 + 1
            sub[i][j] = sub[i-1][j-1] + 1;
            // 更新最长长度与结束索引
            if(sub[i][j] > max_len)
            {
                max_len = sub[i][j];
                end_index=i-1; // 记录a中匹配字符的索引(对应原字符串)
            }
        }
        else // 两个字符不匹配,重置为0(子串必须连续)
        {
            sub[i][j] = 0;
        }
    }
}
  • 核心思想:连续匹配才累加长度,不匹配则重置,这是区分 "公共子串(连续)" 与 "公共子序列(不连续)" 的关键;
  • 状态转移方程:sub[i][j] = sub[i-1][j-1] + 1,只有当前字符匹配时,才能继承前一个位置的匹配长度并加 1;
  • 关键记录:更新max_len的同时,记录end_index(最长子串在a中的结尾索引),为后续提取子串做准备。
步骤 3:提取最长公共子串

c

运行

ini 复制代码
if(max_len>0)// 存在有效公共子串
{
    int start_index = end_index-max_len+1; // 计算起始索引
    for(int k=0;k<max_len;k++)
    {
        result[k]=a[start_index+k]; // 从起始索引开始,逐字符复制
    }
    result[max_len]='\0'; // 手动添加结束符,避免乱码
}
return max_len;
  • 索引计算:start_index = end_index - max_len + 1,通过 "结束索引" 和 "最长长度" 反推起始索引,确保截取范围准确;
  • 关键细节:result[max_len] = '\0',由于result是字符数组,复制完有效字符后必须添加结束符,否则printf输出时会超出有效范围,读取到垃圾值导致乱码;
  • 返回值:将最长长度返回给主函数,用于后续结果输出。

3. 主函数:main(输入、调用、输出)

c

运行

scss 复制代码
int main()
{
    char a[100], b[100];
    char longest_sub[100]; // 存储最长公共子串
    printf("\n===== 最长公共子串(连续)计算 =====\n");
   
    printf("请输入第一个字符串:\n");
    fgets(a, sizeof(a), stdin);
    Remove_Newline(a); // 处理第一个字符串的换行符
    
    printf("请输入第二个字符串:\n");
    fgets(b, sizeof(b), stdin);
    Remove_Newline(b); // 【原代码遗漏】补充处理第二个字符串的换行符

    int max = Substring(a, b,longest_sub); // 调用核心算法

    printf("\n==== 计算结果 ====\n");
    printf("字符串1:%s\n", a);
    printf("字符串2:%s\n", b);
    printf("最长公共子串(连续)的长度:%d\n", max);
    
	if(max > 0)
    {
        printf("最长公共子串内容:%s\n", longest_sub);
    }
    else
    {
        printf("无公共子串!\n");
    }
    return 0;
}
  • 输入处理:使用fgets读取字符串,支持含空格的输入,比scanf更灵活;
  • 原代码修复:第二个字符串读取后未调用Remove_Newline,会导致b中保留换行符,可能影响对比结果,需补充该调用;
  • 结果输出:分 "有公共子串" 和 "无公共子串" 两种场景,输出信息清晰,用户体验友好。

三、原代码核心问题修复与完整可运行版本

1. 原代码核心问题

  • 第二个字符串输入后,未调用Remove_Newline处理换行符,可能导致对比误差;
  • 提取子串的循环注释遗漏(语法小问题),不影响运行但可读性降低;
  • 缺少部分健壮性判断(如输入空字符串的场景),可优化。

2. 修复后的完整代码

c

运行

scss 复制代码
/******************************
*文件名称:3.Common_Substring.c
*作者:czy
*邮箱:caozhiyang_0613@163.com
*创建日期:2025/12/30
*修改日期:2026/01/10
*文件功能:求两个字符串的最长公共子串(连续)长度,输出原字符串和计算结果
*修复点:1. 补充第二个字符串的换行符处理;2. 完善注释;3. 优化健壮性
*****************************/

#include<stdio.h>
#include<string.h>

/************************************************
*函数名称:Remove_Newline
*函数功能: 移除字符串末尾由fgets读取的换行符'\n',替换为字符串结束符'\0'
*输入参数: str - 字符指针,指向待处理的字符串(需保证字符串以'\0'结尾)
*返回参数: 无
*创建时间:2025/12/25
*修改时间:2025/12/30
*函数作者: czy
**************************************************/
void Remove_Newline(char *str)
{
    if(str == NULL)
    {
        return;
    }
    int len = strlen(str);
    if(len > 0 && str[len-1] == '\n')
    {
        str[len-1] = '\0';
    }
}

/************************************************
*函数名称:Substring
*函数功能: 计算两个字符串的最长公共子串(连续)长度,并提取子串内容
*输入参数: 
  - a:字符指针,指向第一个输入字符串
  - b:字符指针,指向第二个输入字符串
  - result:字符指针,指向存储最长公共子串的数组
*返回参数: int - 两个字符串的最长公共子串长度
*创建时间:2025/12/30
*修改时间:2026/01/10
*函数作者: czy
**************************************************/
int Substring(char *a, char *b, char *result)
{
    result[0] = '\0'; // 初始化结果数组,避免垃圾值
    int m = strlen(a);
    int n = strlen(b);
    int i, j;
    int sub[m+1][n+1]; // 动态规划状态表
    int max_len = 0;   // 最长公共子串长度
    int end_index = 0; // 最长子串在a中的结束索引

    // 第一步:初始化动态规划数组所有元素为0(边界条件)
    for(i = 0; i <= m; i++)
    {
        for(j = 0; j <= n; j++)
        {
            sub[i][j] = 0;
        }
    }

    // 第二步:动态规划填充数组,计算最长公共子串长度
    for(i = 1; i <= m; i++)
    {
        for(j = 1; j <= n; j++)
        {
            if(a[i-1] == b[j-1]) // 当前字符匹配
            {
                sub[i][j] = sub[i-1][j-1] + 1; // 状态转移方程
                // 更新最长长度与结束索引
                if(sub[i][j] > max_len)
                {
                    max_len = sub[i][j];
                    end_index = i-1;
                }
            }
            else // 当前字符不匹配,重置为0
            {
                sub[i][j] = 0;
            }
        }
    }

    // 第三步:提取最长公共子串内容
    if(max_len > 0)
    {
        int start_index = end_index - max_len + 1; // 计算起始索引
        for(int k = 0; k < max_len; k++)
        {
            result[k] = a[start_index + k]; // 逐字符复制子串
        }
        result[max_len] = '\0'; // 添加字符串结束符,避免乱码
    }

    return max_len;
}

/*************************************************
*函数名称: main
*函数功能: 主函数,程序入口;完成字符串输入、调用函数计算最长公共子串长度、输出结果
*输入参数: 无
*返回参数: 0 - 表示程序正常退出
*创建时间:2025/12/30
*修改时间:2026/01/10
*函数作者:czy
**************************************************/
int main()
{
    char a[100], b[100];
    char longest_sub[100];
    printf("\n===== 最长公共子串(连续)计算 =====\n");
   
    printf("请输入第一个字符串:\n");
    fgets(a, sizeof(a), stdin);
    Remove_Newline(a); // 处理第一个字符串的换行符
    
    printf("请输入第二个字符串:\n");
    fgets(b, sizeof(b), stdin);
    Remove_Newline(b); // 修复:补充处理第二个字符串的换行符

    int max = Substring(a, b, longest_sub); // 调用核心算法

    printf("\n==== 计算结果 ====\n");
    printf("字符串1:%s\n", a);
    printf("字符串2:%s\n", b);
    printf("最长公共子串(连续)的长度:%d\n", max);
    
    if(max > 0)
    {
        printf("最长公共子串内容:%s\n", longest_sub);
    }
    else
    {
        printf("无公共子串!\n");
    }
    return 0;
}

四、运行结果示例

示例 1:存在有效公共子串

plaintext

diff 复制代码
===== 最长公共子串(连续)计算 =====
请输入第一个字符串:
hello world
请输入第二个字符串:
world hello

==== 计算结果 ====
字符串1:hello world
字符串2:world hello
最长公共子串(连续)的长度:5
最长公共子串内容:hello

示例 2:无公共子串

plaintext

diff 复制代码
===== 最长公共子串(连续)计算 =====
请输入第一个字符串:
abc123
请输入第二个字符串:
def456

==== 计算结果 ====
字符串1:abc123
字符串2:def456
最长公共子串(连续)的长度:0
无公共子串!

示例 3:含空格的公共子串

plaintext

csharp 复制代码
===== 最长公共子串(连续)计算 =====
请输入第一个字符串:
my name is czy
请输入第二个字符串:
his name is tom

==== 计算结果 ====
字符串1:my name is czy
字符串2:his name is tom
最长公共子串(连续)的长度:8
最长公共子串内容: name is 

五、核心知识点总结与扩展

1. 核心知识点

  • 动态规划核心:用二维数组记录中间状态,通过状态转移方程避免重复计算,时间复杂度为 O (m×n)(m、n 为两个字符串长度);
  • 字符串操作细节:fgets的换行符处理、'\0'的手动添加,是避免乱码与越界的关键;
  • 索引计算:通过 "结束索引" 和 "最长长度" 反推 "起始索引",是提取子串的核心技巧。

2. 功能扩展建议

  • 支持多个最长公共子串:当存在多个长度相同的最长公共子串时,全部提取并输出;
  • 不区分大小写对比:在字符匹配前,将a[i-1]b[j-1]统一转为大写(toupper)或小写(tolower);
  • 优化空间复杂度:动态规划数组仅需两行即可完成计算(当前行只依赖上一行),可将二维数组优化为一维数组,节省内存空间。

六、总结

这份代码完美诠释了 "算法 + 字符串操作" 的结合,动态规划的思想不仅适用于最长公共子串问题,还可推广到最长公共子序列、编辑距离等经典字符串问题。掌握本文的核心逻辑,不仅能解决实际开发中的字符串对比需求,还能为后续算法学习打下坚实基础。其中,对字符串结束符、换行符的细节处理,更是 C 语言编程中 "严谨性" 的体现,值得反复揣摩和借鉴。

相关推荐
Cache技术分享2 小时前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
用户948357016512 小时前
拒绝 try-catch:如何设计全局通用的异常拦截体系?
后端
golang学习记2 小时前
Go 1.22 隐藏彩蛋:cmp.Or —— 让“默认值”写起来像呼吸一样自然!
后端
阿里巴巴P8高级架构师2 小时前
从0到1:用 Spring Boot 4 + Java 21 打造一个智能AI面试官平台
java·后端
桦说编程2 小时前
并发编程踩坑实录:这些原则,帮你少走80%的弯路
java·后端·性能优化
小杨同学492 小时前
C 语言实战:枚举类型实现数字转星期(输入 1~7 对应星期几)
前端·后端
用户8307196840822 小时前
Shiro登录验证与鉴权核心流程详解
spring boot·后端
码头整点薯条2 小时前
基于Java实现的简易规则引擎(日常开发难点记录)
java·后端
Codelinghu2 小时前
「 LLM实战 - 企业 」构建企业级RAG系统:基于Milvus向量数据库的高效检索实践
人工智能·后端·llm