字符串单模匹配算法 RK / BM / Sunday / SHIFT-AND / KMP

单模匹配问题

单模匹配问题:一个字符串和另一个字符串之间的包含关系确认的问题,
假设求解字符串A中是否包含字符串B。那么字符串A就是文本串,字符串B就是模式串。
也就是求解文本串中是否含有模式串的问题。

前置条件:

文本串的长度为n;
模式串的长度为m;
n>=m>=0;

RK算法

Rabin-Karp算法:就是利用hash思想,我们通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了。

由于模式串B的长度为m,那么如果能比配的上文本串A,也就是找到文本串中的一段长度为m的子串
RK算法就是利用文本串中每一个长度为m的子串对应hash出来的键值,来获得匹配能力。

存在的问题

hash本身纯在的问题:hash冲突.
hash函数的确定。有关常用的hash函数链接

简单实现
// “RK.cpp"
#include<iostream>
#include<climits>
#include<string>
#include<unordered_map>

using namespace std;

long long int hash_func(string s){
    int n = s.size();
    long long key = 0;
    for(int i = 0; i < n; ++i){
        if(key > LONG_LONG_MAX - s[i]) key = key % LONG_LONG_MAX;
        key += s[i] - 'a';
    }
    return key;
}

bool single_catch(string &A,string &B){
    int n = A.size();
    int m = B.size();
    if(n < m) return false;
    int start = 0,end = m - 1;
    unordered_map<long long,int> cap;
    long long key = 0;
    while(end < n){
        key = hash_func(A.substr(start,m));
        if(cap.count(key) == 0) cap[key] = start;
        start++;
        end = start + m - 1;
    }
    long long need = hash_func(B);
    if(cap.count(need)){
        if(A.substr(cap[need],m) == B) {
            return true;
        }
    }
    return false;
}


int main(){
    string A,B;
    cin >> A >> B;
    if(single_catch(A,B)) cout << "I catch it !!"<<endl;
    else cout << "sorry,a worry string showed!!"<<endl;
    return 0;
}

BM算法

Boyer-Moore算法:
RK和暴力匹配算法都是每次往后移动一位,而BM算法可以按照规则每次往后移动好几位。这匹配规则可以是坏字符方法好后缀方法

坏字符方法

原则是从end向start方向匹配。
以文本串A=”abccdssdff“和模式串B="ccds"的匹配为话题展开。其中n=10,m=4;
我们设定文本串中被匹配字符子串的起始下标为start,结束下标为end

文本串: abccdssdff
模式串: ccds

  1. start = 0,end = 3;此时发现cs匹配不上,坏字符就是c,定义其下标为i=3。那么坏字符之前的子串肯定匹配不上。就需要移动start
 移动start,此时分两种情况:
 - 坏字符在模式串B中存在,找到模式串最右的下标即ccdc中的下标index = 1.即start = i - index; end = start + m;
 - 坏字符不存在,start = end + 1;end = start + m; 直接移动到坏字符后面匹配。

文本串: abccdssdff
模式串: ccds

  1. start=2;end=5;此时匹配上。

warning:
上面就是使用坏字符规则来匹配,但是这也会有极端情况,比如主串aaaaaaaaaaaaaaaa,模式串baaa,这时候使用坏字符规则,会发现倒着移动了,可以解决的是每次增加一个判断,如果重新找到的start 比之前的小,就赋值start为之前的值加1.不过这还是比较麻烦的。
这时候就需要用到好后缀规则了。

好后缀方法

原则是从end向start方向匹配。
以文本串A=”abcacabcbcbacabc“和模式串B="cbacabc"的匹配为话题展开。其中n=16,m=7;
我们设定文本串中被匹配字符子串的起始下标为start,结束下标为end

文本串: abcacabcbcbacabc
模式串: cbacabc

  1. start = 0,end = 6;此时end开始匹配没找到好后缀,start = start + 1; end = start + m;

文本串: abcacabcbcbacabc
模式串: cbacabc

  1. start = 1,end = 7;找到好后缀”acabc",但是到b和c匹配不了了,此时后移start的规则如下。
 移动start,此时分两种情况:
 - 好后缀在模式串B中存在,找到模式串最左的下标,令好后缀和模式串中的最左子串进行匹配后,start置位。
 - 好后缀的后缀(c,bc,abc,cabc等)在B中存在,找到模式串最左的下标,令好后缀和模式串中的最左子串进行匹配后,start置位。
 - 都不存在,start = end + 1;end = start + m;

文本串: abcacabcbcbacabc
模式串: cbacabc

  1. 此时发现最左只能匹配到好后缀的后缀c,此时start = 7,end = 14;没匹配上start = start + 1;

文本串: abcacabcbcbacabc
模式串: cbacabc

  1. start = 8,end = 15;没匹配上start = start + 1;

文本串: abcacabcbcbacabc
模式串: cbacabc

  1. start = 9,end = 16;此时匹配上。
