本来准备参考普林斯顿的《算法》处理一下正则的,结果发现他们的教材编排顺序字符串在图后面,构造NFA自动机使用了有向图的内容,因此打算等学习玩第8章后再考虑。前缀树(trie)打算作为第7章的补充内容。
因此这里主要就是处理一些字符串的基本操作以及模式匹配的习题,综合性的比如涉及了一些回溯、动态规划、贪心等问题并没有列出。当然有些题的解法确实具有启发性,是值得思考的。
说句题外话,没有放vjudge的题因为主要是面向算法竞赛的(虽然也有51Nod、HihoCoder这类面向面试题的),综合度、数据强度都比leetcode的要强一些,单纯作为数据结构这门课的练习难度还是大了点。那些题目打算单独写题解。
另外对于字符串来说,还需要重复说明的是子串和子序列是不一样的概念。
最长回文子串(Longest Palindromic Substring)
求一个串
s
的最长回文子串。例如babad
得到bab
或者aba
,cbbd
得到bb
。
求最长回文字串有一个线性时间的Manacher算法,不过我这边主要考虑串的基本运算和操作,是使用的朴素算法:
列出所有比当前长度更大的子串,判断是否回文,如果回文则更新最大长度和起始位置。
注意到下面的代码,第二层for
循环中,因为j
指向的是当前的子串的最后一个字符的下一个字符,因此需要在判断结束j <= s.length()
中带上等号。
分析这个算法最坏的情况,当最长回文串的长度为j <= s.length()
),因此总计的时间复杂度为
Σni=1(n−i)
,得到的是
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)
给定一个只包含英文大小写字母的串,要求将串中的元音字母(a
,e
,i
,o
,u
,包括大写)的字符在串中逆序,而其他字符不变。例如hello
得到holle
,leetcode
得到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
,满足
k≤min(ni)
(
i≤m
),且
∀i∈[1,m]
,
tk=cik
。因此,很容易想到按行优先,先遍历
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
和
例如A = "abcd"
and B = "cdabcdab"
,此时应该返回A
相连接得到abcdabcdabcd
,使B
为其子串。
这道题参考答案提供了两种解法,一种是我所使用的,比较好理解;另一种是Rabin-Karp算法的变形。
下面主要是叙述一下我自己的算法:
设
A
的长度为B
划分为三个部分:
B1B2B3
,其中
B1
是
A
的后缀真子串,
下面分析其时间复杂度和空间复杂度:外层循环执行
⌊nm⌋+2
次查找子串使用std::string
的find
方法,朴素匹配时间复杂度是
O(mn)
。因此总的时间复杂度是
O(mn×(⌊nm⌋+2))=O(n(m+n))
。如果使用KMP匹配能保证
O(n+n2m))
的时间。
空间方面,因为其需要对
A
连接至多\lfloor\dfrac{n}{m}\rfloor+2
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
得到aaacecaaa
,abcd
得到dcbabcd
。
这道题其实是对KMP的一个扩展。对于回文问题,一般情况下都要对当前字符串
s
进行逆序处理得到
事实上,要求得到的回文串最短,必然要
s
和
时间复杂度,因为KMP算法是
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);
}
};