在本文中,我们已经讨论了用于模式搜索的KMP算法。本文讨论了实时优化的KMP算法。
从上一篇文章中可以知道,KMP(aka Knuth-Morris-Pratt)算法会对模式P进行预处理,并构造一个故障函数F(也称为lps [])来存储子模式的最长后缀的长度。对于l = 0到m-1,P [1..l]也是P的前缀。请注意,子模式从索引1开始,因为后缀可以是字符串本身。在索引P [j]发生不匹配之后,我们将j更新为F [j-1]。
原始KMP算法的运行时复杂度为O(M + N)和辅助空间O(M),其中N是输入文本的大小,M是模式的大小。预处理步骤花费O(M)时间。很难实现比这更好的运行时复杂性,但是我们仍然能够消除一些效率低下的变化。
原始KMP算法的效率低下:通过使用原始KMP算法,请考虑以下情况:
Input: T = “cabababcababaca”, P = “ababaca”
Output: Found at index 8
上述测试用例的最长正确前缀或lps []为{0,0,1,2,3,0,1}。假设红色表示发生不匹配,绿色表示我们跳过的检查。因此,按照原始KMP算法进行的搜索过程如下:
可以注意到的一件事是,在第三,第四和第五匹配中,不匹配发生在同一位置T [7]。如果我们可以跳过第四和第五匹配,那么可以进一步优化原始的KMP算法以回答实时查询。
实时优化:在这种情况下,术语“实时”可以解释为最多检查一次文本T中的每个字符。在这种情况下,我们的目标是适当地转换模式(就像KMP算法一样),但是无需再次检查不匹配的字符。也就是说,对于上述相同示例,优化的KMP算法应以以下方式工作:
方法:实现目标的一种方法是修改预处理过程。
- 令K为图案P的字母的大小。我们将构造一个包含K个故障函数的故障表(即lps [])。
- 故障表中的每个故障函数都映射到模式P的字母中的字符(故障表中的键)。
- 回想一下,原始故障函数F [l] (或lps [])存储了P [1..l]的最长后缀的长度,对于l = 0到m-1,它也是P的前缀。 m是图案的大小。
- 如果在T [i]和P [j]发生不匹配,则j的新值将更新为F [j-1] ,并且计数器“ i”将保持不变。
- 在我们新的故障表FT [] []中,如果故障函数F’与字符c映射,则F'[l]应存储最长后缀P [1..l] + c(’+ ‘表示附加),对于l = 0到m-1,它也是P的前缀。
- 直觉是要做出适当的转变,但也要取决于不匹配的字符。此处的字符c(也是失败表中的键)是我们对文本T中不匹配的字符的“猜测”。
- 也就是说,如果不匹配的字符是c,我们应该如何正确转换模式?由于我们是在预处理步骤中构造故障表的,因此我们必须对不匹配的字符做出足够的猜测。
- 因此,失败表中的lps []的数量等于模式字母的大小,并且每个值(失败函数)对于键P中的字符都应有所不同。
- 假设我们已经构造了所需的故障表。令FT [] []为故障表, T为文本, P为模式。
- 然后,在匹配过程中,如果在T [i]和P [j]处发生不匹配(即T [i]!= P [j]):
- 如果T [i]是P中的字符,则j将更新为FT [T [i]] [j-1] ,“ i ”将更新为“ i +1 ”。我们这样做是因为我们保证T [i]被匹配或跳过。
- 如果T [i]不是字符,则’j’将更新为0,’i’将更新为’i + 1’。
- 请注意,如果不发生不匹配,则其行为与原始KMP算法完全相同。
构造故障表:
- 要构造故障表FT [] [],我们将需要原始KMP算法中的故障函数F(或lps [])。
- 因为F [l]告诉我们子模式P [1..l]的最长后缀的长度,它也是P的前缀,所以存储在故障表中的值比它大了一步。
- 也就是说,对于故障表FT [] []中的任何键t,存储在FT [t]中的值都是满足字符’t’的故障函数,而FT [t] [l]存储故障码FT的长度。子模式P [1..l] + t(’+’表示附加)的最长后缀,也是0到m-1的l的前缀。
- F [l]已经保证P [0..F [l] -1]是子模式P [1..l]的最长后缀,因此我们将需要检查P [F [l] ]是t。
- 如果为true,则可以将FT [t] [l]分配为F [l] + 1,因为我们保证P [0..F [l]]是子模式P [1的最长后缀..l] + t。
- 如果为假,则表明P [F [l]]不是t。也就是说,我们在字符P [F [l]]与字符t的匹配失败,但是P [0..F [l] -1]匹配了后缀P [1..l]。
- 通过借鉴KMP算法的思想,就像我们在原始KMP算法中计算故障函数,如果不匹配发生在字符[ t]不匹配的P [F [l]]处,我们希望从FT [ t] [F [l] -1]。
- 也就是说,我们使用KMP算法的思想来计算故障表。请注意,F [l] – 1始终小于l,因此在计算FT [t] [l]时,FT [t] [F [l] – 1]已经为我们准备好了。
- 一种特殊情况是,如果F [l]为0而P [F [l]]不是t,则F [l] – 1的值为-1,在这种情况下,我们将更新FT [t] [l ]设为0。(即不存在P [1..l] + t的后缀,因此它是P的前缀)
- 作为故障表构造的结论,当我们计算FT [t] [l]时,对于从0到m-1的任何密钥t和l,我们将检查:
If P[F[l]] is t, if yes: FT[t][l] <- F[l] + 1; if no: check if F[l] is 0, if yes: FT[t][l] <- 0; if no: FT[t][l] <- FT[t][F[t] - 1];
这是上述示例的期望输出,为了更好地说明,输出包括故障表。
例子:
Input: T = “cabababcababaca”, P = “ababaca”
Output: Failure Table:
Key Value
‘a’ [1 1 1 3 1 1 1]
‘b’ [0 0 2 0 4 0 2]
‘c’ [0 0 0 0 0 0 0]
Found pattern at index 8下面是上述方法的实现:
// C++ program to implement a // real time optimized KMP // algorithm for pattern searching #include
#include #include #include using std::string; using std::unordered_map; using std::set; using std::cout; // Function to print // an array of length len void printArr(int* F, int len, char name) { cout << '(' << name << ')' << "contain: ["; // Loop to iterate through // and print the array for (int i = 0; i < len; i++) { cout << F[i] << " "; } cout << "]\n"; } // Function to print a table. // len is the length of each array // in the map. void printTable( unordered_map & FT, int len) { cout << "Failure Table: {\n"; // Iterating through the table // and printing it for (auto& pair : FT) { printArr(pair.second, len, pair.first); } cout << "}\n"; } // Function to construct // the failure function // corresponding to the pattern void constructFailureFunction( string& P, int* F) { // P is the pattern, // F is the FailureFunction // assume F has length m, // where m is the size of P int len = P.size(); // F[0] must have the value 0 F[0] = 0; // The index, we are parsing P[1..j] int j = 1; int l = 0; // Loop to iterate through the // pattern while (j < len) { // Computing the failure function or // lps[] similar to KMP Algorithm if (P[j] == P[l]) { l++; F[j] = l; j++; } else if (l > 0) { l = F[l - 1]; } else { F[j] = 0; j++; } } } // Function to construct the failure table. // P is the pattern, F is the original // failure function. The table is stored in // FT[][] void constructFailureTable( string& P, set & pattern_alphabet, int* F, unordered_map & FT) { int len = P.size(); // T is the char where we mismatched for (char t : pattern_alphabet) { // Allocate an array FT[t] = new int[len]; int l = 0; while (l < len) { if (P[F[l]] == t) // Old failure function gives // a good shifting FT[t][l] = F[l] + 1; else { // Move to the next char if // the entry in the failure // function is 0 if (F[l] == 0) FT[t][l] = 0; // Fill the table if F[l] > 0 else FT[t][l] = FT[t][F[l] - 1]; } l++; } } } // Function to implement the realtime // optimized KMP algorithm for // pattern searching. T is the text // we are searching on and // P is the pattern we are searching for void KMP(string& T, string& P, set & pattern_alphabet) { // Size of the pattern int m = P.size(); // Size of the text int n = T.size(); // Initialize the Failure Function int F[m]; // Constructing the failure function // using KMP algorithm constructFailureFunction(P, F); printArr(F, m, 'F'); unordered_map FT; // Construct the failure table and // store it in FT[][] constructFailureTable( P, pattern_alphabet, F, FT); printTable(FT, m); // The starting index will be when // the first match occurs int found_index = -1; // Variable to iterate over the // indices in Text T int i = 0; // Variable to iterate over the // indices in Pattern P int j = 0; // Loop to iterate over the text while (i < n) { if (P[j] == T[i]) { // Matched the last character in P if (j == m - 1) { found_index = i - m + 1; break; } else { i++; j++; } } else { if (j > 0) { // T[i] is not in P's alphabet if (FT.find(T[i]) == FT.end()) // Begin a new // matching process j = 0; else j = FT[T[i]][j - 1]; // Update 'j' to be the length of // the longest suffix of P[1..j] // which is also a prefix of P i++; } else i++; } } // Printing the index at which // the pattern is found if (found_index != -1) cout << "Found at index " << found_index << '\n'; else cout << "Not Found \n"; for (char t : pattern_alphabet) // Deallocate the arrays in FT delete[] FT[t]; return; } // Driver code int main() { string T = "cabababcababaca"; string P = "ababaca"; set pattern_alphabet = { 'a', 'b', 'c' }; KMP(T, P, pattern_alphabet); } 输出:(F)contain: [0 0 1 2 3 0 1 ] Failure Table: { (c)contain: [0 0 0 0 0 0 0 ] (a)contain: [1 1 1 3 1 1 1 ] (b)contain: [0 0 2 0 4 0 2 ] } Found at index 8
注意:上面的源代码将找到模式的第一个匹配项。稍加修改,即可用于查找所有出现的事件。
时间复杂度:
- 新的预处理步骤的运行时间复杂度为O( , 在哪里是模式P的字母集,M是P的大小。
- 整个改进的KMP算法的运行时间复杂度为O( )。 O()的辅助空间使用 )。
- 与原始KMP算法相比,运行时间和空间使用情况看起来“更糟”。但是,如果我们要在多个文本中搜索相同的模式,或者该模式的字母集很小,则由于预处理步骤仅需要执行一次,并且文本中的每个字符最多只能进行一次比较(实时) 。因此,它比原始的KMP算法更有效,并且在实践中也很好。