文章目录

  • 1. 算法背景
  • 2. BM(Boyer-Moore)算法
    • 2.1 坏字符规则(bad character rule)
    • 2.2 好后缀规则(good suffix shift)
    • 2.3 复杂度及完整代码
  • 3. KMP(Knuth Morris Pratt)算法
    • 3.1 好前缀 和 坏字符规则
    • 3.2 高效构建 失效函数
    • 3.3 复杂度及完整代码
  • 4. 总结

1. 算法背景

文本编辑器/搜索引擎 随着处理的数据量的不断增加,对字符串匹配的效率要求越来越高,字符串匹配的效率也在不断演进。

传统的字符串匹配算法:BF(Brute Force) 和 RK(Rabin Karp) 算法 都消耗大量的时间。其中BF 比较粗暴,会将主串中的每一个字符都和模式串进行比较。RK在其基础上做了优化,虽然不再一个一个字符比较,而是从主串中构建模式串长度的子串hash,从而达到高效的hash比较,但是构建每个子串的hash值的过程同样需要取m-n+1个子串,并且对其计算hash,代价同样巨大。

以上两种字符串匹配算法满足不了我们在大数据量下文本编辑器的查找性能(vim / less / more 的内容查找),对字符串匹配高效性能的需求 促进了BM/KMP这样的高效匹配算法的出现。核心匹实现 还是说 每一次发现主串和模式串不匹配的字符之后,尽可能在主串中移动多个一定不会匹配的字符。

当然,目前BF,RK,BM,KMP 这样的算法能够在单模式串之间完成匹配,也就一个主串,一个模式串之间的匹配。对于 文本编辑器,像VIM这样的 还需要有多模式串之间的高性能匹配需求,也就是一个主串多个模式串之间的匹配。所以,Trie树这样的数据结构 以及 AC 自动机这样的高效算法 应运而生,并且他们被广泛应用在了搜索引擎之中。

本篇将带你欣赏 高效的单模式串匹配算法 BMKMP

2. BM(Boyer-Moore)算法

像背景中介绍的BF/RK 算法,核心还是对主串中的一个一个字符进行匹配,如下图,如果第一次匹配到c发现模式串对应下标为d不匹配了,则会回到主串的第二个字符作为重新开始匹配的起始字符。

更加高效的匹配方式应该是跳过一定不会匹配的字符,比如上图中主串的bc ,直接将主串的起始匹配字符向后移动到a开始进行匹配,这样能够有效减少匹配次数。

所以BM算法是为了找到能够保证不会漏过匹配可能性的情况下 高效移动的规律。

主要就是两种规则:

  • 坏字符
  • 好后缀

2.1 坏字符规则(bad character rule)

一般情况下,我们完成字符串匹配的过程是从左向右进行匹配的,比较符合我们的思维习惯,而坏字符规则 是反向 从右向左进行匹配。

先匹配最右边的字符,发现主串c和模式串d不匹配,则认为c是坏字符,接下来模式串的移动将向后滑动三位,移动到坏字符之后。仍然从右向左进行匹配,发现主串中的a和模式串的d不匹配。

接下来的移动则不能直接移动到坏字符之后,因为模式串中也有一个a,如果我们直接移动到主串坏字符之后,就错过了匹配, 应该将模式串向后滑动两位,如下:

所以坏字符规则下的算法移动方式如下:

  1. 从右向左匹配模式串和主串,发现坏字符 ch时记录 此时对应模式串的下标 Si
  2. 检测模式串中是否有ch,有的话将其下标记录Xi,否则记录Xi = -1
  3. 移动时 直接移动 Si - Xi的长度

比如上图 的算法移动方式如下:

  • 第一次发现坏字符时 Si=2;Xi=-1,则直接将模式串滑动Si-Xi=3
  • 第二次发现坏字符时Si=2;,因为模式串存在字符a,所以Xi=0,最后移动Si-Xi=2
  • 第三次 Match

实现代码:

  • generateBC 函数会预先记录模式串中每一个字符的index,如果有两个一样的字符,将后一个字符的下标作为index(防止匹配漏掉)
  • bm_alg 函数中先从右向左,发现不匹配的字符,记录该字符对应的模式串下标; 从mp中取坏字符在模式串中存在的index,相减即可。
