字符串常见算法题
字符串常见算法题
左旋转字符串
在字符串上定义反转的操作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的调用次数,如果子串出现的次数较多,则需要频繁移动字符串,这样做似乎也不是个好主意。但我还没有想到更好的办法。