第4章 字符串综合习题(leetcode)

本来准备参考普林斯顿的《算法》处理一下正则的,结果发现他们的教材编排顺序字符串在图后面,构造NFA自动机使用了有向图的内容,因此打算等学习玩第8章后再考虑。前缀树(trie)打算作为第7章的补充内容。
因此这里主要就是处理一些字符串的基本操作以及模式匹配的习题,综合性的比如涉及了一些回溯、动态规划、贪心等问题并没有列出。当然有些题的解法确实具有启发性,是值得思考的。
说句题外话,没有放vjudge的题因为主要是面向算法竞赛的(虽然也有51Nod、HihoCoder这类面向面试题的),综合度、数据强度都比leetcode的要强一些,单纯作为数据结构这门课的练习难度还是大了点。那些题目打算单独写题解。
另外对于字符串来说,还需要重复说明的是子串子序列是不一样的概念。

最长回文子串(Longest Palindromic Substring)

求一个串 s 的最长回文子串。例如babad得到bab或者abacbbd得到bb
求最长回文字串有一个线性时间的Manacher算法,不过我这边主要考虑串的基本运算和操作,是使用的朴素算法:
列出所有比当前长度更大的子串,判断是否回文,如果回文则更新最大长度和起始位置。
注意到下面的代码,第二层for循环中,因为j指向的是当前的子串的最后一个字符的下一个字符,因此需要在判断结束j <= s.length()中带上等号。
分析这个算法最坏的情况,当最长回文串的长度为1(单字符)时,外层循环执行 n 次,内层循环每次执行ni次(注意j <= s.length()),因此总计的时间复杂度为 Σni=1(ni) ,得到的是 O(n2) 的时间复杂度。

class Solution {
public:
    string longestPalindrome(string s){
        int start = 0, length = 1;
        for (int i = 0; i < s.length(); i++){
            for (int j = i + 1; i + length < s.length() && j <= s.length(); j++){
                //判断回文
                int p = i, q = j - 1;
                while (p <= q && s[p] == s[q]){
                    p++;
                    q--;
                }
                //如果比当前的最长的长,则更新
                if (p > q && j - i > length){
                    start = i;
                    length = j - i;
                }
            }
        }
        return s.substr(start, length);
    }
};

元音字符逆序(Reverse Vowels of a String)

给定一个只包含英文大小写字母的串,要求将串中的元音字母(aeiou,包括大写)的字符在串中逆序,而其他字符不变。例如hello得到holleleetcode得到leotcede
因为串是特殊的线性表,因此完全可以利用前面的线性表的一些算法设计技巧,使用双指针法分别从串尾和串首开始扫描,当都扫描到元音字符时,将其交换,继续直至两个指针碰头。
分析其时间复杂度,很容易看出对每个字符只访问一次,因此时间复杂度为稳定的 O(1)

class Solution {
public:
    //判断是否是元音字符
    bool isvowel(char c){
        switch(c){
            case 'a':
            case 'e':
            case 'i':
            case 'o':
            case 'u':
            case 'A':
            case 'E':
            case 'I':
            case 'O':
            case 'U':
                return true;
            default:
                return false;
        }
    }
    string reverseVowels(string s){
        int i = 0, j = s.length() - 1;
        while (i <= j){
            //i指针必须在j指针之前,下面两个循环同理
            while (!isvowel(s[i]) && i < j){
                i++;
            }
            while (!isvowel(s[j]) && i < j){
                j--;
            }
            swap(s[i], s[j]);
            i++;
            j--;
        }
        return s;
    }
};

最长公共前缀(Longest Common Prefix)

求一个字符串数组的最长公共前缀。

对于一个字符串数组,应该有着很明显的想法:
s1=c11c12c13...c1n1
s2=c21c22c23...c2n2
...
sm=cm1cm2cm3...cmnm
求其公共前缀,就应该找到 scp=t1t2t3...tk ,满足 kmin(ni) im ),且 i[1,m] tk=cik 。因此,很容易想到按行优先,先遍历 k ,对于每一个k,判断每一个字符串中的第 k 个元素是否能够构成公共前缀。边界条件由于k的限制自然在后面遍历时不需要判断了。
整个算法的平均时间复杂度为 O(mn¯) ,其中 n¯ 为字符串的平均长度。空间复杂度为 O(1)

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        //corner cases first
        if(strs.empty()){
            return "";
        }
        if(strs.size() == 1){
            return strs[0];
        }
        string result;
        int i, k;
        //先求最短的串长度,作为k的限制
        int minlen = strs[0].length();
        for(i = 1; i < strs.size(); i++){
            minlen = strs[i].length() < minlen ? strs[i].length() : minlen;
        }
        for(k = 0; k < minlen; k++){
            for(i = 1; i < strs.size(); i++){
                if(strs[i][k] != strs[0][k]){
                    break;
                }
            } 
            if(i == strs.size()){
                result.push_back(strs[0][k]);
            }else{
                break;  //只要找到不符合的就不能继续比较了
            }
        }
        return result;
    }
};

