0%

第欧根尼

什么是字符串匹配

定义

definition:
又称模式匹配(pattern matching)。该问题可以概括为:“给定字符串 SSTT,在主串 SS 中寻找子串 TT。”字符串 TT 又被称为模式串(pattern)。

类型

type:
1.单串匹配:给定一个模式串和一个待匹配串,找出前者在后者中的所有位置。
2.多串匹配:给定多个模式串和一个待匹配串,找出这些模式串在后者中的所有位置。
2.1:出现多个待匹配串时,将它们直接连起来便可作为一个待匹配串处理;
2.2:可以直接当作单串匹配,但是效率不够高;
3.其他类型:例如匹配一个串的任意后缀,匹配多个串的任意后缀…

暴力做法

BF:
简称BF(Brute Force)算法,该算法的基本思想是从主串 SS 的第一个字符开始和模式串 TT 的第一个字符进行比较,若相等则继续比较至后续字符;否则模式串 TT 回退到第一个字符,重新和主串的第二个字符进行比较。如此往复,直到 SSTT 中的所有字符串比较完毕。

实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
s: 待匹配的主串
t: 模式串
n: 主串的长度
m: 模式串的长度
*/
std::vector<int> match(char *s ,char *t,int n,int m){
std::vector<int> ans;
int i,j;
for(i = 0; i < n - m +1; i++){
for(j = 0;j < m; j++){
if(s[i + j] != t[j])
break;
}
if(j == m)
ans.push_back(i);
}
return ans;
}
时间复杂度

nn 为主串的长度,mm 为模式串的长度,默认 mnm \ll n

BF算法匹配成功时,在最好的情况下,只有一趟匹配成功,此趟比较次数为 mm,而其余每趟不成功的匹配都发生在模式串的第一个字符,还需要 nmn-m 次比较,总比较次数为 nn,故时间复杂度为 O(n)O(n);在最坏情况下,匹配成功的趟数为 nm+1n-m+1,每趟比较次数为 mm,总比较次数为 m(nm+1)m(n-m+1),故时间复杂度为 O(mn)O(mn)

BF算法匹配失败时,在最好情况下,每趟不成功的匹配都发生在模式串的第一个字符,BF算法要执行 nm+1n-m+1 次比较,时间复杂度为 O(n)O(n);在最坏情况下,每趟不成功的匹配都发生在模式串的最后一个字符,BF算法要执行 m(nm+1)m(n-m+1) 次比较,时间复杂度为 O(mn)O(mn)

如果模式串中至少有两个不同的字符,则BF算法的平均时间复杂度为 O(n)O(n)

BF暴力匹配示意图|700

KMP算法

一些定义

前缀:从串首开始到某个位置 ii 结束的连续子串。字符串 SS 的以 ii 结尾的前缀记作 prefix(S,i)=S[0,i]\mathrm{prefix}(S, i) = S[0, i],其中 0iS10 \le i \le |S| - 1

真前缀:除字符串 SS 本身之外的所有前缀,即 prefix(S,i)\mathrm{prefix}(S, i) 中满足 0iS20 \le i \le |S| - 2 的部分。

后缀:从某个位置 ii 开始到串尾结束的连续子串。字符串 SS 的以 ii 开始的后缀记作 suffix(S,i)=S[i,S1]\mathrm{suffix}(S, i) = S[i, |S| - 1],其中 0iS10 \le i \le |S| - 1

真后缀:除字符串 SS 本身之外的所有后缀,即 suffix(S,i)\mathrm{suffix}(S, i) 中满足 1iS11 \le i \le |S| - 1 的部分。

下面我们看一组示例字符串

KMP next数组推导|700

这个 nextnext 数组到底起着什么作用呢?假设有一个字符串 “abacad" 现在作为模式串跟主串匹配,当我比较到下标为[5]的时候与主串不匹配,我们看前一位(下标[4])的 nextnext 数组的值为1,于是将模式串下标[1]处的字符与主串当前的字符继续比较,此时模式串中已确认匹配的前缀长度为1。

为什么我们要在不匹配的时候看前一个下标对应的 nextnext 值呢,实际上我们对比朴素算法(BF算法),我们可以发现,我们在失配的瞬间,模式串 PP 的前 next[j1]next[j - 1] 个字符已经与主串匹配,nextnext 数组已经储存了匹配成功的 prefixprefix ,反观BF算法,失配时文本指针 ii 回退到 ij+1i - j + 1,把已经匹配过的 jj 个字符重新比较一遍,这些全是多余操作。这也就是KMP算法优于BF算法的原因。

一句话总结:失配后唯一合法的降级目标,是已匹配部分 P[0..j1]P[0..j-1]borderborderprefix[j1]prefix[j-1] 是其中最大的一个,既保住了最多已确认的进度,又通过 border 链不漏掉任何更小的候选,所以直接跳过去零多余比较。在上一轮DFA 的视角下,这就是用 prefixprefix 按需计算失配转移 δ(j,text[i])δ(j, text[i]),而文本指针永不回退正是 DFA 逐字符推进的体现。

尾迹

来来回回,删删改改,已经一整天了。也许KMP算法只是 C++C++ 算法中微不足道的一环,而我的知识储备也仅仅是沧海一粟,越是学得深入,越觉得自己无知。我愿做那个木桶里的第欧根尼,隔绝市井的喧嚣,在太阳的浸润中,温养岁月绵长,慨歌未央…

参考文献

[1](字符串匹配 - OI Wiki)