• 字符串常见算法题


    字符串常见算法题

    左旋转字符串

    在字符串上定义反转的操作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 != '')
            pEnd ++;
        pEnd--;
    
        // Reverse the whole sentence
        Reverse(pBegin, pEnd);
    
        // Reverse every word in the sentence
        pBegin = pEnd = pData;
        while(*pBegin != '') {
    
            if(*pBegin == ' ') {
                pBegin ++;
                pEnd ++;
                continue;
    
                // A word is between with pBegin and pEnd, reverse it
            } else if(*pEnd == ' ' || *pEnd == '') {
                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
    ", 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) != '')
            hashTable[*(pHashKey++)] ++; 
    
        // find the first char which appears only once in a string
        pHashKey = pString;
        while(*pHashKey != '') {
    
            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
    ", 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
    ", 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 == '') {
            printf("%s
    ", str);
    
        } else {
            char *p;
    
            for (p = begin; *p != ''; 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 == '') {
            printf("%s
    ", str);
    
        } else {
            char *p; 
    
            for (p = begin; *p != ''; 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 == '') 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
    ", 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
    ", 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 = '';
    }

    假如要从字符串中删除多个指定的字符,方法也是一样的,同样是判断当前读到的字符是否是待删除字符中的一个。

    判断可以使用一个循环,将当前字符与所有要删除的字符依次比较,这样做效率比较慢,我们考虑用一个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] != '') {
            flag[c[n++]] = 1;
        }   
    
        while (*pr) {
            *pw = *pr++;
            pw += (flag[*pw] == 0); 
        }   
    
        *pw = '';
    }
    
    int main()
    {
        char str[] = "Hello World, I am from China!";
        DelInStr(str, "loa");
        printf("%s
    ", 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
    ", str);
    
        return 0;
    }

    打印的结果为:, hello, i wanna get rid of  here 

    这个实现的复杂度也取决于库函数memmove的调用次数,如果子串出现的次数较多,则需要频繁移动字符串,这样做似乎也不是个好主意。但我还没有想到更好的办法。

  • 相关阅读:
    Elasticsearch聚合 之 Date Histogram聚合
    Elasticsearch聚合 之 Terms
    Elasticsearch分析聚合
    mysql-聚合函数
    flask学习笔记(-操作数据库)
    在VS中调试javascript脚本
    jquery获取设置input值
    jquery后加Dom绑定事件
    Juicer——a fast template engine
    ASP.NET 一般处理程序
  • 原文地址:https://www.cnblogs.com/chenny7/p/3678591.html
Copyright © 2020-2023  润新知