"《算法导论》之‘字符串’":字符串匹配
本文主要叙述用于字符串匹配的KMP算法。
阮一峰的博文“字符串匹配的KMP算法"将该算法讲述得非常形象,可参考之。
字符串‘部分匹配值’计算
KMP算法重要的一步在于部分匹配值的计算。模仿《算法导论》中的伪代码,对应的C++代码为:
1 vector<int> partialMatching(string P) 2 { 3 int szP = P.size(); 4 vector<int> pMatch; 5 pMatch.resize(szP); 6 // retVec[0] = 0; 7 int k = 0; 8 for (int i = 1; i < szP; i++) 9 { 10 while (k > 0 && P[k] != P[i]) 11 k = pMatch[k - 1]; 12 if (P[k] == P[i]) 13 k = k + 1; 14 pMatch[i] = k; 15 } 16 return pMatch; 17 }
代码看起来挺简洁,但要理解起来就比较麻烦了。其中有一个问题就是:字符串“cabab”的部分匹配值为什么全都为0?其中的“ab”不是重复了吗,应该要算作重复的子串啊?
之所以有这个困惑,是对字符串的前缀和后缀的概念不理解。在阮一峰的博文中,他提到"前缀"是指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
另外他还举了一个例子来说明如何求取一个字符串的部分匹配值:
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
注意:这个例子个人感觉如果没有好好理解,则有可能误解!阮一峰的做法是将一个字符串拆成不同长度的子串,然后求部分匹配值,而我们实际并不是这样子做的(参考上边代码),所以这些步骤可能会让我们误解后缀一定要以该字符串的最后一个字符来结束。
对于一个字符串的前缀而言,一定要从第一个字符开始;而对其后缀而言,则不必要以最后一个字符结束。
KMP算法
模仿《算法导论》中的伪代码,对应的C++代码为:
1 vector<int> KMPMatching(string T, string P) 2 { 3 int szT = T.size(); 4 int szP = P.size(); 5 vector<int> pMatch = partialMatching(P); 6 vector<int> kmpMatch; 7 int k = 0; 8 for (int i = 0; i < szT; i++) 9 { 10 while (k > 0 && T[i] != P[k]) 11 k = pMatch[k - 1]; 12 13 if (T[i] == P[k]) 14 k = k + 1; 15 16 if (k == szP - 1) 17 { 18 kmpMatch.push_back(i - k + 1); 19 k = pMatch[k - 1]; 20 } 21 } 22 return kmpMatch; 23 }
注意,第19行的
k = pMatch[k - 1];
不能直接写成
k = 0;
例如待匹配字串为abababc,匹配模式为aba,则有两个符合匹配模式的子串:aba、aba,而中间的a是共享的。