字符串常见算法题

字符串常见算法题

 

左旋转字符串

在字符串上定义反转的操作XT,即把X的所有字符反转(如X="abc",那么XT="cba")。如果将一个字符串分成两部分,X和Y两个部分,那么我们可以得到下面的结论:(XTYT)T=YX。

定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。 如把字符串abcdef左旋转2位得到字符串cdefab。按照字符串反转的结论,X="ab",Y="cdef",要想把XY变成YX,只要使用YX=(XTYT)即可,也就是分别对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的调用次数,如果子串出现的次数较多,则需要频繁移动字符串,这样做似乎也不是个好主意。但我还没有想到更好的办法。

 

 

posted @ 2014-04-21 15:47  如果的事  阅读(1785)  评论(0编辑  收藏  举报