重复的字符串匹配(Repeated String Match)

给定两个字符串 A B,求 A 至少出现的次数k,使字符串 B k A 相连接得到的字符串的子串。
例如A = "abcd" and B = "cdabcdab",此时应该返回k=3,因为至少3个A相连接得到abcdabcdabcd,使B为其子串。

这道题参考答案提供了两种解法,一种是我所使用的,比较好理解;另一种是Rabin-Karp算法的变形。
下面主要是叙述一下我自己的算法:
A 的长度为m B 的长度为n,为了满足题目要求的匹配条件,可以将B划分为三个部分: B1B2B3 ,其中 B1 A 的后缀真子串,B2 k A 重复连接而成,B3 A 的前缀真子串。因此kk+2nm+2,只要保证按次序检查 t A相连接时, B 是否是其子串。其中1tnm+2
下面分析其时间复杂度和空间复杂度:外层循环执行 nm+2 次查找子串使用std::stringfind方法,朴素匹配时间复杂度是 O(mn) 。因此总的时间复杂度是 O(mn×(nm+2))=O(n(m+n)) 。如果使用KMP匹配能保证 O(n+n2m)) 的时间。
空间方面,因为其需要对 A 连接至多\lfloor\dfrac{n}{m}\rfloor+2nm+2次,总长度不会超过 m(nm+2) ,因此空间复杂度为 O(nm)

class Solution {
public:
    int repeatedStringMatch(string A, string B) {
        int i;
        string s;
        int maxloop = B.length() / A.length() + 2;
        for(i = 1; i <= maxloop; i++){
            s += A;
            if(s.find(B) != string::npos){
                return i;
            }
        }
        return -1;
    }
};

最短回文(Shortest Palindrome)

给定一个字符串s,要求只能在其前面增加字符,使之变成一个回文串。要求生成的回文串最小。
例如aacecaaa得到aaacecaaaabcd得到dcbabcd

这道题其实是对KMP的一个扩展。对于回文问题,一般情况下都要对当前字符串 s 进行逆序处理得到sr,然后考虑对 sr 进行操作。
事实上,要求得到的回文串最短,必然要 s sr公共的部分最多,显然此时的公共部分,既是 s 的前缀,又是sr的后缀。因此可以将 s 作为模式串,让sr与之匹配,不输出完全匹配(事实上在题目条件下也不可能完全匹配),当整个过程结束时, q 的值就是s的前缀又是 sr 的后缀(公共部分)的最大长度(试想在模式匹配中,完全匹配的时候就是 q=m ,即文本主串的某一长度为 q 的后缀与模式串长度为q的前缀相等)。最后输出时,只需要将 sr s 的剩余部分相连接就可以了。
时间复杂度,因为KMP算法是O(m+n),此处 m=n ,因此 O(n) 。空间复杂度和KMP一样是 O(n) (模式串长度 n <script type="math/tex" id="MathJax-Element-9710">n</script>)。

class Solution {
public:
    void kmp_matcher_prefix(string & pattern, vector<int> & prefix){
        int maxlen = 0;
        prefix.push_back(0);
        for (int i = 1; i < pattern.length(); i++){
            while (maxlen > 0 && pattern[maxlen] != pattern[i]){
                maxlen = prefix[maxlen - 1];
            }
            if (pattern[maxlen] == pattern[i]){
                maxlen++;
            }
            prefix.push_back(maxlen);
        }
    }
    string shortestPalindrome(string s){
        string t;
        vector<int> prefix_array;
        for (auto it = s.rbegin(); it != s.rend(); ++it){
            t.push_back(*it);
        }
        kmp_matcher_prefix(s, prefix_array);
        int q = 0, j;
        for (j = 0; j < t.length(); j++){
            while (q > 0 && t[j] != s[q]){
                q = prefix_array[q - 1];
            }
            if (s[q] == t[j]){
                q++;
            }
        }
        if (q == s.length())
            return s;
        return t + s.substr(q);
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值