哈喽,各位小伙伴们。南京今天终于停雨了呢,虽然是个阴天,也是很有感觉的哦。有没有会莫文蔚《阴天》的小伙伴?
阴天,在不开灯的房间,让所有思绪一点一点沉淀。
是的,阴天就是适合一个人在房间里面沉淀的天气。昨天还和小伙伴们谈到现在大家因为谈恋爱而产生快乐依赖于对方的现象,在这儿分享给大家一句话:想要谈恋爱,咱得先在感情上能自我满足了再去。
楼主就希望借助这些算法题来沉淀和提升自己。因为楼主脑子不是很好使,天子不聪颖,就只能借助于后天的努力了啊。
说多了,楼主这篇文章可能会一直处于更新状态。因为我想把字符串中常见的面试算法提都给整理一下,今天暂时只准备了两道题。第一道是字符串移位包含的问题,第二道题是字符串相似度(距离)问题,后面再继续更新。Are you ready?
题目一:字符串移位包含
问题描述:
给定两个字符串s1和s2,要求判定s2是否能够被通过循环移位得到的字符串包含。例如,给定s1 = AABCD和s2 = CDAA,返回true;给定s1 = ABCD和s2 = ACBD,返回false.
问题解答:
解法一:枚举
暴力求解是我们的第一反应,题目要求判定s2是否能被通过循环移位得到的字符串包含,那我们就把s1和它所有的循环移位全部求出来,一个一个判断是否存在字符串包含。话不多说,见代码:
#include <iostream>
#include <string>
using namespace std;
bool isContain(char *src, char* dest);
void main()
{
char s1[] = "AABCD";
char s2[] = "CDAA";
int len = strlen(s1); //总共可移位length-1次
char temp = s1[len - 1];
if (isContain(s1, s2))
{
cout << "包含" << endl;
return;
}
for (int i = 0; i < len - 1; i++)
{
for (int j = len - 1; j > 0; j--)
{
s1[j] = s1[j - 1];
}
s1[0] = temp;
temp = s1[len - 1];
//一次右移完毕
if (isContain(s1, s2))
{
cout << "包含" << endl;
return;
}
}
cout << "不包含" << endl;
system("pause");
}
bool isContain(char *src, char* dest)
{
if (NULL != strstr(src, dest))
return true;
return false;
}
解法二:规律分析法
我们队循环移位之后的结果进行分析,以s1 = “ABCD”而言,我们将ABCD所有的移位之后的字符串序列写出来,如下所示:
ABCD -> DABC -> CDAB -> BCDA
可以发现,s1及其所有循环移位的序列都是ABCDABCD的子序列。那么如果s2是s1的循环移位子序列,那么s2肯定是s1s1的子序列。那么原题就转换成判断s2是不是s1s1的子序列了,不需要再进行循环移位的操作了。有同学肯定会疑问,属于s1s1的子序列,并不一定是s1循环移位的子序列啊。但是同学们可以试一试,属于s1s1的子序列,并且长度等于或者小于s1的子序列一定是s1的循环移位子序列。那么代码就可以变成:
#include <iostream>
#include <string>
using namespace std;
bool isContain(char *src, char* dest, int lenSrc, int lenDest);
void main()
{
char s1[] = "AABCD";
char s2[] = "CDAAF";
int len1 = strlen(s1); //总共可移位length-1次
int len2 = strlen(s2);
if (isContain(s1, s2, len1, len2))
cout << "包含" << endl;
else
cout << "不包含" << endl;
system("pause");
}
bool isContain(char *src, char* dest, int lenSrc, int lenDest)
{
if (lenDest > lenSrc)
return false;
char *doubleSrc = new char[2 * lenSrc + 1]();
strcpy_s(doubleSrc, 2 * lenSrc + 1, src);
strcat_s(doubleSrc, 2 * lenSrc + 1, src);
doubleSrc[2 * lenSrc + 1] = ‘\0‘;
if (NULL == strstr(doubleSrc, dest))
return false;
return true;
}
题目二:计算字符串相似度
问题描述:
为计算将str1变换到str2所需最小操作步骤,必须先对变换操作进行定义:
1.修改一个字符(如把“a”替换为“g”);
2.增加一个字符(如把“abcd”变为“abcdz”);
3.删除一个字符(如把“travelling”变为“traveling”);
字符串变换过程中执行了上述三个操作之间的任一操作,则两字符串间距离就加1。例如将上文中的str1变换到str2,即“abcd”到“gbcdz”,通过“abcd”->(操作1)->“gbcd”->(操作2)->“gbcdz”,需进行一次修改和一次添加字符操作,故两字符串距离为2,那么字符串相似度则为距离+1的倒数,即1/3=0.333。这是由俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。因此也叫Levenshtein Distance。
问题解答:
解法一:递归法
很多时候看起来不知道怎么下手的题目,可以尝试一下递归法。递归法的思想就是把大问题不断转换成小问题。我们看下面一个例子,A=xabcdae和B=xfdfa,先比较第一个元素,相等,那么直接进入到下一个字符的比较。如果不相等,我们可以进行如下的操作,使得这个不相等的字符串相等起来。
操作1:删除A串的第一个字符,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离;
操作2:删除B串的第一个字符,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;
操作3:将A的第一个字符修改为B的第一个字符,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;
操作4:将B的第一个字符修改为A的第一个字符,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;
操作5:增加B的第一个字符串到A的第一个字符串前面,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;
操作6:增加A的第一个字符串到B的第一个字符串前面,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离;
我们并不在乎具体需要什么操作(删除,修改或添加),我们只关心最后的距离,所以我们将上面六种操作总结一下就是
操作1:一步操作之后,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离。比如这个操作可以是删除A串的第一个字符或者是将B的第一个字符修改为A的第一个字符,但是我们不关心具体的操作;
操作2:一步操作之后,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;
操作3:一步操作之后,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;
递归结束的条件:
上面的操作1-操作3只是告诉我们写程序的时候有这么三个分支,递归总是要有退出或者返回条件的啊。所以下面我们讨论一下,这个递归程序几个技术时的情况。
由上面的分析,我们可以大概知道,肯定要传入的参数是,A字符串和B字符串的地址,然后A/B的起始和末尾索引,假设分别为aStartIndex/bStartIndex和aEndIndex/bEndIndex。结束的三种情况:
情况一:A字符串全部遍历完了,那么step保留的是前面操作的步骤,bEndIndex - bStartIndex + 1保留的时候的字符串的距离,所以返回bEndIndex - bStartIndex + step + 1
情况二:B字符串全部遍历完了,那么step保留的是前面操作的步骤,bEndIndex - bStartIndex + 1保留的时候的字符串的距离,所以返回bEndIndex - bStartIndex + step + 1
下面直接看代码吧:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int min2(int a, int b);
int min3(int a, int b, int c);
int calc(char* a, char* b, int aStartIndex, int aEndIndex, int bStartIndex, int bEndIndex, int step);
int main(void)
{
char *a = "abdd";
char *b = "aebdd";
int step = 0;
printf("%d",calc(a, b, 0, strlen(a) - 1, 0, strlen(b) - 1, step));
return EXIT_SUCCESS;
}
int calc(char* a, char* b, int aStartIndex, int aEndIndex, int bStartIndex, int bEndIndex, int step)
{
int d1,d2,d3;
if (aStartIndex > aEndIndex)
{
return bEndIndex - bStartIndex + step + 1;
}
if (bStartIndex > bEndIndex)
{
return aEndIndex - aStartIndex + step + 1;
}
if (a[aStartIndex] == b[bStartIndex])
{
//如果该位置处两个字符串的字符相等,那么perfect,直接进入下一个位置的比较
return calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex + 1, bEndIndex, step);
}
else
{
step ++;
d1 = calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex, bEndIndex, step);
d2 = calc(a, b, aStartIndex, aEndIndex, bStartIndex + 1, bEndIndex, step);
d3 = calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex + 1, bEndIndex, step);
return min3(d1, d2, d3);
}
}
int min2(int a, int b)
{
return a>b ? b : a;
}
int min3(int a, int b, int c)
{
if(a > b)
a = b;
if(a > c)
a = c;
return a;
}
很显然,递归方法最为诟病的就是重复计算的问题,用递归方法的很多都有这个问题。那我们这时候呢,又可以拿出我们的第二把刷子啦,叫做”动态递归法“,动态递归的主要原理就是保存小规模时候程序运行的结果,然后计算大规模数据的时候就可以直接调用小规模的结果了。
解法二:动态递归法
设str1=“abcd”,str2=“gbcdz”,定义一个二维数组d[][],d[i][j]表示str1中取前i个字符和str2中取前j个字符的最短距离,例如d[3][2]表示“abc”到“gb”的最短距离。
d[i][j]的计算规则有三条:
- 来自d[i - i][j - 1],即 “str1的前i-1个字符组成的子串” 到 “str2的前j-1个字符组成的子串” 的最小距离,此时如果str1[i] = str2[j],则最短距离不变,否则最短距离加1(即把str1[i]变为str2[j] ),所以d[i][j] = d[i - 1][j - 1] + (str1[i] == str2[j] ? 0 : 1)
- 来自d[i - 1][j],即 “A的前i-1个字符组成的子串” 到 “B的前j个字符组成的子串” 的编辑距离。此时删除在A的第i个位置上的字符即可,所以d[i][j] = d[i - 1][j] + 1
- 来自d[i][j - 1], 即 “A的前i个字符组成的子串” 到 “B的前j-1个字符组成的子串” 的编辑距离。此时在A的子串后面添加一个字符B[j]即可,所以d[i][j] = d[i][j - 1] + 1
于是状态转移方程:d[i][j] = min (d[i - 1][j - 1] + (str1[i] == str2[j] ? 0 : 1) , d[i - 1][j] + 1 , d[i][j - 1] + 1)
例如str1=“abcd”,str2=“gbcdz”的d[][]就为(注意i,j的取值范围):
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int min(int a, int b, int c);
int calc(char* a, char* b);
int main(void)
{
printf("%d",calc("abcd", "gbcdz"));
return EXIT_SUCCESS;
}
int min(int a, int b, int c)
{
if(a > b)
a = b;
if(a > c)
a = c;
return a;
}
int calc(char* a, char* b)
{
int lenA = strlen(a);
int lenB = strlen(b);
int **d = new int*[lenA + 1];
for (int i = 0; i < lenA + 1; i++)
{
d[i] = new int[lenB + 1];
}
for(int i = 0;i <= lenA; ++i)
d[i][0] = i;
for(int i = 0; i <= lenB; ++i)
d[0][i] = i;
for(int i = 1; i <= lenA; ++i)
for(int j = 1; j <= lenB; ++j)
d[i][j] = min(d[i-1][j-1] + (a[i-1] == b[j-1] ? 0:1), d[i-1][j]+1, d[i][j-1]+1);
return d[lenA][lenB];
}
题目三:字符串逆序
未完待续,敬请期待
题目四:如何统计一行字符串中有多少个单词?
未完待续, 敬请期待
题目五:自己编写字符串库函数strlen(), strcat(), strcpy()。
未完待续, 敬请期待
题目六:如何找出一个字符串中第一个只出现一次的字符
未完待续, 敬请期待
题目七:如何输出字符串所有组合
未完待续, 敬请期待
题目八:字符转整数及整数转字符串
未完待续, 敬请期待