给定两个字符串S和T,在主串S中查找子串T的过程称为串匹配(string
matching,也称模式匹配),T称为模式。这里将介绍处理串匹配问题的两种算法,BF算法和KMP算法。
BF算法
(暴力匹配算法)
BF算法简介
F(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。
BF算法原理
首先,给定 “主串” 和 “模式串” 如下:
BF算法使用简单粗暴的方式,对主串和模式串进行逐个字符的比较:
检测到字符不匹配,模式串向后挪动一位,和主串的第二个等长子串比较。
检测到字符不匹配,模式串继续向后挪动一位,和主串的第三个等长子串比较
···········不断循环上面步骤,直到匹配成功或者匹配失败(即到主串末尾结束)。
算法
int BF(char* S, char* T)
{
int sLen = strlen(S); //文本串长度
int tLen = strlen(T); //模式串长度
int i = 0;
int j = 0;
while (i < sLen && j < tLen)
{
if (S[i] == T[j])
{
//①如果当前字符匹配成功(即S[i] == T[j]),则i++,j++
i++;
j++;
}
else
{
//②如果失配(即S[i]! = T[j]),令i = i - (j - 1),j = 0
i = i - j + 1;
j = 0;
}
}
//匹配成功,返回模式串t在文本串s中的位置,否则返回-1
if (j == tLen)
return i - j;
else
return -1;
}
测试
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
int BF(char* S, char* T)
{
int sLen = strlen(S); //文本串长度
int tLen = strlen(T); //模式串长度
int i = 0;
int j = 0;
while (i < sLen && j < tLen)
{
if (S[i] == T[j])
{
//①如果当前字符匹配成功(即S[i] == T[j]),则i++,j++
i++;
j++;
}
else
{
//②如果失配(即S[i]! = T[j]),令i = i - (j - 1),j = 0
i = i - j + 1;
j = 0;
}
}
//匹配成功,返回模式串t在文本串s中的位置,否则返回-1
if (j == tLen)
return i - j;
else
return -1;
}
int main()
{
char S[] = "ABC ABCDAB ABCDABCDABDW";
char T[] = "ABCDABD";
int num;
char *ch = T;
num = BF(S,T);
if (num >= 0)
{
printf("matched @: %s\n", ch);
}
else
{
printf("matched fail!\n");
}
return 0;
}
执行结果:
matched @: ABCDABD
总结:BF算法比较直接,是一种蛮力法,该算法最坏情况下要进行m*(n-m+1)次比较,时间复杂度为O(m*n),下面来看一个效率非常高的字符串匹配算法,即KMP算法。
KMP算法
KMP算法简介
之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
简单理解就是:KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。
KMP算法原理
(参考自:看,未来的博客)
首先,给定 “主串” 和 “模式串” 如下:
第一轮,模式串和主串的第一个等长子串比较,发现前5个字符都是匹配的,第6个字符不匹配,是一个“坏字符”:
这时候,我们可以有效利用已匹配的前缀 “GTGTG” 。通过观察我们可以发现,在前缀“GTGTG”当中,后三个字符“GTG”和前三位字符“GTG”是相同的:
在下一轮的比较时,只有把这两个相同的片段对齐,才有可能出现匹配。这两个字符串片段,分别叫做最长可匹配后缀子串和最长可匹配前缀子串。
第二轮,我们直接把模式串向后移动两位,让两个“GTG”对齐,继续从刚才主串的坏字符A开始进行比较:
显然,主串的字符A仍然是坏字符,这时候的匹配前缀缩短成了GTG:
按照第一轮的思路,我们来重新确定最长可匹配后缀子串和最长可匹配前缀子串:
第三轮,我们再次把模式串向后移动两位,让两个“G”对齐,继续从刚才主串的坏字符A开始进行比较:
·········接下来还是重复步骤,就不往写了。
next数组
什么是next数组?next数组用来干什么?
next数组是决定kmp算法快速移动的核心。
next数组的生成示例:
说白了就是把子串拆分,然后看尾部有多少个字符重复,没有写-1,有一个就写0,有两个写1,依次类推(你也可以写0,1,2,具体想写啥看个人吧,我这里只是全部减1了,当然在代码里也要记得修改)。
有了next数组,我们就可以通过已匹配前缀的下一个位置(坏字符位置),快速寻找到最长可匹配前缀的下一个位置,然后把这两个位置对齐。
算法
void compute_prefix(const char *pattern, int next[])
{
int i; //前缀
int j = -1; //前缀尾巴
const int m = strlen(pattern);
next[0] = j;
for (i = 1; i < m; i++)
{
while (j > -1 && pattern[j + 1] != pattern[i]) //前后缀不相等
{
j = next[j];
}
if (pattern[i] == pattern[j + 1]) //前后缀相等
{
j++;
}
next[i] = j;
}
}
int kmp(const char *text, const char *pattern)
{
int i;
int j = -1;
const int n = strlen(text);
const int m = strlen(pattern);
if (n == 0 && m == 0) return 0;
if (m == 0) return 0;
int *next = (int*)malloc(sizeof(int) * m);
compute_prefix(pattern, next);
for (i = 0; i < n; i++)
{
while (j > -1 && pattern[j + 1] != text[i]) j = next[j];
if (text[i] == pattern[j + 1]) j++;
if (j == m - 1)
{
free(next);
return i-j;
}
}
free(next);
return -1;
}
测试
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
void compute_prefix(const char *pattern, int next[])
{
int i; //前缀
int j = -1; //前缀尾巴
const int m = strlen(pattern);
next[0] = j;
for (i = 1; i < m; i++)
{
while (j > -1 && pattern[j + 1] != pattern[i]) //前后缀不相等
{
j = next[j];
}
if (pattern[i] == pattern[j + 1]) //前后缀相等
{
j++;
}
next[i] = j;
}
}
int kmp(const char *text, const char *pattern)
{
int i;
int j = -1;
const int n = strlen(text);
const int m = strlen(pattern);
if (n == 0 && m == 0) return 0;
if (m == 0) return 0;
int *next = (int*)malloc(sizeof(int) * m);
compute_prefix(pattern, next);
for (i = 0; i < n; i++)
{
while (j > -1 && pattern[j + 1] != text[i]) j = next[j];
if (text[i] == pattern[j + 1]) j++;
if (j == m - 1)
{
free(next);
return i-j;
}
}
free(next);
return -1;
}
int main()
{
char text[] = "ABC ABCDAB ABCDABCDABDE";
char pattern[] = "ABCDABD";
char *ch = pattern;
int i = kmp(text, pattern);
if (i >= 0)
{
printf("matched @: %s\n", ch);
}
else
{
printf("matched fail!\n");
}
return 0;
}
执行结果:
matched @: ABCDABD