BF算法
BF算法,即暴风(Brute Force)算法,也叫暴力破解法,是普通的模式匹配算法。
算法思想:将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。
时间复杂度:O(n*m)
int BF_match(char *T,char *P){
int i = 0, j = 0;
for (; T[i] != '\0' && P[j] != '\0'; i++)
if (T[i] == P[j])
j++;
else
{
i -= j;
j = 0;
}
if (P[j] == '\0') return i - j;
else return -1;
}
RK算法
算法思想:首先计算子串的HASH值,之后分别取原字符串中子串长度的字符串计算HASH值,比较两者是否相等:如果HASH值不同,则两者必定不匹配,如果相同,由于哈希冲突存在,也需要一一比对进行判定
时间复杂度:O(n)~O(n*m)
#define M 97 //散列表长度:既然这里并不需要真地存储散列表,不妨取更大的素数,以降低误判的可能
#define R 10 //基数:对于二进制串,取2;对于十进制串,取10;对于ASCII字符串,取128或256
#define DIGIT(S, i) ((S)[i] - '0') //取十进制串S的第i位数字值(假定S合法)
typedef __int64 HashCode; //用64位整数实现散列码
bool check1by1(char *P, char *T, size_t i) { //指纹相同(概率很小)时,逐位比对以确认是否真正匹配
for (size_t m = strlen(P), j = 0; j < m; j++, i++)
if (P[j] != T[i]) return false;
return true;
}
HashCode prepareDm(size_t m) { //预处理:计算最高位的基数R^(m - 1) % M
HashCode Dm = 1;
for (size_t i = 1; i < m; i++) Dm = (R * Dm) % M; //直接累乘m - 1次,并取模
return Dm;
}
void updateHash(HashCode &hashT, char *T, size_t m, size_t k, HashCode Dm) { //子串指纹快速更新算法
hashT = (hashT - DIGIT(T, k - 1) * Dm) % M; //在前一指纹基础上,去除首位T[k - 1]
hashT = (hashT * R + DIGIT(T, k + m - 1)) % M; //添加末位T[k + m - 1]
if (0 > hashT) hashT += M; //确保散列码落在合法区间内
}
int RK_match(char *T, char *P) { //串匹配算法(Karp-Rabin)
size_t m = strlen(P), n = strlen(T);
if (m > n) return -1; //m必须大于n
HashCode Dm = prepareDm(m), hashP = 0, hashT = 0;
for (size_t i = 0; i < m; i++) { //初始化
hashP = (hashP*R + DIGIT(P, i)) % M; //计算模式串对应的散列值
hashT = (hashT*R + DIGIT(T, i)) % M; //计算主串(前m位)的初始散列值
}
for (size_t k = 0;;) {
if (hashT == hashP) {
if (check1by1(P, T, k)) return k; //成功
}
if (++k > n - m) return -1; //失败
else updateHash(hashT, T, m, k, Dm); //否则,更新子串散列码,继续查找
}
}
KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的。
算法思想:用next数组存储当前字符之前的字符串前、后缀最长公共元素长度,匹配失败时通过next数组偏移
时间复杂度:O(n+m)
int *buildNext(char *P) {
size_t len = strlen(P), i = 0;
int *next = new int[len];
int j = next[0] = -1;
while (i < len - 1)
if (j == -1 || P[i] == P[j]) {
i++;
j++;
next[i] = j;
}
else
j = next[j];
return next;
}
int KMP_match(char *T, char *P) {
int *next = buildNext(P);//构造next表
int Tlen = (int)strlen(T), i = 0;
int Plen = (int)strlen(P), j = 0;
while ((j < Plen) && (i < Tlen)){
if (0 > j || T[i] == P[j]){
i++; j++;
}
else
j = next[j];
}
delete[] next;
if (j == Plen) return i - j;//成功
else return -1;//失败
}
KMP算法(优化)
原KMP算法不适用于多个相同字符的情况,例如aaaa,原KMP的next数组应该是[-1,0,1,2],优化后的next数组是[-1,-1,-1,2],既然第四个a都匹配不成功,第三个a去匹配肯定还是失败的,这就没必要重复去失败了。
算法思想:把原KMP的最长公共元素长度改为最优公共元素长度,去除重复字符
时间复杂度:O(n+m)
int *buildNext_plus(char *P) {
size_t len = strlen(P), i = 0;
int *next = new int[len];
int j = next[0] = -1;
while (i < len - 1)
if (j == -1 || P[i] == P[j]) {
i++;
j++;
next[i] = (next[i] != next[j]) ? j : next[j];
}
else
j = next[j];
return next;
}
int KMP_match(char *T, char *P) {
int *next = buildNext_plus(P);//构造next表
int Tlen = (int)strlen(T), i = 0;
int Plen = (int)strlen(P), j = 0;
while ((j < Plen) && (i < Tlen)){
if (0 > j || T[i] == P[j]){
i++; j++;
}
else
j = next[j];
}
delete[] next;
if (j == Plen) return i - j;//成功
else return -1;//失败
}
BM算法
Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore设计于1977年。
算法思想:用坏字符表(BS)记录每个字符在模式串中最后的位置,用好后缀表(GS)记录模式串中与后缀匹配的位置,当匹配失败时,取二者中最大的位移量移动,减少字符对比次数
时间复杂度:O(n/m)~O(n+m)
#include <stdlib.h>
int *buildBC(char *P) { //构造坏字符表Bad Charactor Shift:O(m + 256)
int *BC = new int[256]; //BC表,与字符表等长
for (size_t i = 0; i < 256; i++) BC[i] = -1; //初始化
for (size_t m = strlen(P), i = 0; i < m; i++) //从左往右,相同字符取最后的位置
BC[P[i]] = i;
return BC;
}
int *buildSS(char *P) { //构造最大匹配后缀长度表Suffix Size:O(m)
int m = strlen(P);
int *SS = new int[m];
SS[m - 1] = m; //对最后一个字符而言,与之匹配的最长后缀就是整个P串
//从倒数第二个字符起自右向左扫描P,依次计算出SS[]其余各项
for (int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j--) //lo是匹配子串的前端,hi是匹配子串的后端
if ((lo < j) && (SS[m - hi + j - 1] <= j - lo)) //子串除最后一位的前部,已经扫描过直接取值
SS[j] = SS[m - hi + j - 1];
else {
hi = j; lo = __min(lo, hi);
while ((0 <= lo) && (P[lo] == P[m - hi + lo - 1]))
lo--;
SS[j] = hi - lo;
}
return SS;
}
int *buildGS(char *P) { //构造好后缀位移量表Good Suffix shift:O(m)
int* SS = buildSS(P);
size_t m = strlen(P);
int* GS = new int[m];
for (size_t j = 0; j < m; j++) GS[j] = m; //初始化
for (size_t i = 0, j = m - 1; j < UINT_MAX; j--) //逆向逐一扫描各字符P[j]
if (j + 1 == SS[j]) //以j为末字符的子串恰好是前缀
while (i < m - j - 1)
GS[i++] = m - j - 1;
for (size_t j = 0; j < m - 1; j++)
GS[m - SS[j] - 1] = m - j - 1;
delete[] SS; return GS;
}
int BM_match(char *T, char *P) { //Boyer-Morre算法(完全版,兼顾Bad Character与Good Suffix)
int *BC = buildBC(P);
int *GS = buildGS(P);
size_t i = 0;
while (strlen(T) >= i + strlen(P)) {
int j = strlen(P) - 1; //自右向左比对
while (P[j] == T[i + j])
if (0 > --j) break;
if (0 > j) break; //完全匹配
else i += __max(GS[j], j - BC[T[i + j]]); //位移量根据BC表和GS表选择大者
}
delete[] GS; delete[] BC; //销毁GS表和BC表
return i;
}
Sunday算法
Sunday算法由Daniel M.Sunday在1990年提出。其实Sunday算法就是用BM算法的坏字符表的实现的,只是Sunday算法是从前往后匹配,而且Sunday算法关注的是当前模式串在目标串的最后的位置的下一个位置的字符。
Sunday算法获得比BM算法更大的跳跃距离,但是缺点很明显,就是很容易退化,比如在目标串baaabaaabaaaa寻找模式串aaaa,目标串最多只能跳两个位置,复杂度达到O(nm)。
算法思想:Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符,如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1
时间复杂度:O(n/m)~O(nm)
int Sunday_match(char *T, char *P)
{
int i, j, n = strlen(T), m = strlen(P);
int *S = new int[256];
for (i = 0; i < 256; i++) S[i] = m + 1; //初始化
for (i = 0; i < m; i++) //从左往右,相同字符取最后的位置
S[P[i]] = i;
for (i = 0, j = 0; j < m && (i - j) < (n - m);)
if (T[i] == P[j]){
i++;
j++;
}
else{
i += S[T[i + m]] - j;
j = 0;
}
if (j == m) return i - j;
else return -1;
}
总结
算法 | 效率 | 优点 | 缺点 | 适用环境 |
---|---|---|---|---|
BF | O(n*m) | 简单实现 | 效率低 | 仅简单字符串匹配,不要求效率的情况 |
RK | O(n)~O(n*m) | 串与串对比速度快 | 稳定性差、代码量大 | 模式串较长或文本串与模式串的差较小的情况 |
KMP | O(n+m) | 效率高、稳定性强 | 无明显短板 | 要求稳定性或文本串较短的情况 |
BM | O(n/m)~O(n+m) | 效率极高 | 前期准备工作量大 | 文本串较长的情况 |
Sunday | O(n/m)~O(n*m) | 代码简单且速度客观 | 稳定性差 | 模式串无大量重复的情况 |