<字符串移位包含问题>和<字符串相似度问题>

字符串处理的问题无论是做ACM题,还是实际项目开发中都是经常遇到的问题,字符串处理问题往往用到很多有意思的经典算法,个人觉得,对字符串的处理技巧能看出一个将要毕业的大学生的编码能力、算法基础和遇到问题的思考能力。最近就遇到两个字符串处理的问题:(1)字符串移位包含问题 (2)计算字符串相似度的问题。

1.字符串的移位包含问题


问题定义 :给定两个字符串str1和str2,所谓的移位包含就是说如果str2属于str1经过移位得到的新串的子串,则说str1移位包含str2,那么什么是str1移位得到的新串呢?比如ABCD经过移位1位可以得到的新串是BCDA,这种移位方式实际上是循环左移。这时如果str2 = “DA”,那么就可以说str1移位包含str2.

分析问题:我们可以枚举str1经过循环移位得到的新串str_tmp,然后如果str2是str_tmp的子串那么就满足了移位包含的问题了,但是这种思路效率并不高,整个算法的时间复杂度包括:循环移位的时间复杂度和判断是不是子串的复杂度(循环移位算法可以参见:数组循环移位中的学问数组循环移位中的学问)。这显然不是我们要的解题思路了,实际上无论字符串循环移位几位,最终得到的新串str_tmp都是经过两个str1连接得到的,比如ABCD经过循环移位可以得到:BCDA、CDAB、DABC、ABCD,实际上这四个经str1循环移位得到的新串都是str1str1 = “ABCDABCD”的子串。这时,您明白了吧,我们就可以省掉循环移位的复杂度了,使得整个算法的时间复杂度提高。

现在我们可以着重考虑判断str2是否是str1str1的子串的算法了,实际上在头文件<string.h>已经提供了strstr(str1,str2)函数,用来完成我们需要的功能,但是strstr函数的内部代码是这样的:

View Code
 1 char *strstr( const char *s1, const char *s2 )   
 2 {   
 3     if ( !(len2 = strlen(s2)) )   
 4         return (char *)s1;   
 5     for(;*s1;++s1)   
 6     {   
 7         if ( *s1 == *s2 && strncmp(s1,s2,len2)==0 )   
 8             return (char *)s1;   
 9     }   
10     return NULL;   
11 } 

可以看出来,这个函数采用的判断方法效率并不高,如果要进行大量的这种str2是不是str1str1的子串的判断或者str1和str2的长度比较长,则用这个算法就不是很好了,我们就要想别的算法了,实际上很自然就能想到快速模式匹配算法(KMP)了,这是基本每个数据结构的书都基本会介绍的算法,具体可以参考刘大有《数据结构》第2版,或者本人的另一篇随笔:快速模式匹配算法(KMP)相信通过这些资料,把完整的代码写出来应该不是什么问题了,我这里就不写了。

但是可能大家会发现str1str1接在一起要花费O(N)的辅助空间啊,N是str1的长度,这个实际上是存在的,但是我们可以不是真正意义上的合并,这只是一种思想,实际操作的时候,比如要访问str1str1[N+3],我们可以让其实际访问的是str1[3]来完成啊,这样就不需要多增加O(N)的辅助空间了。 

2.计算字符串的相似度


问题定义:现存在两个字符串str1和str2,要想把这两个字符变得相同,可以有下面的操作:1.修改一个字符 2.增加一个字符 3.删除一个字符。比如:str1 = abc和str2 = ab,就可以通过增加或者删除一个c使得这两个字符变得相同;还比如str1 = abc,str2 = abd,这时需要修改c -> d或者修改d->c都可以使得两个字符串变得相同。可以看出这两个简单的例子,所需要的操作都是1,不妨称之为距离是1,实际上这里所说的相似度 = 距离+ 1 的倒数。那么如何计算给定的两个字符串的相似度呢?

分析问题:实际上str1和str2比较若首字母相同,则只需要比较str1[1~len1-1] 和 str2[1~len2-1],不影响距离的变化。若str1和str2的首字母并不相同,那么我们科进行下面的6种操作的一种:

1.删除str1的第一个字符,然后继续计算str1[1~len1-1]和str2[0~len2-1] 之间的距离。

2.删除str2的第一个字符,然后继续计算str1[0~len1-1]和str2[1~len2-1]之间的距离 。

3.修改str1的第一个字符等于str2的第一个字符,然后继续计算str1[1~len1-1]和str2[1~len2-1] 之间的距离。

4.修改str2的第一个字符等于str1的第一个字符,然后继续计算str1[1~len1-1]和str2[1~len2-1] 之间的距离。

5.在str1最前面增加一个字符使其等于str2的第一个字符,然后继续计算str1[0~len1-1]和str2[1~len2-1]之间的距离 。

