【问题标题】:Understanding Knuth-Morris-Pratt Algorithm了解 Knuth-Morris-Pratt 算法
【发布时间】:2012-11-07 14:34:38
【问题描述】:

谁能给我解释一下?我一直在阅读它,但仍然很难理解。

文字:ababdbaababa
图案:阿巴巴

ababa 的表格是 -1 0 0 1 2。

我想我了解表格的构造方式,但是,一旦发生不匹配,我不明白如何转移。换班的时候好像连桌子都不用?

我们什么时候使用这张桌子?

【问题讨论】:

标签: string algorithm pattern-matching


【解决方案1】:

这里我已经简要描述了计算前缀函数并在此处切换文本。

更多信息:Knuth–Morris–Pratt string search algorithm

在文本中移动:

Text:     ABC ABCDAB ABCDABCDABDE
Pattern : ABCDABD

场景 1 - Pattern 和 Text 中有一些匹配的字符。
例如 1:这里有 3 个匹配字符。



从表中获取 3 个字符的值。 (索引 2,ABC)即 0 因此 shift = 3 - 0 即 3

例如 2:这里有 6 个匹配字符。

从表中获取 6 个字符的值。 (索引 5,ABCDAB)即 2 因此 shift = 6 - 2 即 4

场景 2 - 如果没有匹配的字符,则移动一位。

【讨论】:

  • 非常感谢您用图片解释!我希望这个答案高于所有答案。我找不到任何其他资源可以如此清楚地解释前缀表的生成。
【解决方案2】:

当您发生不匹配时使用该表。让我们将模式应用于您的文本:

您开始将文本与模式匹配并测试您的模式是否可以在文本中,从第一个位置开始。您将text[1]pattern[1] 进行比较,结果证明是匹配的。您对 text[2]text[3]text[4] 执行相同操作。

当您想将 text[5]pattern[5] 匹配时,您没有匹配 (da)。然后您就知道您的模式不会从第一个位置开始。然后,您可以为位置 2 重新开始匹配,但这效率不高。您现在可以使用该表格

错误发生在pattern[5],因此您转到table[5],即 2。这告诉您可以在当前位置再次开始匹配 2 个已经匹配的字符。不必从匹配位置 2 开始,您可以从之前的位置 (1) + table[5] (2)=3 开始。事实上,如果我们查看text[3]text[4],我们会看到它分别等于pattern[1]pattern[2]

表中的数字告诉您发生错误时已经匹配了多少个位置。在这种情况下,下一个模式的 2 个字符已经匹配。然后您可以立即开始匹配位置 3 并跳过位置 2(因为从位置 [2] 开始找不到模式)。

【讨论】:

  • 是的。我理解第一部分。但是如果你继续这样做,你最终只会移动一次直到结束,这就是它令人困惑的原因(似乎毫无意义)。这正常吗?
  • 所以你的问题是“如果我们只使用一次,为什么要制作这张桌子?”?好吧,您的文本字符串通常要长得多(如 DNA 序列),然后您将更多地使用该表。每次 text[i] 与 pattern[j] 不匹配时,您实际上都会使用该表
  • “2 个已经匹配的字符”这敲响了铃声并打开了我大脑中的灯现在我明白为什么它们采用最大长度前缀和后缀 num.. 谢谢!
【解决方案3】:

嗯,这是一个老话题,但希望将来搜索此内容的人会看到它。上面给出的答案很好,但我自己通过一个例子来看看到底发生了什么。

说明的第一部分取自wiki,我真正想详细说明的部分是这个回溯数组是如何构造的。

这里是:

我们通过算法的(相对人为的)运行来工作,其中

W = "ABCDABD" and 
S = "ABC ABCDAB ABCDABCDABDE". 

在任何给定时间,算法都处于由两个整数确定的状态:

m 表示 S 中的位置,它是 W 预期匹配的开始

i W 中的索引表示当前正在考虑的字符。

在每个步骤中,我们将S[m+i]W[i] 进行比较,如果它们相等则前进。这是在运行开始时描绘的,例如

              1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W: ABCDABD
i: 0123456

我们将 W 的连续字符与 S 的“平行”字符进行比较,如果它们匹配,则从一个字符移动到下一个字符。然而,在第四步中, 我们得到 S[3] 是一个空格,而 W[3] = 'D' 是一个不匹配。我们没有在 S[1] 处再次开始搜索,而是注意到在 S 中的位置 0 和 3 之间没有出现“A” 0除外;因此,在之前检查了所有这些字符后,我们知道如果再次检查它们,就不可能找到匹配的开头。 因此我们继续下一个字符,设置 m = 4 和 i = 0。

              1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:     ABCDABD
i:     0123456

当在 W[6] (S[10]) 处再次出现差异时,我们很快获得了几乎完全匹配的“ABCDAB”。然而,就在当前部分结束之前 比赛中,我们传递了一个“AB”,它可能是新比赛的开始,所以我们必须考虑到这一点。我们已经知道这些字符匹配 当前位置之前的两个字符,我们不需要再检查它们;我们只需重置 m = 8, i = 2 并继续匹配当前字符。因此, 我们不仅省略了 S 的先前匹配的字符,而且还省略了 W 的先前匹配的字符。

              1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:         ABCDABD
