C语言求解最长公共子字符串问题及相关的算法分析

所属分类: 软件编程 / C 语言 阅读数: 71
收藏 0 赞 0 分享

题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。请编写一个函数,输入两个字符串,求它们的最长公共子序列,并打印出最长公共子序列。
例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子序列,则输出它们的长度4,并打印任意一个子序列。
分析:求最长公共子序列(Longest Common Subsequence, LCS)是一道非常经典的动态规划题,因此一些重视算法的公司像MicroStrategy都把它当作面试题。

完整介绍动态规划将需要很长的篇幅,因此我不打算在此全面讨论动态规划相关的概念,只集中对LCS直接相关内容作讨论。如果对动态规划不是很熟悉,请参考相关算法书比如算法讨论。

考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bn-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。不难证明有以下性质:

(1) 如果am-1==bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;

(2) 如果am-1!=bn-1,则若zk-1!=am-1时,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;

(3) 如果am-1!=bn-1,则若zk-1!=bn-1时,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。

这样,在找A和B的公共子序列时,如果有am-1==bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。

求解:
引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定输出最长公共字串时搜索的方向。
我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] == Y[j]还是X[i] != Y[j],就可以计算出c[i][j]。

问题的递归式写成:

201664173807574.gif (673×287)

回溯输出最长公共子序列过程:    

201664173831586.gif (674×501)

算法分析:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m + n)。

完整的实现代码如下:

/** 
找出两个字符串的最长公共子序列的长度 
** author :liuzhiwei  
** data  :2011-08-15 
**/  
#include "stdio.h" 
#include "string.h" 
#include "stdlib.h" 
int LCSLength(char* str1, char* str2, int **b) 
{ 
  int i,j,length1,length2,len; 
  length1 = strlen(str1); 
  length2 = strlen(str2); 
 
  //双指针的方法申请动态二维数组 
  int **c = new int*[length1+1];   //共有length1+1行 
  for(i = 0; i < length1+1; i++) 
    c[i] = new int[length2+1];   //共有length2+1列 
 
  for(i = 0; i < length1+1; i++) 
    c[i][0]=0;    //第0列都初始化为0 
  for(j = 0; j < length2+1; j++) 
    c[0][j]=0;    //第0行都初始化为0 
 
  for(i = 1; i < length1+1; i++) 
  { 
    for(j = 1; j < length2+1; j++) 
    { 
      if(str1[i-1]==str2[j-1])  //由于c[][]的0行0列没有使用,c[][]的第i行元素对应str1的第i-1个元素 
      { 
        c[i][j]=c[i-1][j-1]+1; 
        b[i][j]=0;     //输出公共子串时的搜索方向 
      } 
      else if(c[i-1][j]>c[i][j-1]) 
      { 
        c[i][j]=c[i-1][j]; 
        b[i][j]=1; 
      } 
      else 
      { 
        c[i][j]=c[i][j-1]; 
        b[i][j]=-1; 
      } 
    } 
  } 
  /* 
  for(i= 0; i < length1+1; i++) 
  { 
  for(j = 0; j < length2+1; j++) 
  printf("%d ",c[i][j]); 
  printf("\n"); 
  } 
  */ 
  len=c[length1][length2]; 
  for(i = 0; i < length1+1; i++)  //释放动态申请的二维数组 
    delete[] c[i]; 
  delete[] c; 
  return len; 
} 
void PrintLCS(int **b, char *str1, int i, int j) 
{ 
  if(i==0 || j==0) 
    return ; 
  if(b[i][j]==0) 
  { 
    PrintLCS(b, str1, i-1, j-1);  //从后面开始递归,所以要先递归到子串的前面,然后从前往后开始输出子串 
    printf("%c",str1[i-1]);    //c[][]的第i行元素对应str1的第i-1个元素 
  } 
  else if(b[i][j]==1) 
    PrintLCS(b, str1, i-1, j); 
  else 
    PrintLCS(b, str1, i, j-1); 
} 
 