6.在str2最前面增加一个字符使其等于str1的第一个字符,然后继续计算str1[1~len1-1]和str2[0~len2-1] 之间的距离。

我们发现,无论是上述的哪一种变动方式,最后需要继续计算的情况只有三种:(1)计算str1[1~len1-1]和str2[1~len2-1] 之间的距离d1 (2)计算str1[1~len1-1]和str2[0~len2-1] 之间的距离d2 (3)计算str1[0~len1-1]和str2[1~len2-1]之间的距离d3,并且str1[0~len1-1]和str2[0~len2-1]之间的距离等于min(d1,d2,d3)+ 1,加1是因为做了一次操作。很显然了,这是一个递归程序:

代码:

CalStringDistance
 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 int MinValue(int d1,int d2,int d3)
 6 {
 7     int min = d1;
 8     if(d2 < min)
 9     {
10         min = d1;
11     }
12     if(d3 < min)
13     {
14         min = d3;
15     }
16     return min;
17 }
18 
19 int CalStringDistance(string strA,int pABegin,int pAEnd,string strB,int pBBegin,int pBEnd)
20 {
21     if(pABegin > pAEnd)
22     {
23         if(pBBegin > pBEnd)
24         {
25             return 0;
26         }
27         else
28         {
29             return pBEnd - pBBegin + 1;
30         }
31     }
32     if(pBBegin > pBEnd)
33     {
34         if(pABegin > pAEnd)
35         {
36             return 0;
37         }
38         else
39         {
40             return pAEnd - pABegin + 1;
41         }
42     }
43     if(strA[pABegin] == strB[pBBegin])//当前的字母相同,距离不必+1
44     {
45         return CalStringDistance(strA,pABegin+1,pAEnd,strB,pBBegin+1,pBEnd);
46     }
47     else
48     {
49         //d1,d2,d3保存可能三种递归情况所得到的距离
50         int d1 = CalStringDistance(strA,pABegin+1,pAEnd,strB,pBBegin,pBEnd);
51         int d2 = CalStringDistance(strA,pABegin,pAEnd,strB,pBBegin+1,pBEnd);
52         int d3 = CalStringDistance(strA,pABegin+1,pAEnd,strB,pBBegin+1,pBEnd);
53         return MinValue(d1,d2,d3)+1;
54     }
55 }
56 
57 int main()
58 {
59     string strA = "abe";
60     string strB = "abcd";
61     cout<<CalStringDistance(strA,0,2,strB,0,2)<<endl;
62     return 0;
63 }
64     

总结:实际上在递归的过程中进行了大量的重复计算,比如我们要进行CalStringDistance(strA,1,2strB,1,2),这时就会递归调用函数CalStringDistance(strA,1,2,strB,2,2)和CalStringDistance(strA,2,2,strB,2,2),那么在执行CalStringDistance(strA,1,2,strB,2,2)的时候也难免调用CalStringDistance(strA,2,2,strB,2,2),并且随着strA和strB的长度增加,这种重复会很多,也因此导致算法时间复杂度不高。那么如何避免这种重复计算呢??

1.可以利用备忘录的思想,我们通过二维数组int flag[][]记录当前CalStringDistance(strA,pABegin,pAEnd,strB,pBBegin,pBEnd)的是否调用过,首先将数组初始化为-1,当第一次调用函数CalStringDistance(string strA,2,2,strB,2,2)的时候,将flag[2][2]赋值为函数的计算,前一个代表pABegin,后一个2代表pBBegin。这样下次调用CalStringDistance(string strA,2,2,strB,2,2)的时候发现flag[2][2]并不等于-1,就知道CalStringDistance(string strA,2,2,strB,2,2)已经计算过了,可以直接从flag[2][2]获得函数的结果。修改后的代码和原来的代码大同小异!!可是大家也发现了,这种方式多引入了O(len1*len2)的空间复杂度,不过随着strA和strB的长度增加,如果不这样做一定会大大影响效率的,是一定。

2. 也可以用递推的方法,既然已经知道了递推关系式,使用递推应该是能解决问题的,首先初始化cal[2][2],cal[1][2],cal[2][1],cal这个二维数组的下标也和pABegin和pBBegin对应,然后计算CalStringDistance(string strA,1,2,strB,1,2),实际上计算cal[1][1],我们就可以利用cal[2][2],cal[1][2],cal[2][1]得到了,这样不断递推,就能得到最终结果,这种方式避免递归程序先天的劣势。

实际上上面的两种方法是完成动态规划的典型思路。动态规划真实太伟大啦,值得学习!!! 


如果您觉得文章内容对您有用,不妨动一下手中的鼠标,轻击一下右下角的“推荐”,您的鼓励是我前进的动力~~~~

如果您觉得文章内容存在问题,不妨在评论中直言不讳,好的文章也需要热心而聪明的读者的参与~~~~


原文地址:https://www.cnblogs.com/BeyondAnyTime/p/2604733.html