BF算法与KMP算法

1.BF算法

BF 算法,即暴力(Brute Force)算法,是普通的【模式匹配】算法,BF 算法的思想就是将目标串 S 的第一个字符与模式串 T 的第一个字符进行匹配,若相等,则继续比较 S 的第二个字符和 T 的第二个字符;若不相等,则比较 S 的第二个字符和 T 的第一个字符,依次比较下去,直到得出最后的匹配结果。

例如,对于目标串 S("ababcabcacbab")和模式串 T("abcac"),那么其匹配过程如下:

(1) 首先,将模式串 T 与目标串 S 的首字符对齐,逐个判断相对应的字符是否相等,如下图所示:

(2) 上图中,由于模式串 T 和目标串 S 的第 3 个字符匹配失败,所以此时将模式串 T 后移一个字符的位置,采用同样的方法重新进行匹配,如下图所示:

(3) 上图中可以看到,两个串依旧匹配失败,模式串 T 继续后移一个位置,如下图所示:

(4) 上图依然匹配失败,模式串 T 继续向后移动,直到移动到下图所示的位置才匹配成功:

可以看到,假设目标串 S 的长度为 n,模式串 T 的长度为 m,那么遍历目标串的时间复杂度为 \(O(n)\),而遍历子串的长度为 \(O(m)\),所以 BF 算法的时间复杂度就是 \(O(n)*O(m)=O(mn)\)

其代码实现如下:

int bruteForce(const string& s, const string& t) {
    int i = 0, j = 0;
    while (i < s.size() && j < t.size()) {
        if (s[i] == t[j]) {
            i++;
            j++;
        } else {
            // 将i回退到之前匹配的起始位置的下一个字符
            i = i - j + 1;
            j = 0;
        }
    }
    
    return (j == t.size()) ? (i - j) : -1;
}

观察下图所示的 BF 算法匹配过程,可以看到由于模式串 T 的第一个字符 a 和第二个字符 b 并不相等,且在第 1 步和第 2 步中已经判断了目标串 S 和模式串 T 的前两个字符是互相对应的,但是在第 4 步中还是要去做一个明知不可能匹配的无用匹配操作,这就是 BF 算法低效的原因,根据串的特点,我们应该可以直接从第 3 步跳到第 5 步。

总结而言,BF 算法中对于模式串 T 的形状并没有做任何的分析,导致匹配过程中做了很多的无用操作,以致算法效率低下。因此,在匹配过程中,应避免目标串 S 的回退,以提升算法效率,这也就是 KMP 算法的核心思想。

2.KMP算法

2.1 BF算法的缺点

2.1.1 例子1

首先,我们来看如下例子,目标串 S("abcdefgab") 和模式串 T("abcdex"),当采用 BF 算法进行匹配时,其匹配过程如下:

从上图中可以看到,目标串指针从字符 a 走到字符 f 时已经得知模式串 T 的前五个字符互不相等,然而在 BF 算法中,后序的几个步骤依然要去做一些无用的比较操作。因此,我们需要将上述匹配过程优化为如下过程:

即仅保留上图中蓝色虚线框所示的部分即可,以提升算法效率。

2.1.2 例子2

再来看如下示例,目标串 S("abcababca") 和模式串 T("abcabx"),当采用 BF 算法进行匹配时,其匹配过程如下所示:

在上图中的第二步中,即遍历到模式串的最后一个字符时,已经可以看到在模式串 T 中的前三个字符并不相等,因此我们可以采用例子 1 的优化手段,直接省略到上图中的第三步和第四步,得到下图:

我们再来分析上图中的第五步和第六步,由于在第二步时已经知道串 "abcab" 和目标串的前 5 个字符是一一匹配的了,那么此时再在第五步和第六步中去匹配模式串 T 的前两个字符也是一种无用操作,因此可以继续对其进行优化,得到下图:

最终的优化结果如上图所示,只需去执行蓝色虚线框所示的部分即可。

2.2 next数组