int main(void) 
{ 
  char str1[100],str2[100]; 
  int i,length1,length2,len; 
  printf("请输入第一个字符串:"); 
  gets(str1); 
  printf("请输入第二个字符串:"); 
  gets(str2); 
  length1 = strlen(str1); 
  length2 = strlen(str2); 
  //双指针的方法申请动态二维数组 
  int **b = new int*[length1+1]; 
  for(i= 0; i < length1+1; i++) 
    b[i] = new int[length2+1]; 
  len=LCSLength(str1,str2,b); 
  printf("最长公共子序列的长度为:%d\n",len); 
  printf("最长公共子序列为:"); 
  PrintLCS(b,str1,length1,length2); 
  printf("\n"); 
  for(i = 0; i < length1+1; i++)  //释放动态申请的二维数组 
    delete[] b[i]; 
  delete[] b; 
  system("pause"); 
  return 0; 
} 

程序的效果图如下:

201664174047130.gif (674×285)

第二种方法为:

/** 
找出两个字符串的最长公共子序列的长度 
** author :liuzhiwei  
** data  :2011-08-15 
**/  
#include "stdio.h" 
#include "string.h" 
#include "stdlib.h" 
int LCSLength(char* str1, char* str2)  //求得两个字符串的最大公共子串长度并输出公共子串 
{ 
  int i,j,length1,length2; 
  length1 = strlen(str1); 
  length2 = strlen(str2); 
 
  //双指针的方法申请动态二维数组 
  int **c = new int*[length1+1];   //共有length1+1行 
  for(i = 0; i < length1+1; i++) 
    c[i] = new int[length2+1];   //共有length2+1列 
 
  for(i = 0; i < length1+1; i++) 
    c[i][0]=0;    //第0列都初始化为0 
  for(j = 0; j < length2+1; j++) 
    c[0][j]=0;    //第0行都初始化为0 
 
  for(i = 1; i < length1+1; i++) 
  { 
    for(j = 1; j < length2+1; j++) 
    { 
      if(str1[i-1]==str2[j-1])  //由于c[][]的0行0列没有使用,c[][]的第i行元素对应str1的第i-1个元素 
        c[i][j]=c[i-1][j-1]+1; 
      else if(c[i-1][j]>c[i][j-1]) 
        c[i][j]=c[i-1][j]; 
      else 
        c[i][j]=c[i][j-1]; 
    } 
  } 
 
  //输出公共子串 
  char s[100]; 
  int len,k; 
  len=k=c[length1][length2]; 
  s[k--]='\0'; 
  i=length1,j=length2; 
  while(i>0 && j>0) 
  { 
    if(str1[i-1]==str2[j-1]) 
    { 
      s[k--]=str1[i-1]; 
      i--; 
      j--; 
    } 
    else if(c[i-1][j]<c[i][j-1]) 
      j--; 
    else 
      i--; 
  } 
  printf("最长公共子串为:"); 
  puts(s); 
 
  for(i = 0; i < length1+1; i++)  //释放动态申请的二维数组 
    delete[] c[i]; 
  delete[] c; 
  return len; 
} 
 
int main(void) 
{ 
  char str1[100],str2[100]; 
  int length1,length2,len; 
 
  printf("请输入第一个字符串:"); 
  gets(str1); 
  printf("请输入第二个字符串:"); 
  gets(str2); 
  length1 = strlen(str1); 
  length2 = strlen(str2); 
  len=LCSLength(str1,str2); 
  printf("最长公共子串的长度为:%d\n",len); 
  system("pause"); 
  return 0; 
} 

       问题拓展:设A、B、C是三个长为n的字符串,它们取自同一常数大小的字母表。设计一个找出三个串的最长公共子序列的O(n^3)的时间算法。
       思路:跟上面的求2个字符串的公共子序列是一样的思路,只不过这里需要动态申请一个三维的数组,三个字符串的尾字符不同的时候,考虑的情况多一些而已。