坏字符+好后缀配合。

坏字符存在问题:

可能会倒退start,产生越界问题。

好后缀存在问题:

一旦好后缀没匹配上,那么每次都会start = start+1,极端情况下会变成BF匹配算法(Brute Froce),即暴力匹配,本文不涉及暴力匹配。

所以在BM算法中可以同时以模式串构建最右hash(坏字符规则每次匹配模式中最右出现的该字符)和最左hash(好后缀每次匹配最左后缀或最左后缀后缀)。计算两个规则移动更加靠后的来后移start

有兴趣大侠可以课下实现以下。

Sunday算法

sunday算法也是每次移动好几个位,区别是sunday是从start向end方向去匹配的。
该算法与BM算法类似,不做细讲。
可以查看链接讲解,讲的挺好的。

这里我根据链接的讲解写了一个代码,可以参考一下:

// Sunday.cpp"
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//26个字母,仅包括小写字母的字符串单模匹配。可以自己拓展。
vector<int> hasht;

void func(string s){
    int len = s.size();
    for(int i = 0; i < len; ++i){
        hasht[s[i]-'a'] = i;
    }
    return;
}

string Sun(string &A,string &B){
    int n = A.size(),m=B.size();
    int start = 0, end = start + m - 1;
    while(end < n){
        int i = start;
        while(A[i] == B[i]) ++i;
        if(i > end) return A.substr(start,m);
        if(end == n - 1) return "";
        int index = hasht[A[end + 1]];
        if(index == -1)start = end + 1;
        else start = end + 1 - index;
        end = start + m  - 1;
    }
    return "";
}

int main(){
    string A,B;
    cin >> A >> B;
    hasht = vector<int>(26,-1);
    func(B);
    cout << Sun(A,B);
    return 0;
}

SHIFT-AND算法(位运算)

shift-and算法也可以解决字符串单模匹配问题,不过shift-and算法常用于多模匹配;
可以看一下2016ACM/ICPC亚洲区大连站

将模式串转换位二进制数来参与匹配的方法。

文本串:aecaeaecaed
模式串:aecaed

  • 构建模式串二进制数:
    在这里插入图片描述
d[a] = 001001;
d[c] = 000100;
d[d] = 100000;
d[e] = 010010;
  • 构建判断二进制数P,初始化为0。
    P二进制数的意义,P的某位index为1,意味着:
    当前文本串遍历的前index位(以当前遍历位置为结尾)能匹配到模式串的前ndex位前缀

  • P二进制数的迭代,
    P = (P << 1 | 1) & d[s[i]]

运转过程如下:

P = 0;P初始化为00000000;
for(int i = 0; i < n; ++i); 令i从文本串S起始位开始遍历匹配。

  • i = 0时, s[i] = a; d[a]= 001001;
    (P << 1 | 1) 是 00000001,此时与d[s[i]]即d[a]进行与得到P = 00000001;
    解释:当前i=0,且P的第一位为1,即当前遍历位置往前一位可以匹配到模式串的前一位。
  • i = 1时,s[i]=e;P=00000010;
    解释:当前i=1,且P的第二位为1,即当前遍历位置往前二位可以匹配到模式串的前二位。
  • i = 2,P=00000100;
  • i = 3, P = 00001001;
  • i = 4 ; P = 00010010;
  • I = 5; P = 00000001;
  • I= 6; P = 00000010;
  • I = 7; P = 00000100;
  • I = 8; P = 00001001;
  • I = 9; P = 00010010;
  • I = 10;P= 00100000;此时匹配上了(即P == 1 << m);

可以自己理解一下:

KMP算法

KMP算法是字符串单模匹配算法的高频面试点。其思想与BM和Sunday相似,都是每次后移几位。
关键点是next数组的构建
这个讲解的也有很多,链接放在这里 ,最好自己实现一下。

next数组实现方法:

	public static int[] next(String t) {
		int[] next = new int[t.length()];
		next[0] = -1;// 这里规定next[]第1个元素值为-1
		next[1] = 0;
		int j = 2;// 从给定的字符串的第3个字符(下标从0开始)开始计算next[j]数值
		while (j < t.length()) {// 从第三个字符开始逐次求解对应的next[]值
			if (t.charAt(next[j - 1]) == t.charAt(j - 1)) {// 判断Y与X是否相等(X,Y对应上图,下同)
				next[j] = next[j - 1] + 1;// 若Y与X相等,当前next[j]为上一个next[]数组值加1
				j++;// 开始自增一个字符,准备下一次求解
			} else if (t.charAt(0) == t.charAt(j - 1)) {// 若不相等,判断X与子串(模式串)第一个字符是否相同
				next[j] = 1;// 若相同,找到一个长度为1的相同前后缀,即next[j]为1
				j++;
			} else {// 若上述两个条件都不满足,即意味着没有相等的前后缀,即next[j]为0
				next[j] = 0;
				j++;
			}
		}
		return next;
	}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

四库全书的酷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值