首页 > 编程语言 >BF算法与KMP算法

BF算法与KMP算法

时间:2023-02-12 22:01:10浏览次数:38  
标签:字符 BF 匹配 模式 next 算法 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)\)。

标签:字符,BF,匹配,模式,next,算法,KMP,回退
From: https://www.cnblogs.com/tuilk/p/17114816.html

相关文章

  • 《分布式技术原理与算法解析》学习笔记Day09
    非集中式结构什么是非集中式结构?在非集中式结构中,服务的执行和数据的存储被分散到不同的服务器集群,服务器集群之间通过消息传递进行通信和协调,非集中式结构没有中央服务......
  • 算法刷题-插入区间、杨辉三角、移除链表元素
    插入区间给你一个无重叠的,按照区间起始端点排序的区间列表。在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。示例1:输入......
  • 初探富文本之CRDT协同算法
    初探富文本之CRDT协同算法CRDT的英文全称是Conflict-freeReplicatedDataType,最初是由协同文本编辑和移动计算而发展的,现在还被用作在线聊天系统、音频分发平台等等。当......
  • 【LeetCode字符串#05】基于个人理解的KMP算法图解,以及应用到strStr()函数实现
    KMP算法(用于实现strStr())strStr()函数是用来在一个字符串中搜索是否存在另一个字符串的函数,其匹配字符串方式为KMP算法KMP算法基础理论假设有如下两个字符串文本串......
  • 6.4通过莫尔斯编码来看哈夫曼算法的基础
       哈夫曼算法是哈夫曼(D.A.Huffman)于1952年提出来的压缩算法。日本人比较常用的压缩软件LHA,使用的就是哈夫曼算法。   文本文件是由不同类型的字符组合而成的......
  • 6.3RLE算法的缺点
       在实际的文本文件中,同样字符多次重复出现的情况并不多见。虽然针对相同数据经常连续出现的图像、文件等,RLE算法可以发挥不错的效果,但它并不适合文本文件的压缩。......
  • 基础算法三
    单链表//head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点inthead,e[N],ne[N],idx;//初始化voidinit(){head=-1;i......
  • LeetCode回溯算法
    回溯模板1privatevoidbacktrack("原始参数"){2//终止条件(递归必须要有终止条件)3if("终止条件"){4//一些逻辑操作(可有可无,视情况而定)......
  • 6.2RLE算法的机制
       由于半角字母中,1个字符是作为1个字节的数据被保存在文件中的,因此上述文件的大小就是17个字节。我们可以使用方式来压缩。   把文件内容用“数据x重复次数......
  • 代码随想录算法Day10 | 理论基础 232.用栈实现队列 225. 用队列实现栈
    理论基础栈是先进后出,队列是先进先出。如图所示。232.用栈实现队列题目链接:232.用栈实现队列-力扣(LeetCode)题目请你仅使用两个栈实现先入先出队列。队列应当支......