/** 
找出三个字符串的最长公共子序列的长度 
** author :liuzhiwei  
** data  :2011-08-15 
**/  
#include "stdio.h" 
#include "string.h" 
#include "stdlib.h" 
int max1(int m,int n) 
{ 
  if(m>n) 
    return m; 
  else 
    return n; 
} 
int max2(int x,int y,int z,int k,int m,int n) 
{ 
  int max=-1; 
  if(x>max) 
    max=x; 
  if(y>max) 
    max=y; 
  if(z>max) 
    max=z; 
  if(k>max) 
    max=k; 
  if(m>max) 
    max=m; 
  if(n>max) 
    max=n; 
  return max; 
} 
int LCSLength(char* str1, char* str2, char* str3)  //求得三个字符串的最大公共子序列长度并输出公共子序列 
{ 
  int i,j,k,length1,length2,length3,len; 
  length1 = strlen(str1); 
  length2 = strlen(str2); 
  length3 = strlen(str3); 
 
  //申请动态三维数组 
  int ***c = new int**[length1+1];   //共有length1+1行 
  for(i = 0; i < length1+1; i++) 
  { 
    c[i] = new int*[length2+1];   //共有length2+1列 
    for(j = 0; j<length2+1; j++) 
      c[i][j] = new int[length3+1]; 
  } 
 
  for(i = 0; i < length1+1; i++) 
  { 
    for(j = 0; j < length2+1; j++) 
      c[i][j][0]=0; 
  } 
  for(i = 0; i < length2+1; i++) 
  { 
    for(j = 0; j < length3+1; j++) 
      c[0][i][j]=0; 
  } 
  for(i = 0; i < length1+1; i++) 
  { 
    for(j = 0; j < length3+1; j++) 
      c[i][0][j]=0;   
  } 
 
  for(i = 1; i < length1+1; i++) 
  { 
    for(j = 1; j < length2+1; j++) 
    { 
      for(k = 1; k < length3+1; k++) 
      { 
        if(str1[i-1]==str2[j-1] && str2[j-1]==str3[k-1]) 
          c[i][j][k]=c[i-1][j-1][k-1]+1; 
        else if(str1[i-1]==str2[j-1] && str1[i-1]!=str3[k-1]) 
          c[i][j][k]=max1(c[i][j][k-1],c[i-1][j-1][k]); 
        else if(str1[i-1]==str3[k-1] && str1[i-1]!=str2[j-1]) 
          c[i][j][k]=max1(c[i][j-1][k],c[i-1][j][k-1]); 
        else if(str2[j-1]==str3[k-1] && str1[i-1]!=str2[j-1]) 
          c[i][j][k]=max1(c[i-1][j][k],c[i][j-1][k-1]); 
        else 
        { 
          c[i][j][k]=max2(c[i-1][j][k],c[i][j-1][k],c[i][j][k-1],c[i-1][j-1][k],c[i-1][j][k-1],c[i][j-1][k-1]); 
        } 
      } 
    } 
  } 
  len=c[length1][length2][length3]; 
  for(i = 1; i < length1+1; i++)     //释放动态申请的三维数组 
  { 
    for(j = 1; j < length2+1; j++) 
      delete[] c[i][j]; 
    delete[] c[i]; 
  } 
  delete[] c; 
  return len; 
} 
 
int main(void) 
{ 
  char str1[100],str2[100],str3[100]; 
  int len; 
 
  printf("请输入第一个字符串:"); 
  gets(str1); 
  printf("请输入第二个字符串:"); 
  gets(str2); 
  printf("请输入第三个字符串:"); 
  gets(str3); 
  len=LCSLength(str1,str2,str3); 
  printf("最长公共子序列的长度为:%d\n",len); 
  system("pause"); 
  return 0; 
} 

程序的效果图如下:

201664175534056.gif (673×287)

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多