串的匹配:朴素匹配&KMP算法
引言
字符串的模式匹配是一种经常使用的操作。
模式匹配(pattern matching),简单讲就是在文本(text,或者说母串str)中寻找一给定的模式(pattern)。通常文本都非常大。而模式则比較短小。典型的样例如文本编辑和DNA分析。
在进行文本编辑时,文本一般是一段话或一篇文章,而模式则经常是一个单词。若是对某个指定单词进行替换操作,则要在整篇文章中进行匹配,效率要求肯定是非常高的。
模式匹配的朴素算法
最简单也最easy想到的是朴素匹配。何为朴素匹配,简单讲就是把模式串跟母串从左向右或从右向左一点一点比較:先把模式串的第一个字符同母串的第一个字符比較,若相等则接着比較后面的相应字符。若不等。把模式串后移一个位置,再次从模式串的头部比較……
这如同枚举法:把母串中与模式串同样长度的子串挨个比較。则这样的匹配方式显然无不论什么启示性或智能性。
例如以下图:
以上步骤非常easy看懂。它的代码也各种各样,以下是当中一种:
/*
朴素的模式匹配算法,匹配方向:从前往后
匹配成功,则返回匹配成功时主串的下标位置(仅仅返回第一次匹配成功的位置)
否则,返回-1
母串或子串有一个为空也返回-1
*/
int naiveStringMatching(const char* T, const char* P)
{
if (T && P)
{
int i, j, lenT, lenP;
lenT = strlen(T);
lenP = strlen(P);
//模式串的长度比主串还长,显然无法匹配
if (lenP > lenT)
return -1;
i = 0;
while (i <= lenT-lenP)
{
j = 0;
if (T[i] == P[j])
{
j++;
//指针的写法是这种:while(j < lenP && *(T + i + j) == *P(j))j++;
while (j < lenP && T[i + j] == P[j])
j++;
//顺利匹配到了模式串的结尾,则匹配成功
if (j == lenP)
return i;
}
i++;
}
//假设程序执行到这里,仍然没有结束,说明没有匹配上
return -1;
}
return -1;
}
考虑到有时须要使用c++中的string类型,这时它的代码是这种:
int naiveStringMatching(const string T, const string P)
{
int i, j, lenT, lenP;
lenT = T.length();
lenP = P.length();
//串空或模式串的长度比主串还长,显然无法匹配
if (lenT == 0 || lenP == 0 || lenP > lenT)
return -1;
i = 0;
while (i <= lenT - lenP)
{
j = 0;
if (T[i] == P[j])
{
j++;
while (j < lenP && T[i + j] == P[j])
j++;
if (j == lenP)
return i;
}
i++;
}
return -1;
}
不要小看上面的代码。尽管效率不高,但仍有掌握的必要。代码和算法总是在不断优化的,而这一切的优化都是从简单的情形開始的。
朴素匹配的时间复杂度是非常easy分析的。若是成功匹配。最好的情况下,第一次比較就匹配上了,此时仅仅需strlen(P)次(模式串的长度次)比較。最坏的情况下,一直须要比較到母串最后一个长度与模式串同样的子串。共(strlen(T)-strlen(P)+1)*strlen(P)次比較。平均下是O(strlen(T)*strlen(P))。
KMP算法
KMP算法是一种用于字符串匹配的算法,这个算法的高效之处在于当在某个位置匹配不成功的时候能够依据之前的匹配结果从模式字符串的还有一个合适的位置開始,而不必每次都从头開始匹配。该算法由Knuth、Morris和Pratt三人设计,故取名为KMP。
KMP的改进之处
回想一下上文讲的的朴素匹配算法。每次失配的时候,都是 i++;j=0; 从画面上看,就是把模式串相对于主串向后移动一个位置。再次从模式串的首字符開始新的一轮比較。用数学的形式讲,这是 i++;j=0; 的几何意义。
失配时,记主串的下标为i,此时模式串的下标是j。
则能够肯定已有j个字符成功匹配。例如以下图:
Ti-j Ti-j+1 ...Ti-2 Ti-1 Ti
P0 P1
...Pj-2 Pj-1 Pj
图(a)
在上图中,红色的字符是匹配上的。下标 0,1..j-1 不正好是j个吗?
KMP的做法是,充分利用已匹配的信息,失匹配时:i不变,j=next[j],当中next[j]<=j-1。即失配的时候,又一次用模式串的next[j]位置的字符和主串的i位置进行匹配。
故模式串相对主串移动的位置大小是j-next[j]>=1,在普通情况下都比朴素匹配的移动一个位置高效。这就是KMP的改进之处。
之所以敢把主串T[i]与模式串P[next[j]]直接匹配。也就是说跳过模式串的前next[j]个字符,那么以下的事实必须存在:
Ti-next[j]
Ti-next[j]+1 ...Ti-2
Ti-1 Ti
P0 P1
...Pnext[j]-2 Pnext[j]-1 Pnext[j]
图(b)
这个事实就是:模式串下标从0到next[j]-1位置的字符已经匹配成功了。
两个论断
(1)从图(a)中要明白一点:Ti-j...Ti-1和P0...Pj-1是一模一样的。所以把前者看成后者是没有问题的。
(2)从图(b)中能够得到,P0...Pnext[j]-1 是 P0...Pj-1 的前
next[j] 个连续字符子串,而 Ti-next[j]...Ti-1 也就是 Pj-next[j]...Pj-1(依据上一条结论)是 P0...Pj-1 的后next[j]个连续字符子串。而且它们是一模一样的(相应匹配)。
前缀、后缀
这就引出了前缀、后缀的概念。举一个样例就可以说明:
字符串 "abc"
它有前缀 "a" "ab" "abc"
它有后缀 "c" "bc" "abc"
看到这里,不用过多的解释,大家也可明确前后缀指的是什么呢。
next[j]的含义
结合前后缀的概念,可得出next[j]的的实际含义:next[j]就是模式串下标 0...j-1 这j个字符的最长相相应前缀和后缀的长度。
非常显然。相相应是指能够匹配得上。至于为什么是最长?,基于两点考虑:
(i)直观上看,next[j]最大。说明剩下须要进行匹配验证的字符就最少嘛。
(ii)本质上,仅仅能取最大,否则会遗漏可能的匹配。(这一点,须要细致想想!
)
须要指出。这里我们仅仅能考虑非平庸的前后缀。否则,对于平庸的无意义。
(平庸前后缀是指:空串和串本身。
其他的都是非平庸的。
)
另一点我们得明确:next数组全然由模式串本身确定。与主串无关。
求解next数组的步骤
- 先求出以当前字符结尾的子串的最长相相应前缀和后缀的长度。
- 当前字符的next值,须要參考(參考就是取的意思)上一个字符的步骤1中的求解结果。
至于第一个字符,因为没有“上一个字符”的说法。直接设置为-1。就可以。
模式串 | a | b | a | b | c | a |
最长前后缀长度 | 0 | 0 | 1 | 2 | 0 | 1 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
next | -1 | 0 | 0 | 1 | 2 | 0 |
next数组的递推求解
可手动计算终究不是办法,必须机器计算。实际上next数组能够递推求解。这也是一个理解上的难点。
- if(Pk==Pj),则 next[j+1]=k+1=next[j]+1;
道理显而易见:若Pk与Pj相等,则最长前后缀顺势增长一个,由*式能够看出。 - 若Pk与Pj不相等,则更新
k=next[k];if(Pk==Pj)
next[j+1]=k+1;否则。反复此过程。(这里也是个匹配问题)
代码
#include<iostream>
#include<iomanip>
using namespace std;
/*
依据模式串P,设置next数组的值
*/
void setNext(const char* P, int* next)
{
int j, k, lenP;
lenP = strlen(P);
j = 1;
next[0] = -1;
while (j < lenP)
{
k = next[j-1];
//P[j]!=P[k]
while ((k >= 0) && P[j-1]!=P[k])
k = next[k];
if (k < 0)
next[j] = 0;
else
next[j] = k + 1;
j++;
}
}
/*
串的模式匹配:KMP算法
(i)T是主串,其形式是字符串常量或者是以'\0'结尾的字符串数组,如"abc"或{'a','b','c','\0'}
(i)P是子串。其形式和主串一样
(i)next数组
(o)匹配成功,返回主串中第一次匹配成功的下标;否则,返回-1
*/
int KMP(const char *T, const char *P, const int *next)
{
if (T && P)
{
//lenT是主串长度,lenP是子串长度
int lenT, lenP;
lenT = strlen(T), lenP = strlen(P);
//主串长度小于子串,显然无法匹配
if (lenT < lenP)
return -1;
int i, j, pos;
i = j = -1;
pos = lenT - lenP; //i最多仅仅需变化到pos位置。想想?非常easy的
while (i <= pos && j < lenP)
{
//匹配成功或第一次匹配
if (j == -1 || T[i] == P[j])
{
i++;
j++;
}
else//匹配失败
j = next[j];
}
if (j == lenP)
return i - lenP; //这个返回值非常好理解
else
return -1;
}
return -1;
}
void print(int *array, int n)
{
if (array && n > 0)
{
int i;
for (i = 0; i < n; i++)
cout << setw(4) << array[i];
cout << endl;
}
}
int main()
{
cout << "***串的模式匹配:KMP算法***by David***" << endl;
char T[] = "cadabababcacadda";
char P[] = "ababca";
cout << "主串" << endl;
cout << T << endl;
cout << "子串" << endl;
cout << P << endl;
int n = strlen(P);
int *next=new int[n];
setNext(P, next);
cout << "打印next数组" << endl;
print(next, n);
cout << "使用KMP算法进行模式匹配" << endl;
int index = KMP(T, P, next);
if (index == -1)
cout << "无法匹配!" << endl;
else
cout << "匹配成功。,匹配到的下标是 " << index << endl;
delete[]next;
system("pause");
return 0;
}
执行
代码下载:KMP算法
转载请注明出处。本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/35569257
若有所帮助,顶一个哦!
专栏文件夹:
串的匹配:朴素匹配&KMP算法的更多相关文章
- 串的模式匹配 BF算法和KMP算法
设有主串s和子串t,子串t的定位就是要在主串中找到一个与子串t相等的子串.通常把主串s称为目标串,把子串t称为模式串,因此定位也称为模式匹配. 模式匹配成功是指在目标串s中找到一个模式串t: 不成功则 ...
- javascript实现数据结构:串--定长顺序存储表示以及kmp算法实现
串(string)(或字符串)是由零个或多个字符组成的有限序列.串中字符的数目称为串的长度.零个字符的串称为空串(null string),它的长度为零. 串中任意个连续的字符组成的子序列称为该串的子 ...
- 数据结构与算法JavaScript (五) 串(经典KMP算法)
KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配 ...
- KMP算法详解 --从july那学的
KMP代码: int KmpSearch(char* s, char* p) { ; ; int sLen = strlen(s); int pLen = strlen(p); while (i &l ...
- 字符串匹配算法KMP算法
数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多 ...
- LeetCode刷题--基础知识篇--KMP算法
KMP算法 关于字符串匹配的算法,最知名的莫过于KMP算法了,尽管我们日常搬砖几乎不可能去亲手实现一个KMP算法,但作为一种算法学习的锻炼也是很好的,所以记录一下. KMP算法是根据三位作者(D.E. ...
- 【数据结构与算法】字符串匹配(Rabin-Karp 算法和KMP 算法)
Rabin-Karp 算法 概念 用于在 一个字符串 中查找 另外一个字符串 出现的位置. 与暴力法不同,基本原理就是比较字符串的 哈希码 ( HashCode ) , 快速的确定子字符串是否等于被查 ...
- 【面向打野编程】——KMP算法入门
一.问题 咱们先不管什么KMP,来看看怎么匹配两个字符串. 问题:给定两个字符串,求第二个字符串是否包含于第一个字符串中. 为了具体化,我们以 ABCAXABCABCABX 与 ABCABCABX为例 ...
- KMP算法的一次理解
1. 引言 在一个大的字符串中对一个小的子串进行定位称为字符串的模式匹配,这应该算是字符串中最重要的一个操作之一了.KMP本身不复杂,但网上绝大部分的文章把它讲混乱了.下面,咱们从暴力匹配算法讲起,随 ...
- 字符串查找KMP算法(转)
如果你用过ctrl+F这个快捷键,那么你有很大的概率使用过这个算法,这就是在待查找字符串(可能有成千上万个字符)中找出模式串(比较小,可能有几个字符),可能找到大于或者等于1次的位置.例如,在abab ...
随机推荐
- Codeforces Round #404 (Div. 2) C 二分查找
Codeforces Round #404 (Div. 2) 题意:对于 n and m (1 ≤ n, m ≤ 10^18) 找到 1) [n<= m] cout<<n; 2) ...
- Extjs MVC模式
最近在学习Extjs,发现他可以使用MVC模式,不但可以组织代码,而且可以 减少实现的内容,模型(Models)和控制器(Controllers)也被引入其中. Model模型是字段和它们的数据的集合 ...
- 【BZOJ 4650】【UOJ #219】【NOI 2016】优秀的拆分
http://www.lydsy.com/JudgeOnline/problem.php?id=4650 http://uoj.ac/problem/219 这里有非常好的题解qwq 接着道题复习一下 ...
- JZYZOJ1376 [coci2011]友好数对 容斥定理 状态压缩
http://172.20.6.3/Problem_Show.asp?id=1376 题意:找给出的数中含有相同数字的数对的对数. mmp数论题竟然卡快读,莫名拉低通过率什么的太过分了. 刚开始想到了 ...
- 【矩阵乘法】OpenJ_POJ - C17F - A Simple Math Problem
算(7+4*sqrt(3))^n的整数部分(mod 1e9+7). 容易想到矩乘快速幂,但是怎么算整数部分呢? (7+4*sqrt(3))^n一定可以写成a+b*sqrt(3),同理(7-4*sqrt ...
- 【bitset】【推导】hdu5961 传递
<法一>http://blog.csdn.net/u014325920/article/details/53046890 1.判断传递的条件为:若G中有 一条边从a到b且有一条边从b到c ...
- 关于网络流sap算法
今天终于学习了网络流..之前一直很怕这类问题,个人觉得网络流算是图论里面最难的了.... sap学习下来感觉一般,关于解法都是意识流,细节也是蛮多的.. 我这里先贴一份模版,自已也加了点注释(只是个人 ...
- Mobiscroll手机触屏日期选择器
最近在制作jquery mobile因要用到日历控件,突然发现Mobiscroll非常不错.于是摘下来记录. A Mobiscroll是一个用于触摸设备(Android phones.iPhon ...
- oracle client PLSQL配置
date:20140525auth:Jin platform :windows 一.服务端启动服务和创建账号# su - oracle$ lsnrctl start$ sqlplus / as sys ...
- IIS配置Asp.net时,出现“未能加载文件或程序集“System.Web.Extensions.Design, Version=1.0.61025.0”
如果出现未能加载文件或程序集“System.Web.Extensions.Design, Version=1.0.61025.0, 主要是没有安装.net framwork 3.5,安装一下就行了. ...