i:         0123456

这个搜索立即失败,然而,因为模式仍然不包含空格,所以在第一次试验中,我们回到 W 的开头并开始 搜索 S 的下一个字符:m = 11,重置 i = 0。

              1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:            ABCDABD
i:            0123456

我们再次立即找到匹配“ABCDAB”,但下一个字符“C”与单词 W 的最后一个字符“D”不匹配。推理如前, 我们设置m = 15,从两个字符串“AB”开始直到当前位置,设置i = 2,从当前位置继续匹配。

              1         2  
m: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
W:                ABCDABD
i:                0123456

这次我们可以完成匹配,第一个字符是S[15]。

上面的例子包含了算法的所有元素。目前,我们假设存在一个“部分匹配”表 T,如下所述,它 指示如果当前匹配以不匹配结束,我们需要在哪里寻找新匹配的开始。 T 的条目是这样构造的 如果我们有一个从 S[m] 开始的匹配在比较 S[m + i] 和 W[i] 时失败,那么下一个可能的匹配将从 S 中的索引 m + i - T[i] 开始(即 T[i] 是我们在不匹配后需要做的“回溯”量)。这有两个含义:首先,T[0] = -1,这表明如果 W[0] 是不匹配的, 我们不能回溯,必须简单地检查下一个字符;其次,尽管下一个可能的匹配将从索引 m + i - T[i] 开始,如示例中所示 上面,我们实际上不需要检查任何 T[i] 字符,因此我们继续从 W[T[i]] 搜索。

回溯阵列构造:

所以这个回溯数组T[]我们将调用lps[],让我们看看我们如何计算这个家伙

lps[i] = the longest proper prefix of pat[0..i] 
            which is also a suffix of pat[0..i].

示例: 对于“AABAACAABAA”模式,

lps[] is [0, 1, 0, 1, 2, 0, 1, 2, 3, 4, 5]

//所以只是快速浏览一下

 lps[0] is just 0 by default
 lps[1] is 1 because it's looking at AA and A is both a prefix and suffix
 lps[2] is 0 because it's looking at AAB and suffix is B but there is no prefix equal to B unless you count B itself which I guess is against the rules
 lps[3] is 1 because it's looking at AABA and first A matches last A
 lps[4] is 2 becuase it's looking at AABAA and first 2 A matches last 2 A
 lps[5] is 0 becuase it's looking at AABAAC and nothing matches C
 ...


 For the pattern “ABCDE”, lps[] is [0, 0, 0, 0, 0]
 For the pattern “AAAAA”, lps[] is [0, 1, 2, 3, 4]
 For the pattern “AAABAAA”, lps[] is [0, 1, 2, 0, 1, 2, 3]
 For the pattern “AAACAAAAAC”, lps[] is [0, 1, 2, 0, 1, 2, 3, 3, 3, 4]

如果你仔细想想这完全有道理......如果你不匹配,你想尽可能地回溯很明显,你回溯了多远(后缀 part) 本质上是前缀,因为您必须根据定义再次从第一个字符开始匹配。所以如果你的字符串看起来像

aaaaaaaaaaaaaaa..b..aaaaaaaaaaaaaaac 并且你在最后一个字符 c 上不匹配,那么你想重用 aaaaaaaaaaaaaaa 作为你的新头,仔细考虑一下

【讨论】:

    【解决方案4】:

    使用 Java 的完整解决方案:

    package src.com.recursion;
    /*
     * This Expains the Search of pattern in text in O(n)
     */
    public class FindPatternInText {
        public int checkIfExists(char[] text, char[] pattern) {
            int index = 0;
            int[] lps = new int[pattern.length];
            createPrefixSuffixArray(pattern, lps);
            int i = 0;
            int j = 0;
            int textLength = text.length;
            while (i < textLength) {
                if (pattern[j] == text[i]) {
                    j++;
                    i++;
                }
                if (j == pattern.length)
                    return i - j;
                else if (i < textLength && pattern[j] != text[i]) {
                    if (j != 0) {
                        j = lps[j - 1];
                    } else {
                        i++;
                    }
                }
            }
            return index;
        }
    
        private void createPrefixSuffixArray(char[] pattern, int[] lps) {
            lps[0] = 0;
            int index = 0;
            int i = 1;
            while (i < pattern.length) {
                if (pattern[i] == pattern[index]) {
                    lps[i] = index;
                    i++;
                    index++;
                } else {
                    if (index != 0) {
                        index = lps[index - 1];
                    } else {
                        lps[i] = 0;
                        i++;
                    }
                }
            }
        }
        public static void main(String args[]) {
            String text = "ABABDABACDABABCABAB";
            String pattern = "ABABCABAB";
            System.out.println("Point where the pattern match starts is "
                    + new FindPatternInText().checkIfExists(text.toCharArray(), pattern.toCharArray()));
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2014-02-21
      • 2014-05-01
      • 2014-11-26
      • 1970-01-01
      • 2020-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-13
      相关资源
      最近更新 更多