📜  实时优化的KMP模式搜索

📅  最后修改于: 2021-04-23 18:34:07             🧑  作者: Mango

在本文中,我们已经讨论了用于模式搜索的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算法,请考虑以下情况:

上述测试用例的最长正确前缀或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]):
    1. 如果T [i]P中的字符,则j将更新为FT [T [i]] [j-1] ,“ i ”将更新为“ i +1 ”。我们这样做是因为我们保证T [i]被匹配或跳过。
    2. 如果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];
    

    这是上述示例的期望输出,为了更好地说明,输出包括故障表。

    例子:

    下面是上述方法的实现:

    // 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( |\Sigma_P| \cdot M) , 在哪里 \Sigma_P是模式P的字母集,M是P的大小。
    • 整个改进的KMP算法的运行时间复杂度为O( |\Sigma_P| \cdot M + N )。 O()的辅助空间使用|\Sigma_P| \cdot M )。
    • 与原始KMP算法相比,运行时间和空间使用情况看起来“更糟”。但是,如果我们要在多个文本中搜索相同的模式,或者该模式的字母集很小,则由于预处理步骤仅需要执行一次,并且文本中的每个字符最多只能进行一次比较(实时) 。因此,它比原始的KMP算法更有效,并且在实践中也很好。