从上述两个例子中,可以得知,KMP 算法的核心思想,就是字符失配后,目标串的 i 不做回退,只回退模式串的 j,由于在任意一个字符匹配时都有可能失配,所以 KMP 算法的关键就是给模式串计算出一个 next 数组,其中存储的时当前字符失配时,j 要回退到的位置,同理,也就是存储的当前字符前面的子串的公共前后缀的长度

由于 next 数组存储的是当前位置字符失配时应当回退的位置,因此其长度就等于模式串 T 的长度。我们假设 j 是模式串 T 所对应的字符下标,k 是当前位置字符适配时应当回退的位置,即第 j 个字符前面字符串的最长公共前后缀的长度。

考虑当 j = 0 时,即模式串的首字母,如果在匹配的过程中首字母都不相同,那么就应该将指向目标串的指针向后移动,因此我们设定 next[0] = -1,来作为移动目标串指针的标志。

而当 j 位于如下图所示的位置,且满足 \(P_0...P_{k-1}=P_{j-k}...P_{j-1}\) 时,即前后缀对应的字符相等时,此时有 next[j] = k:

那么根据上述条件,我们来求解 next[j+1] 的值,其分为两种情况:

情况一:当满足 \(P_k=P_j\) 时,如下图所示,那么此时有 next[j+1] = k + 1;

情况二:当满足 \(P_k\neq P_j\) 时,如下图所示,为了找寻最长公共前后缀,此时应该回退 next 数组,直到找到一个和 \(P_j\) 相等的元素:

而对于 \(P_k\) 而言,它也满足下图所示的条件,其也存在最长公共前后缀,即图中的黄色部分,而其应该回退的位置即为图中的蓝色部分,所以对于上述情况,我们直接让其回退到 next[k] 即可。

3.3 代码实现

KMP 算法的代码实现如下:

vector<int> getNext(const string& str) {
    vector<int> next(str.size(), -1);
    // j用来遍历字符串 k表示公共前后缀的长度
    int j = 0, k = -1;
    while (j < str.size() - 1) {
        if (k == -1 || str[k] == str[j]) {
            next[j++] = k++;
        } else {
            k = next[k];
        }
    }
    return next;
}

int kmp(const string& s, const string& t) {
    int i = 0, j = 0;
    vector<int> next = getNext(t);
    while (i < s.size() && j < static_cast<int>(t.size())) {
        if (j == -1 || s[i] == s[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    return (j == t.size()) ? (i - j) : -1;
}

3.4 算法优化

考虑如下情况,目标串 S("abcabdef") 和模式串 T("abcabc"),通过上面的 next 数组分析,我们可以轻易地写出模式串 T 相应的 next 数组: -1,0,0,0,1,2。它们的匹配过程中如下所示:

可以看到,当模式串 T 匹配到最后一个字符时发生了失配,此时通过 next 数组进行回退,通过 next 数组看到应当回退到第 2 个元素,但是回退后可以看到,该字符依然是字符 c,而回退前的字符也是 c,所以此时肯定是匹配失败的,应当继续向前回退,此时回退的位置则继续查询 next 数组即可。

因此,如上便是 KMP 算法所应该优化的点,即在求解 next 数组的过程中,直接对其进行优化,优化后的求解 next 数组的代码如下:

vector<int> getNext(const string& str) {
    vector<int> next(str.size(), -1);
    // j用来遍历字符串 k表示公共前后缀的长度
    int j = 0, k = -1;
    while (j < str.size() - 1) {
        if (k == -1 || str[k] == str[j]) {
            j++;
            k++;
            // 算法优化
            if (str[k] == str[j]) {
                next[j] = next[k];
            } else {
                next[j] = k;
            }
        } else {
            k = next[k];
        }
    }
    return next;
}

那么优化后,模式串 T("abcabc") 对应的 next 数组就变成了 -1,0,0,-1,0,0。

综上所述,假设目标串的长度为 n,模式串的长度为 m,那么 KMP 算法的时间复杂度就是 \(O(m+n)\)

posted @ 2023-02-12 21:52  Leaos  阅读(105)  评论(0编辑  收藏  举报