字符串常见算法题
左旋转字符串
在字符串上定义反转的操作XT,即把X的所有字符反转(如X="abc",那么XT="cba")。如果将一个字符串分成两部分,X和Y两个部分,那么我们可以得到下面的结论:(XTYT)T=YX。
定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。 如把字符串abcdef左旋转2位得到字符串cdefab。按照字符串反转的结论,X="ab",Y="cdef",要想把XY变成YX,只要使用YX=(XTYT)T 即可,也就是分别对X、Y进行反转,然后再整体反转一次即可。
翻转句子中单词的顺序
问题描述:
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。
例如输入“I am a student.”,则输出“student. a am I”。
思路:
借鉴字符串旋转的方法,我们先颠倒句子中的所有字符。这时,不但翻转了句子中单词的顺序,而且单词内字符也被翻转了。我们再颠倒每个单词内的字符。由于单词内的字符被翻转两次,因此顺序仍然和输入时的顺序保持一致。还是以上面的输入为例子。翻转“I am a student.”中所有字符得到“.tneduts a ma I”,再翻转每个单词中字符的顺序得到“students. a am I”,正是符合要求的输出。
void Reverse(char *pBegin, char *pEnd)
{
if (pBegin == NULL || pEnd == NULL)
return;
char temp;
while (pBegin < pEnd) {
temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
pBegin ++, pEnd --;
}
}
char* ReverseSentence(char *pData)
{
if(pData == NULL)
return NULL;
char *pBegin = pData;
char *pEnd = pData;
while(*pEnd != '\0')
pEnd ++;
pEnd--;
// Reverse the whole sentence
Reverse(pBegin, pEnd);
// Reverse every word in the sentence
pBegin = pEnd = pData;
while(*pBegin != '\0') {
if(*pBegin == ' ') {
pBegin ++;
pEnd ++;
continue;
// A word is between with pBegin and pEnd, reverse it
} else if(*pEnd == ' ' || *pEnd == '\0') {
Reverse(pBegin, --pEnd);
pBegin = ++pEnd;
} else {
pEnd ++;
}
}
return pData;
}
字符串的编辑距离
将两个不同的字符串变得相同,具体的操作方法为:
1、修改一个字符(如 把"a"替换为"b");
2、增加一个字符(如 把"abdd"变为"aebdd");
3、删除一个字符(如 把"travelling"变为"traveling");
首先,两个字符串的距离肯定不超过它们的长度之和(可以通过删除操作把两个串都转化为空串)。
如果两个字符串A和B的第一个字符相同,则问题转化为求A[2:lenA]和B[2:lenB]的编辑距离;
如果两个字符串A和B的第一个字符不同,可以作如下操作:
1、删除A[1],然后计算A[2:lenA]和B[1:lenB]的距离;
2、删除B[1],然后计算A[1:lenA]和B[2:lenB]的距离;
3、修改A[1],使之等于B[1],然后计算A[2:lenA]和B[2:lenB]的距离;
4、修改B[1],使之等于A[1],然后计算A[2:lenA]和B[2:lenB]的距离;
5、将B[1]放到A串前,然后计算A[1:lenA]和B[2:lenB]的距离;
6、将A[1]放到B串前,然后计算A[2:lenA]和B[1:lenB]的距离;
其实,我们不需要关心字符串是如何变得相同的,所以,上面6个操作可以合并为:
1、一步操作之后,计算A[2:lenA]和B[1:lenB]的距离;
2、一步操作之后,计算A[1:lenA]和B[2:lenB]的距离;
3、一步操作之后,计算A[2:lenA]和B[2:lenB]的距离;
#define MIN_META(a,b) (a>b?b:a )
#define MIN(a,b,c) MIN_META(MIN_META(a,b), c)
int CalculateStringDistance(char *A, int startA, int endA, char *B, int startB, int endB)
{
if (startA > endA) {
if (startB > endB) {
return 0;
} else {
return endB - startB + 1;
}
}
if (startB > endB) {
if (startA > endA) {
return 0;
} else {
return endA - startA + 1;
}
}
if (A[startA] == B[startB]) {
return CalculateStringDistance(A, startA+1, endA, B, startB+1, endB);
} else {
int t1 = CalculateStringDistance(A, startA+1, endA, B, startB, endB);
int t2 = CalculateStringDistance(A, startA, endA, B, startB+1, endB);
int t3 = CalculateStringDistance(A, startA+1, endA, B, startB+1, endB);
return MIN(t1,t2,t3)+1;
}
}
int main()
{
char str1[] = "Hello World";
char str2[] = "Hillo Worl";
int len1 = strlen(str1);
int len2 = strlen(str2);
int d = CalculateStringDistance(str1, 0, len1, str2, 0, len2);
printf("Distance=%d\n", d);
return 0;
}
第一个只出现一次的字符
问题描述:
在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。
思路:
我们可以在第一次扫描字符串的时候,把每个字符出现的次数记录到一个容器中(hash结构),然后第二次扫描的时候,再读这个容器,就知道每个字符出现的次数。
char FirstNotRepeatingChar(char* pString)
{
if (!pString) return 0;
// get a hash table, and initialize it
const int tableSize = 256;
unsigned int i, hashTable[tableSize];
for (i = 0; i < tableSize; ++ i)
hashTable[i] = 0;
// get the how many times each char appears in the string
char* pHashKey = pString;
while (*(pHashKey) != '\0')
hashTable[*(pHashKey++)] ++;
// find the first char which appears only once in a string
pHashKey = pString;
while(*pHashKey != '\0') {
if(hashTable[*pHashKey] == 1)
return *pHashKey;
pHashKey++;
}
// if the string is empty
// or every char in the string appears at least twice
return 0;
}
int main()
{
char array[] = "abcdacd";
char p = FirstNotRepeatingChar(array);
printf("the first not repeatring char: %c\n", p);
return 0;
}
注意:这里默认输入的字符串是ASCII码,因此只需要一个256大小的数组就可以容纳所有可能出现的字符。
对称字符串的最大长度
问题描述:
输入一个字符串,输出该字符串中对称的子串的最大长度。
例如输入"google",由于该字符串里最长的对称子字符串是"goog",因此输出4.
思路:
最暴力的解法就是遍历字符串所有的子串,然后依次检查每个子串是否是对称字符串,最后得到一个最大的对称子串。
我们换一种思路,从里向外来判断。也就是先判断子串A是不是对称的,如果A不对称,那么向该字符串两端各延长一个字符得到的字符串肯定不是对称的;如果A对称,那么只需要判断A两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。
int GetLongestSymmetricalLength(char *str)
{
if (NULL == str) return 0;
char *p = str;
char *last = str + strlen(str);
char *ret = (char*)malloc(sizeof(str));
memset(ret, 0, sizeof(str));
int length = 1;
while (p < last ) {
// substrings with odd length
char *begin = p - 1;
char *end = p + 1;
while (begin >= str && end < last && *begin == *end) {
begin--;
end++;
}
int new_len = end - begin - 1;
if (new_len > length) {
length = new_len;
strncpy(ret, begin+1, length);
}
// substrings with even length
begin = p;
end = p + 1;
while (begin >= str && end < last && *begin == *end) {
begin--;
end++;
}
new_len = end - begin - 1;
if (new_len > length) {
length = new_len;
strncpy(ret, begin+1, length);
}
p++;
}
printf("got %s\n", ret);
free(ret);
return length;
}
字符串的排列
问题描述:
输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab、cba。
思路:
以三个字符abc为例来分析一下求字符串排列的过程。首先我们固定第一个字符a,求后面两个字符bc的排列。当两个字符bc的排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列。现在是把c放到第一位置的时候了。记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍然是和原先处在第一位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列。既然我们已经知道怎么求三个字符的排列,那么固定第一个字符之后求后面两个字符的排列,就是典型的递归思路了。
void swap(char *a, char *b)
{
if (NULL == a || NULL == b || a ==b ) return;
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
void Permutation(char *str, char *begin)
{
if (!str || !begin)
return;
if (*begin == '\0') {
printf("%s\n", str);
} else {
char *p;
for (p = begin; *p != '\0'; p++) {
swap(p, begin);
Permutation(str, begin + 1); // recursive
swap(p, begin);
}
}
}
int main()
{
char str[] = "abcd";
Permutation(str, str);
return 0;
}
如果输入的字符串有重复字符,要考虑不要出现重复的排列项:
void Permutation(char *str, char *begin)
{
if (!str || !begin)
return;
if (*begin == '\0') {
printf("%s\n", str);
} else {
char *p;
for (p = begin; *p != '\0'; p++) {
if (strchr(begin, *p) == p) {
swap(p, begin);
Permutation(str, begin + 1); // recursive
swap(p, begin);
}
}
}
}
如上面的红色部分,如果当前*p的字符(即准备与*begin交换的字符)在前面的子字符串中是否出现过了,若出现了,就不交换,若没出现就交换
字符串的组合
问题描述:
输入一个字符串,输出该字符串中字符的所有组合。
例如如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
思路:
假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;而是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。
C++代码实现:
bool IsContinuous(vector<int> numbers, int maxNumber)
{
if(numbers.size() == 0 || maxNumber <=0)
return false;
// Sort the array numbers.
sort(numbers.begin(), numbers.end());
int numberOfZero = 0;
int numberOfGap = 0;
// how many 0s in the array?
vector<int>::iterator smallerNumber = numbers.begin();
for (; smallerNumber != numbers.end(); ++smallerNumber) {
if (*smallerNumber == 0) {
numberOfZero++;
} else {
break;
}
}
// get the total gaps between all adjacent two numbers
vector<int>::iterator biggerNumber = smallerNumber + 1;
while (biggerNumber < numbers.end()) {
// if any non-zero number appears more than once in the array,
// the array can't be continuous
if (*biggerNumber == *smallerNumber)
return false;
numberOfGap += *biggerNumber - *smallerNumber - 1;
smallerNumber = biggerNumber;
++biggerNumber;
}
return (numberOfGap > numberOfZero) ? false : true;
}
void Combination(char* string, int number, vector<char>& result)
{
if(number == 0) {
vector<char>::iterator iter = result.begin();
for (; iter < result.end(); ++ iter) cout<<*iter;
cout<<endl;
return;
}
if(*string == '\0') return;
result.push_back(*string);
Combination(string + 1, number - 1, result);
result.pop_back();
Combination(string + 1, number, result);
}
void Combination(char* string, int length)
{
if (string == NULL || length <= 0) return;
vector<char> result;
for (int i = 1; i <= length; ++ i) {
Combination(string, i, result);
}
}
int main()
{
char str[] = "abcd";
Combination(str, sizeof(str) - 1);
}
最长不重复子串
给定一个字符串,找出这个字符串中最长的不重复子串。
比如对于字符串“sadus”,那么返回的结果应该是“sadu”或者“adus”(返回一个即可);
int lengthOfLongestSubstring(string s) {
vector<int> dict(256, -1);
int maxLen = 0, start = -1;
for (int i = 0; i != s.length(); i++) {
if (dict[s[i]] > start)
start = dict[s[i]];
dict[s[i]] = i;
maxLen = max(maxLen, i - start);
}
return maxLen;
}
最长公共子串(Longest Common Substring)
子字符串的定义和子序列的定义类似,但要求是连续分布在其他字符串中。比如输入两个字符串BDCABA和ABCBDAB的最长公共字符串有BD和AB,它们的长度都是2。
X = <a, b, c, f, b, c>
Y = <a, b, f, c, a, b>
X和Y的最长公共子串(Longest Common Sequence)为<a, b, c, b>,长度为4;
X和Y的最长公共子序列(Longest Common Substring)为 <a, b>,长度为2。
对于最长公共子串问题,可以用一个二维矩阵来记录中间的结果,最长斜对角线即对应最长连续子串;
例如: ABBEDGHK与CCHENBEDHKH 的最长公共子串是“ BED”:
xi表示横轴第i个字符的值,yj表示纵轴第j个字符的值,c[i][j]表示矩阵某位置的累积值,则动态转移方程为:
如果xi ! = yj, 那么c[i][j] = 0;
如果xi == yj, 则 c[i][j] = c[i-1][j-1]+1。
最后求Longest Common Substring的长度等于:
max{ c[i][j], 1<=i<=n, 1<=j<=m}
void longest_common_substring(char *str1, char *str2)
{
int i,j,x,y;
int len1 = strlen(str1);
int len2 = strlen(str2);
int c[len1][len2]; // 使用变量作为数组长度是一种灰色的做法
memset(c, 0, len1*len2*sizeof(int));
int max = 0;
for (i=0; i<len1; i++) {
for (j=0; j<len2; j++) {
if (str1[i] == str2[j]) {
if (i==0 || j==0) {
c[i][j] = 1;
} else {
c[i][j] = c[i-1][j-1] + 1;
}
}
if (c[i][j] > max) {
max = c[i][j];
x=i;
y=j;
}
}
}
char s[max+1];
int k = max;
s[k--] = 0;
for(i=x,j=y; i>=0 && j>=0; i--,j--) {
if (str1[i]==str2[j]) {
s[k--] = str1[i];
} else {
break;
}
}
printf("LCS:%s\n", s);
}
最长公共子序列(Longest Common Subsequence)
考虑最长公共子序列问题如何分解成子问题,设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]记录x0,x1,...,xi与y0,y1,...,yj 的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]。
问题的递归式写成:
回溯输出最长公共子序列过程:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m + n)。
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);
}
}
void longest_common_subsequence(char* str1, char* str2)
{
int i,j;
int len1 = strlen(str1);
int len2 = strlen(str2);
int **b = malloc((len1+1)*sizeof(int*));
for (i=0; i<=len1; i++) {
b[i] = malloc((len2+1) * sizeof(int));
memset(b[i], 0, len2 * sizeof(int));
}
int **c = malloc((len1+1)*sizeof(int*));
for (i=0; i<=len1; i++) {
c[i] = malloc((len2+1) * sizeof(int));
memset(c[i], 0, len2 * sizeof(int));
}
for (i=1; i<=len1; i++) {
for (j=1; j<=len2; j++) {
if (str1[i-1] == str2[j-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;
}
}
}
printf("length of LCS=%d\n", c[len1][len2]);
PrintLCS(b, str1, len1, len2);
}
从字符串中删除指定字符
利用两个指针,一个读指针,一个写指针,读指针在每个循环中走一步,写指针根据当前读取的字符来决定是否前进。
如果读到的当前字符是要删除的字符,则写指针原地不动,等待下一个读操作来赋值。
static void DelInStr(char* str, char c)
{
char *pr = str;
char *pw = str;
while (*pr) {
*pw = *pr++;
pw += (*pw != c);
}
*pw = '\0';
}
假如要从字符串中删除多个指定的字符,方法也是一样的,同样是判断当前读到的字符是否是待删除字符中的一个。
判断可以使用一个循环,将当前字符与所有要删除的字符依次比较,这样做效率比较慢,我们考虑用一个hash的结构来代替查找过程。
我们知道一个ASCII字符表示的范围是0~255,故我们构造一个长度为256的数组,并将其所有元素初始化为0,然后将要删除字符的ASCII码作为索引的元素置1。
static void DelInStr(char* str, char* c)
{
char *pr = str;
char *pw = str;
int n = 0;
short flag[256];
memset(flag, 0, 256);
while (c[n] != '\0') {
flag[c[n++]] = 1;
}
while (*pr) {
*pw = *pr++;
pw += (flag[*pw] == 0);
}
*pw = '\0';
}
int main()
{
char str[] = "Hello World, I am from China!";
DelInStr(str, "loa");
printf("%s\n", str);
return 0;
}
打印的结果为:He Wrd, I m frm Chin!
OK,删除单个或多个字符问题解决了,让我们把它再拓展一下,如果是要从一个字符串中删除一个子串,应该怎么做呢?
对这个问题,通常最直接、最粗暴的反应是利用strstr函数找到子串,然后memmove把子串后面的剩余串往前覆盖,实现如下:
static void DelInStr2(char* str, char* del)
{
char *p;
int n = strlen(del);
while (p = strstr(str, del)) {
memmove(p, p+n, strlen(p+n)+1);
}
}
int main()
{
char str[] = "world, hello, i wanna get rid of world here world";
DelInStr2(str, "world");
printf("%s\n", str);
return 0;
}
打印的结果为:, hello, i wanna get rid of here
这个实现的复杂度也取决于库函数memmove的调用次数,如果子串出现的次数较多,则需要频繁移动字符串,这样做似乎也不是个好主意。但我还没有想到更好的办法。