// storage the des's position
//
// @params:
// des: destination string
// mp: update the relationship between char in
// des with it's index in des
//
// examples:
// s t r _ e x a m p l e
// 0 1 2 3 4 5 6 7 8 9 10
//
// mp['s'] = 0
// ...
// mp['e'] = 10
// ....
void generateBC(string des, map<char ,int> &mp) {int i;for(i = 0;i < des.size(); i ++) {// and the map relationship between des's// element with it's positionmp[des[i]] = i; }
} int bm_alg(string src, string des) {map<char ,int> last_pos;generateBC(des, last_pos);int i=0;int src_len = src.size();int des_len = des.size();while (i <= (src_len - des_len)) {int j;// find the bad charfor (j = des_len - 1; j >= 0; j--) {if (src[i+j] != des[j]) {break;}}if (j < 0) {return i;}// bad char's move positionint x = j - last_pos[src[i+j]];// move the max of position between badchari = i + x;}return -1;
}

坏字符实现的最好时间复杂度能够达到O(n/m),尤其是主串和模式串有类似特点的场景:aaabaaabaaabaaabaaaa 这种情况下的匹配时十分高效的。

但是坏字符并不能处理所有的匹配情况,比如aaaaaaaaaaaaabaaa这种情况,使用坏字符规则 下的滑动位置会变成负数(Si=0,Xi=3,滑动-3,显然不合理),所以BM算法还需要好后缀规则。

2.2 好后缀规则(good suffix shift)

当我们从右往左遍历模式串时找到坏字符时,前面已经相同的部分表示好后缀。

通过好后缀方式移动的算法有两种:

  • 如果好后缀在模式子串v 中的另一个位置还存在v’,则将模式子串的v’ 和主串的好后缀对齐。
  • 如果好后缀不存在于模式串中的其他位置,则判断好后缀的后缀子串 是否包含在模式串的前缀子串中;包含,则移动模式串 使后缀子串和匹配的前缀子串对齐;否则,按照坏字符规则移动。

对于第一种算法,移动方式如下:

找到好后缀在模式串的起始下标Si,以及另一个在模式串中匹配的好后缀的起始下标Xi,最终移动Si - Xi。

为什么会有第二种判断好后缀的后缀子串的过程呢?如下匹配:

但实际上,这种情况已经过度滑动了,按照第一种移动方式会错过正确的结果。

第二种办法是取好后缀的后缀子串,判断其是否能够和模式串的前缀子串匹配,匹配则移动到两个子串相匹配的位置。

后缀子串表示最后一个字符相同,前缀子串表示第一个字符相同;比如:abcd的后缀子串为 d,cd,bcd;对应的前缀子串为 a,ab,abc。这两个指标是固定的,且能够在开始匹配前快速初始化模式串的所有后缀子串和前缀子串 ,已经其是否匹配。

构建过程如下:

  • suffix 数组下标表示 后缀子串的长度,值表示后缀子串在模式串中匹配的另一个子串的起始位置。比如suffix[1]=2,后缀子串b长度为1,模式串中还有另一个b,则2表示这个b的其实下标。
  • Prefix 数组 的bool值表示这个后缀子串是否和前缀子串匹配。

构建两个数组的代码如下:

// generate suffix and prefix arr
//
// @params:
// des: destination str
// suffix: the first position of the suffix str in des
// prefix: if the suffix exist in prefix of des, update
// prefix arr vector<bool> to true
//
// examples:
// des: b a b c a b
// pos: 0 1 2 3 4 5
// suffix: b, ab, cab, bcab, abcab
// suffix[1] = 0
// suffix[2] = 1
// suffix[3] = -1
// suffix[4] = -1
// suffix[5] = -1
//
// prefix[1] = true
// prefix[2] = true
// prefix[3] = false
// ...
void generateGS(string des, vector<int> &suffix,vector<bool> &prefix) {int i;int des_len = des.size();for (i = 0;i < des_len; i++) {suffix.push_back(-1);prefix.push_back(false);}// traverse the prefix str , update the suffix vector // and prefix vectorfor (i =0; i< des_len - 1; i++) {int j = i; // prefix start indexint k = 0;while(j >= 0 && des[j] == des[des_len - 1 -k]) {--j;++k;suffix[k] = j + 1;}if (j == -1) {prefix[k] = true;}}
}

完整好后缀的匹配规则 整体以尽可能短得方式移动:

  1. 判断好后缀u是否包含在模式串中的其他位置u’, 移动到u’的起始位置,结束。
  2. 判断好后缀的后缀子串v 是否包含在模式串的其他位置v’,并且和前缀子串匹配,则移动到前缀子串的起始位置,结束。
  3. 移动模式串长度 m 个位置,结束。

移动代码如下:

// move the des str to a position
//
// @params:
// j: bad char's index
// des: destination string
// suffix: suffix in des's start index
// prefix: wether the suffix math with prefix
//
// case:
// src: a b c a c a b c b c b a c a b c
// des: c b a c a b c
int movePosition(int j, string des, vector<int> suffix, vector<bool> prefix) {// case1: move to the longest len of sufix// 'k' is the good suffix's start index// 'j' is the bad char's indexint k = des.size() - 1 - j;if (suffix[k] != -1) {return j - suffix[k] + 1;}// case2: move to other suffix position//// longest suffix's range [j+1, len(des)-1]// other suffix's range [j+2, len(des)-1]for (int r = j + 2; r <= des.size() - 1; r++) {if (prefix[des.size() - r] == true) {return r;}}// case3: move the len of desreturn des.size();
}

在好后缀的移动规则和坏字符的移动规则之间取最大的移动位置来移动即可(好后缀的移动是普遍大于坏字符的移动方式),也就是好后缀的规则可以单独使用。

2.3 复杂度及完整代码

  • 空间复杂度:使用额外的两个vector和一个map来保存中间数据。其大小与模式串的长度m有关。
  • 时间复杂度:
    1. A new proof of the linearity of the Boyer-Moore string searching algorithm 证明最坏场景上限为O(5n)
    2. Tight bounds on the complexity of the Boyer-Moore string matching algorithm 证明最坏场景上限为O(3n)

source_code: https://github.com/BaronStack/DATA_STRUCTURE/blob/master/string/bm_alg.cc

完整代码实现如下:

#include <iostream>
#include <map>
#include <string>
#include <vector>#define MAP_SIZE 256using namespace std;// storage the des's position
//
// @params:
// des: destination string
// mp: update the relationship between char in
// des with it's index in des
//
// examples:
// s t r _ e x a m p l e
// 0 1 2 3 4 5 6 7 8 9 10
//
// mp['s'] = 0
// ...
// mp['e'] = 10
// ....
void generateBC(string des, map<char ,int> &mp) {int i;for(i = 0;i < des.size(); i ++) {// and the map relationship between des's// element with it's positionmp[des[i]] = i; }
} // generate suffix and prefix arr
//
// @params:
// des: destination str
// suffix: the first position of the suffix str in des
// prefix: if the suffix exist in prefix of des, update
// prefix arr vector<bool> to true
//
// examples:
// des: b a b c a b
// pos: 0 1 2 3 4 5
// suffix: b, ab, cab, bcab, abcab
// suffix[1] = 0
// suffix[2] = 1
// suffix[3] = -1
// suffix[4] = -1
// suffix[5] = -1
//
// prefix[1] = true
// prefix[2] = true
// prefix[3] = false
// ...
void generateGS(string des, vector<int> &suffix,vector<bool> &prefix) {int i;int des_len = des.size();for (i = 0;i < des_len; i++) {suffix.push_back(-1);prefix.push_back(false);}// traverse the prefix str , update the suffix vector // and prefix vectorfor (i =0; i< des_len - 1; i++) {int j = i; // prefix start indexint k = 0;while(j >= 0 && des[j] == des[des_len - 1 -k]) {--j;++k;suffix[k] = j + 1;}if (j == -1) {prefix[k] = true;}}
}// move the des str to a position
//
// @params:
// j: bad char's index
// des: destination string
// suffix: suffix in des's start index
// prefix: wether the suffix math with prefix
//
// case:
// src: a b c a c a b c b c b a c a b c
// des: c b a c a b c
int movePosition(int j, string des, vector<int> suffix, vector<bool> prefix) {// case1: move to the longest len of sufixint k = des.size() - 1 - j;if (suffix[k] != -1) {return j - suffix[k] + 1;}// case2: move to other suffix position//// longest suffix's range [j+1, len(des)-1]// other suffix's range [j+2, len(des)-1]for (int r = j + 2; r <= des.size() - 1; r++) {if (prefix[des.size() - r] == true) {return r;}}// case3: move the len of desreturn des.size();
}// boyer-moore algorithm
//
// two rules:
// 1. bad char
// 2. good suffix
int bm_alg(string src, string des) {if (src.size() == 0 || des.size() == 0) {return -1;}map<char ,int> last_pos;vector<int> suffix;vector<bool> prefix;generateBC(des, last_pos);generateGS(des, suffix, prefix);int i=0;int src_len = src.size();int des_len = des.size();while (i <= (src_len - des_len)) {int j;// find the bad charfor (j = des_len - 1; j >= 0; j--) {if (src[i+j] != des[j]) {break;}}if (j < 0) {return i;}// bad char's move positionint x = j - last_pos[src[i+j]];int y = 0;// good suffix's move positionif (j < des_len - 1) {y = movePosition(j, des, suffix, prefix);}// move the max of position between badchar and // good suffixi = i + std::max(x,y);}return -1;
} int main() {string src;string des;cout << "input src ";cin >> src;cout << "input des ";cin >> des;if (bm_alg(src, des) != -1) {printf("%s is the substr of %s with index : %d\n",des.c_str(), src.c_str(), bm_alg(src,des));} else {printf("not match \n");}return 0;
}

3. KMP(Knuth Morris Pratt)算法

有了之前对BM算法的理解,接下来再看KMP算法就事半功倍了。

BM算法中通过 坏字符好后缀 规则 来约束模式串的滑动,两个规则的结合能够高效得检索模式串是否和主串匹配。KMP算法相比于BM算法 都希望加速模式串的滑动,提升匹配效率。不同的是KMP将好后缀规则变成了好前缀规则。

如上图,将模式串和主串中已经匹配的字符串称为好前缀,不同的字符同样是坏字符,就像BM算法的好后缀规则下,监测到好后缀的后缀子串在模式串中的其他位置,则向后滑动到这个匹配后缀子串的起始位置即可。同样,KMP的好前缀中也希望滑动到匹配的后缀子串的位置(为了避免滑动过多,跳过匹配的情况,这里选择最大匹配的后缀子串)。

3.1 好前缀 和 坏字符规则

如下图,取好前缀的最大匹配后缀子串,其长度为k,将模式串匹配的前缀子串的结尾index移动 j-k个位置 即能够和最大匹配后缀子串对齐,此事坏字符的位置i不用发生变化。

由上图可知,KMP的滑动除了需要依赖主串找到坏字符之外, 其他的最大匹配前缀子串的计算并不需要主串的参与,仅仅通过模式串就能够预先完成所有情况的最大匹配前缀子串的结尾下标计算。

也就是我们在KMP的算法规则中 只需要知道当出现坏字符时,模式串的最大可匹配前缀的下标即可,这样就能够完成模式串的移动了。这个最大可匹配下标的计算也就是KMP 常说的失效函数的计算,属于KMP的核心计算过程了,我们将失效函数用next数组来表示。

基本的kmp代码实现如下:

int kmp_alg(string src, string des) {if (src.size() == 0 || des.size() <= 0) {return -1;}int src_len = src.size();int des_len = des.size();vector<int> next;int i, j;next.resize(des_len);// Get the next vectorgetNext(des,next);for (i = 0;i < src_len; i++) {// 1. Find the bad char// 2. Next[j-1] is the longest match prefix's tail index//    move the j to the destinations.// // Example://                i//      0 1 2 3 4 5 6 7 8 9// src: a b a b a e a b a c// des: a b a b a c d //      0 1 2 3 4 5 6//                j//// when find bad char: i = 5, j = 5;// good prefix is : ababa// longest match prefix : aba// longest match prefix tail in des: 2// next[j-1] : next[5-1] = 2// j: 2 + 1 = 3//// after slide ,the src and des is bellow://                i//      0 1 2 3 4 5 6 7 8 9// src: a b a b a e a b a c// des:     a b a b a c d //          0 1 2 3 4 5 6//                j// while(j > 0 && src[i] != des[j]) {j = next[j-1] + 1; }// The good prefix, just add the jif (src[i] == des[j]) {j++;}// Match the des and return the index in srsif (j == des_len) {return (i - des_len + 1);}}return -1;
}

接下来看一下如何构建失效函数这一部分。

3.2 高效构建 失效函数

如下图,左侧的图能够非常直观得看到 当发现了好前缀之后,如何找到其最大可匹配前缀的过程。

通过将好前缀的所有前缀子串和后缀子串列出来,找到其中最长一个能够完成匹配的前缀子串,即是我们的最长可匹配前缀子串。这个寻找过程,我们也可以用来直接构造如右侧图的next数组(失效函数),也就是暴力法。

暴力法就是 **要对模式串中的每一个好前缀(m-1个)都要分别找出其后缀子串和前缀子串,并从中找到能够匹配的最长的子串,**这个代价是极大的。

更优的办法 是使用next[i-1] 来 尝试构建next[i],理论上 next[i] 中肯定包含next[i-1],因为next[i]对应的好前缀是包含next[i-1]对应的好前缀的,这种方式理论上是可行的,需要关注的细节如下:

  1. case1: 假如next[i-1] = k-1,也就是子串des[0,k-1]是des[0,i-1]的最长可匹配前缀子串。如果des[0,k-1]的下一个字符des[k]和 des[0,i-1]的下一个字符des[i] 相等,则next[i] = next[i-1] + 1= k。

  2. case2: 如果des[0,k-1]的下一个字符des[k]和 des[0,i-1]的下一个字符des[i] 不相等,这个时候的处理稍微麻烦一些。

    也就是des[0,k-1]中无法满足找到最长可匹配前缀子串,那么des[0,next[k-1]]的下一个字符能否满足等于des[i]的要求呢,即des[next[k-1]]和des[i]是否相等,想等则认为des[0,next[k-1]] 是 des[0,i]的最长可匹配前缀子串,next[i] = next[k-1]+1 。

    整体上类似于数学的递推公式:

    • 前一个的最长串des[0,k] 的下一个字符des[k+1]不与最后一个字符des[i]相等,则需要找前一个的次长串
    • 问题也就变成了求des[0, next[k]]的最长串:如果下一个字符des[next[k] + 1]还不和des[i]相等,那么继续回溯到求des[0,next[next[k]]]的最长串,再判断des[next[next[k]] + 1]和des[i]是否相等,相等则next[i] = next[next[k]] + 1
    • 否则继续,依此直到找到 能够和des[i]相等的字符。此时的next[next[…]]+1 就是next[i]的值。

    next数组的求值代码如下:

    void getNext(string des, vector<int> &next) {next[0] = -1;int k = -1;int i;// i begin with 1, next[0] always is -1for (i = 1; i< des.size(); i++) {// Find the longest match prefix by judge // two char dex[i] and des[k+1].// Just like case1 and case2: next[i-1]=k-1// if des[i] == des[k-1], then next[i] = next[i-1] + 1 = k;while(k != -1 && des[i] != des[k+1]) {//let 'k' storage 'next[next[...]]'k = next[k];}// find the match char, let k ++if (des[i] == des[k+1]) {k++;}next[i] = k;}
    }
    

关于次长可匹配前缀和最长可匹配前缀之间的关系可以参考如下图理解一下:
y = next[i-1] 其实就是上面代码中的 k = next[k]的逻辑。

3.3 复杂度及完整代码

KMP算法以难理解著称,总体上就是一个回溯的过程,当前next[k]不满足匹配子串[0,k]的下一个字符相等时,则回到next[next[k]]看看。

空间复杂度:维护一个 m(模式串长度)大小的数组。

时间复杂度:

  • 在next数组计算过程中,第一层for 循环[1,m-1],第二层的while循环中k=next[k]并不是每次都会执行,总的执行次数可以通过语句k++来判断,并不会超过m次,毕竟next[k]的回溯过程一定会碰到-1的情况。所以next数据计算过程整体的时间复杂度是O(m) – while循环的执行次数小于m,且不稳定,可以当作常量来看。

  • 在外部主体进行匹配的过程中,外部循环[0,n-1],n表示主串的长度;因为j本身比i小, 所以while循环中 j=next[j-1]+1不可能循环n次,又因为next[j-1] 肯定小于j,相当于j之前的一个下标,也就是while中语句的执行次数是小于m的,也能够看作一个常量。

    即外部匹配过程的时间复杂度是O(n)

所以总体的时间复杂度是O(m+n),当然相比于BM 的复杂度计算还不够严谨,像O(5n)也是O(n)的范畴,这个就比BM算法消耗时间更久了。

source code:
https://github.com/BaronStack/DATA_STRUCTURE/blob/master/string/kmp_alg.cc

完整代码如下:

#include <iostream>
#include <vector>
#include <string>using namespace std;void getNext(string des, vector<int> &next) {next[0] = -1;int k = -1;int i;// i begin with 1, next[0] always is -1for (i = 1; i< des.size(); i++) {// Find the longest match prefix by judge // two char dex[i] and des[k+1].// Just like case1 and case2: next[i-1]=k-1// if des[i] == des[k-1], then next[i] = next[i-1] + 1 = k;while(k != -1 && des[i] != des[k+1]) {//let 'k' storage 'next[next[...]]'k = next[k];}// find the match char, let k ++if (des[i] == des[k+1]) {k++;}next[i] = k;}
}int kmp_alg(string src, string des) {if (src.size() == 0 || des.size() <= 0) {return -1;}int src_len = src.size();int des_len = des.size();vector<int> next;int i, j;next.resize(des_len);// Get the next vectorgetNext(des,next);j = 0;for (i = 0;i < src_len; i++) {// 1. Find the bad char// 2. Next[j-1] is the longest match prefix's tail index// move the j to the destinations.// // Example://                i//      0 1 2 3 4 5 6 7 8 9// src: a b a b a e a b a c// des: a b a b a c d //      0 1 2 3 4 5 6//                j//// when find bad char: i = 5, j = 5;// good prefix is : ababa// longest match prefix : aba// longest match prefix tail in des: 2// next[j-1] : next[5-1] = 2// j: 2 + 1 = 3//// after slide ,the src and des is bellow://                i//      0 1 2 3 4 5 6 7 8 9// src: a b a b a e a b a c// des:     a b a b a c d //          0 1 2 3 4 5 6//                j//while(j > 0 && src[i] != des[j]) {j = next[j-1] + 1; }// The good prefix, just add the jif (src[i] == des[j]) {j++;}// Match the des and return the index in srsif (j == des_len) {return (i - des_len + 1);}}return -1;
}int main() {string s1,s2;cin >> s1;cin >> s2;if (kmp_alg(s1,s2) == -1) {cout << s1 << " with " << s2 << " not match !" << endl;} else {cout << s1 << " with " << s2 << " match !" << endl;}return 0;
}

4. 总结

本节仅仅描述的是高效的单模式串匹配算法,也能够看到BM/KMP这样的复杂算法的设计,还是比较难以理解,尤其是KMP中用到了一些动态规划的思想。需要花费大量的时间和精力去思考实验,才能了解清楚内部的详细设计(从上周天开始研究,跟着极客时间的王争老师理解,差点怀疑自己智力是不是有问题,最后还是下班路上想明白细节。。。还是得尽可能多得思考训练,数据结构和算法功底决定未来技术壁垒的高度,尤其是现在搞的存储引擎,分布式存储/分布式数据库领域的核心,更是一点不能马虎),欢迎大家一起讨论。

后续将会深入多模式串的数据结构Tire树和算法AC自动机的设计,毕竟搜索引擎/文本编辑器中还是需要高效的多字符串的匹配才能满足实际生产环境的需求。

字符串匹配算法 -- BM(Boyer-Moore) 和 KMP(Knuth-Morris-Pratt)详细设计及实现相关推荐

  1. iptables --algo 字符串匹配算法 bm kmp

    http://blog.csdn.net/l953972252/article/details/51331001 字符串匹配一直是计算机领域热门的研究问题之一,多种算法层出不穷.字符串匹配算法有着很强 ...

  2. Python:实现knuth morris pratt(KMP)算法(附完整源码)

    Python:实现knuth morris pratt(KMP)算法 from __future__ import annotationsdef kmp(pattern: str, text: str ...

  3. Prefix function. Knuth–Morris–Pratt algorithm

    Prefix function. Knuth–Morris–Pratt algorithm Prefix function definition You are given a string \(s\ ...

  4. Python:实现前缀Knuth–Morris–Pratt 算法(附完整源码)

    Python:实现前缀Knuth–Morris–Pratt 算法 def prefix_function(input_string: str) -> list:# list for the re ...

  5. 【算法篇-字符串匹配算法】BF算法和KMP算法

    目录 前言 1. BF算法 1.1 画图分析 1.3 BF 算法的时间复杂度 2. KMP 算法 2.1 KMP 算法和 BF 算法 的区别 2.1.1 为什么主串不回退? 2. 2 next 数组 ...

  6. 数据结构之字符串匹配算法(BF算法和KMP算法)

    字符串匹配算法: 就是给定两个串,主串(s)和子串(sub), 查找子串是否在主串里面,如果找到便返回子串在主串中第一个元素的位置下标,否贼返回-1,. 在这里我 们讨论的时候主要用字符串来举例实现. ...

  7. KMP子串匹配算法(Knuth–Morris–Pratt algorithm)

    假设原串S,模式串M 简单的说这个算法的主要思想就是利用模式串M自身的匹配性质,快速找到下一跳的位置. 而这个下一跳的位置只与模式串相关,所以可以根据这个串制作一个next的表T,来指示出一旦匹配不成 ...

  8. 字符串匹配算法:KMP算法

    KMP (Knuth–Morris–Pratt) 算法是一种改进的字符串匹配算法,它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的. 题目:给定一个 T 字符串和一个 ...

  9. 数据结构与算法之美笔记——基础篇(下):图、字符串匹配算法(BF 算法和 RK 算法、BM 算法和 KMP 算法 、Trie 树和 AC 自动机)

    图 如何存储微博.微信等社交网络中的好友关系?图.实际上,涉及图的算法有很多,也非常复杂,比如图的搜索.最短路径.最小生成树.二分图等等.我们今天聚焦在图存储这一方面,后面会分好几节来依次讲解图相关的 ...

最新文章

  1. 机器学习在高德搜索建议中的应用优化实践
  2. cdn与服务器的关系_掐指算算: 你的CDN多花了几百万?
  3. springmvc+mybatis+ehcache+redis+dubbo架构
  4. 隐藏html和php后缀的简易方法
  5. 记录一次失败的Git操作
  6. CNVD-2022-10270/CNVD-2022-03672 向日葵RCE复现
  7. [Noi2015]小园丁和老司机
  8. 中国矿业大学(北京)第二届ACM程序设计公开赛(决赛)
  9. css 入场动画_CSS3 六边形标题入场动画
  10. 阿里 替换mysql的数据库_mysql替换数据库
  11. java中写html标签,java写html剔出标签
  12. shell中sudo和su命令
  13. cannot have an existing value
  14. 从erp入门说到产业互联网络
  15. 雨中的尾巴(线段树合并+树上差分)
  16. AndroidStudio检测不到模拟器的解决方案(百试不爽)
  17. 判断两条直线是否相交c语言,计算几何-两条线段是否相交(三种算法)
  18. Android 增量更新实战
  19. 高新兴与中兴通讯再合作:受让中兴物联11.43%股权
  20. 电脑无线不显示宽带不能连接服务器,电脑上网为什么只显示宽带连接不显示无线网络连接?...

热门文章

  1. CCS Font 知识整理总结
  2. hibernate3
  3. 软件行业项目经理主要的职责是什么?(转)
  4. 调试 后台 ajax post 对应的php的方法
  5. redis服务器索引文件删除,Redis基本命令整理
  6. 南昌计算机专业好的专科,2021年南昌计算机专业学校好不好
  7. mysql sql语句编码_SQL语句实用例子 MySQL编码设置
  8. window linux上传文件命令,windows通过cmd命令行使用sftp上传文件至linux
  9. php for等边三角形,利用js实现等腰三角形
  10. 抖音爬取粉丝用户列表_抖音分享页用户信息爬取