引言

字符串的模式匹配是一种经常使用的操作。

模式匹配(pattern matching),简单讲就是在文本(text,或者说母串str)中寻找一给定的模式(pattern)。通常文本都非常大。而模式则比較短小。典型的样例如文本编辑和DNA分析。

在进行文本编辑时,文本一般是一段话或一篇文章,而模式则经常是一个单词。若是对某个指定单词进行替换操作,则要在整篇文章中进行匹配,效率要求肯定是非常高的。

模式匹配的朴素算法

最简单也最easy想到的是朴素匹配。何为朴素匹配,简单讲就是把模式串跟母串从左向右或从右向左一点一点比較:先把模式串的第一个字符同母串的第一个字符比較,若相等则接着比較后面的相应字符。若不等。把模式串后移一个位置,再次从模式串的头部比較……

这如同枚举法:把母串中与模式串同样长度的子串挨个比較。则这样的匹配方式显然无不论什么启示性或智能性。

例如以下图:

以上步骤非常easy看懂。它的代码也各种各样,以下是当中一种:

  1. /*
  2. 朴素的模式匹配算法,匹配方向:从前往后
  3. 匹配成功,则返回匹配成功时主串的下标位置(仅仅返回第一次匹配成功的位置)
  4. 否则,返回-1
  5. 母串或子串有一个为空也返回-1
  6. */
  7. int naiveStringMatching(const char* T, const char* P)
  8. {
  9. if (T && P)
  10. {
  11. int i, j, lenT, lenP;
  12. lenT = strlen(T);
  13. lenP = strlen(P);
  14. //模式串的长度比主串还长,显然无法匹配
  15. if (lenP > lenT)
  16. return -1;
  17. i = 0;
  18. while (i <= lenT-lenP)
  19. {
  20. j = 0;
  21. if (T[i] == P[j])
  22. {
  23. j++;
  24. //指针的写法是这种:while(j < lenP && *(T + i + j) == *P(j))j++;
  25. while (j < lenP && T[i + j] == P[j])
  26. j++;
  27. //顺利匹配到了模式串的结尾,则匹配成功
  28. if (j == lenP)
  29. return i;
  30. }
  31. i++;
  32. }
  33. //假设程序执行到这里,仍然没有结束,说明没有匹配上
  34. return -1;
  35. }
  36. return -1;
  37. }

考虑到有时须要使用c++中的string类型,这时它的代码是这种:

  1. int naiveStringMatching(const string T, const string P)
  2. {
  3. int i, j, lenT, lenP;
  4. lenT = T.length();
  5. lenP = P.length();
  6. //串空或模式串的长度比主串还长,显然无法匹配
  7. if (lenT == 0 || lenP == 0 || lenP > lenT)
  8. return -1;
  9. i = 0;
  10. while (i <= lenT - lenP)
  11. {
  12. j = 0;
  13. if (T[i] == P[j])
  14. {
  15. j++;
  16. while (j < lenP && T[i + j] == P[j])
  17. j++;
  18. if (j == lenP)
  19. return i;
  20. }
  21. i++;
  22. }
  23. return -1;
  24. }

不要小看上面的代码。尽管效率不高,但仍有掌握的必要。代码和算法总是在不断优化的,而这一切的优化都是从简单的情形開始的。

朴素匹配的时间复杂度是非常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数组的步骤

  1. 先求出以当前字符结尾的子串的最长相相应前缀和后缀的长度。
  2. 当前字符的next值,须要參考(參考就是取的意思)上一个字符的步骤1中的求解结果。

    至于第一个字符,因为没有“上一个字符”的说法。直接设置为-1。就可以。

看一个详细的样例:
模式串 "ababca"
最长前后缀长度表
模式串 a b a b c a
最长前后缀长度 0 0 1 2 0 1

由上表得出next数组
下标 0 1 2 3 4 5
next -1 0 0 1 2 0

直观地看就是:把“最长前后缀长度表”右移一个位置,于是最右边的一个长度被丢弃掉了,最左边空出的填上-1。这样得到的就是next数组。


next数组的递推求解


当模式串的长度非常小的时候。手动计算next数组也没什么问题。

可手动计算终究不是办法,必须机器计算。实际上next数组能够递推求解。这也是一个理解上的难点。

(i)初始 next[0]=-1;
(ii)若next[j]为k,即有 P0...Pk-1(Pk...)=Pj-k...Pj-1(Pj...)(*式)('='的意义:相应位置相匹配)。分两种情况。求解next[j+1]:
  1. if(Pk==Pj),则 next[j+1]=k+1=next[j]+1;
    道理显而易见:若Pk与Pj相等,则最长前后缀顺势增长一个,由*式能够看出。

  2. 若Pk与Pj不相等,则更新
    k=next[k];if(Pk==Pj)
    next[j+1]=k+1;否则。反复此过程。

    (这里也是个匹配问题)

我们用手动计算得到的next数组,来进行下測试:
给定一主串 "cadabababcacadda"。模式串 "ababca",next数组同上:next[]={-1,0,0,1,2,0}

代码

  1. #include<iostream>
  2. #include<iomanip>
  3. using namespace std;
  4. /*
  5. 依据模式串P,设置next数组的值
  6. */
  7. void setNext(const char* P, int* next)
  8. {
  9. int j, k, lenP;
  10. lenP = strlen(P);
  11. j = 1;
  12. next[0] = -1;
  13. while (j < lenP)
  14. {
  15. k = next[j-1];
  16. //P[j]!=P[k]
  17. while ((k >= 0) && P[j-1]!=P[k])
  18. k = next[k];
  19. if (k < 0)
  20. next[j] = 0;
  21. else
  22. next[j] = k + 1;
  23. j++;
  24. }
  25. }
  26. /*
  27. 串的模式匹配:KMP算法
  28. (i)T是主串,其形式是字符串常量或者是以'\0'结尾的字符串数组,如"abc"或{'a','b','c','\0'}
  29. (i)P是子串。其形式和主串一样
  30. (i)next数组
  31. (o)匹配成功,返回主串中第一次匹配成功的下标;否则,返回-1
  32. */
  33. int KMP(const char *T, const char *P, const int *next)
  34. {
  35. if (T && P)
  36. {
  37. //lenT是主串长度,lenP是子串长度
  38. int lenT, lenP;
  39. lenT = strlen(T), lenP = strlen(P);
  40. //主串长度小于子串,显然无法匹配
  41. if (lenT < lenP)
  42. return -1;
  43. int i, j, pos;
  44. i = j = -1;
  45. pos = lenT - lenP; //i最多仅仅需变化到pos位置。想想?非常easy的
  46. while (i <= pos && j < lenP)
  47. {
  48. //匹配成功或第一次匹配
  49. if (j == -1 || T[i] == P[j])
  50. {
  51. i++;
  52. j++;
  53. }
  54. else//匹配失败
  55. j = next[j];
  56. }
  57. if (j == lenP)
  58. return i - lenP; //这个返回值非常好理解
  59. else
  60. return -1;
  61. }
  62. return -1;
  63. }
  64. void print(int *array, int n)
  65. {
  66. if (array && n > 0)
  67. {
  68. int i;
  69. for (i = 0; i < n; i++)
  70. cout << setw(4) << array[i];
  71. cout << endl;
  72. }
  73. }
  74. int main()
  75. {
  76. cout << "***串的模式匹配:KMP算法***by David***" << endl;
  77. char T[] = "cadabababcacadda";
  78. char P[] = "ababca";
  79. cout << "主串" << endl;
  80. cout << T << endl;
  81. cout << "子串" << endl;
  82. cout << P << endl;
  83. int n = strlen(P);
  84. int *next=new int[n];
  85. setNext(P, next);
  86. cout << "打印next数组" << endl;
  87. print(next, n);
  88. cout << "使用KMP算法进行模式匹配" << endl;
  89. int index = KMP(T, P, next);
  90. if (index == -1)
  91. cout << "无法匹配!" << endl;
  92. else
  93. cout << "匹配成功。,匹配到的下标是 " << index << endl;
  94. delete[]next;
  95. system("pause");
  96. return 0;
  97. }

执行



 

代码下载:KMP算法

转载请注明出处。本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/35569257

若有所帮助,顶一个哦!

专栏文件夹:

串的匹配:朴素匹配&amp;KMP算法的更多相关文章

  1. 串的模式匹配 BF算法和KMP算法

    设有主串s和子串t,子串t的定位就是要在主串中找到一个与子串t相等的子串.通常把主串s称为目标串,把子串t称为模式串,因此定位也称为模式匹配. 模式匹配成功是指在目标串s中找到一个模式串t: 不成功则 ...

  2. javascript实现数据结构:串--定长顺序存储表示以及kmp算法实现

    串(string)(或字符串)是由零个或多个字符组成的有限序列.串中字符的数目称为串的长度.零个字符的串称为空串(null string),它的长度为零. 串中任意个连续的字符组成的子序列称为该串的子 ...

  3. 数据结构与算法JavaScript (五) 串(经典KMP算法)

    KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配 ...

  4. KMP算法详解 --从july那学的

    KMP代码: int KmpSearch(char* s, char* p) { ; ; int sLen = strlen(s); int pLen = strlen(p); while (i &l ...

  5. 字符串匹配算法KMP算法

    数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多 ...

  6. LeetCode刷题--基础知识篇--KMP算法

    KMP算法 关于字符串匹配的算法,最知名的莫过于KMP算法了,尽管我们日常搬砖几乎不可能去亲手实现一个KMP算法,但作为一种算法学习的锻炼也是很好的,所以记录一下. KMP算法是根据三位作者(D.E. ...

  7. 【数据结构与算法】字符串匹配(Rabin-Karp 算法和KMP 算法)

    Rabin-Karp 算法 概念 用于在 一个字符串 中查找 另外一个字符串 出现的位置. 与暴力法不同,基本原理就是比较字符串的 哈希码 ( HashCode ) , 快速的确定子字符串是否等于被查 ...

  8. 【面向打野编程】——KMP算法入门

    一.问题 咱们先不管什么KMP,来看看怎么匹配两个字符串. 问题:给定两个字符串,求第二个字符串是否包含于第一个字符串中. 为了具体化,我们以 ABCAXABCABCABX 与 ABCABCABX为例 ...

  9. KMP算法的一次理解

    1. 引言 在一个大的字符串中对一个小的子串进行定位称为字符串的模式匹配,这应该算是字符串中最重要的一个操作之一了.KMP本身不复杂,但网上绝大部分的文章把它讲混乱了.下面,咱们从暴力匹配算法讲起,随 ...

  10. 字符串查找KMP算法(转)

    如果你用过ctrl+F这个快捷键,那么你有很大的概率使用过这个算法,这就是在待查找字符串(可能有成千上万个字符)中找出模式串(比较小,可能有几个字符),可能找到大于或者等于1次的位置.例如,在abab ...

随机推荐

  1. Sqli-labs less 5

    Less-5 这里说一下,有很多的blog是翻译或者copy的,这关正确的思路是盲注.从源代码中可以看到,运行返回结果正确的时候只返回you are in....,不会返回数据库当中的信息了,所以我们 ...

  2. Python开发基础-Day1-python入门

    编程语言分类 机器语言 使用二进制代码直接编程,直接与硬件交互,执行速度非常快,灵活,但是开发难度高,开发效率低下,缺乏移植性. 汇编语言 对机器语言指令进行了英文封装,较机器语言容易记忆,直接与硬件 ...

  3. 某5道CF水题

    1.PolandBall and Hypothesis 题面在这里! 大意就是让你找一个m使得n*m+1是一个合数. 首先对于1和2可以特判,是1输出3,是2输出4. 然后对于其他所有的n,我们都可以 ...

  4. 【欧拉回路】【欧拉路径】【Fleury算法】CDOJ1634 记得小苹初见,两重心字罗衣

    Fleury算法看这里 http://hihocoder.com/problemset/problem/1181 把每个点看成边,每个横纵坐标看成一个点,得到一个无向图. 如果新图中每个点的度都是偶数 ...

  5. 【数论】【Polya定理】【枚举约数】【欧拉函数】【Java】poj2154 Color

    你随便写一下出来,发现polya原理的式子里面好多gcd是相同的,gcd(n,i)=k可以改写成gcd(n/k,i/k)=1,也就是说指数为k的项的个数为phi(n/k),就很好求了,最后除的那个n直 ...

  6. [NOIP2011]聪明的质检员

    [问题描述] 小 T 是一名质量监督员,最近负责检验一批矿产的质量.这批矿产共有$n$个矿石,从 1 到$n$逐一编号,每个矿石都有自己的重量$w_i$以及价值$v_i$.检验矿产的流程是: 1. 给 ...

  7. mysql交叉表查询解决方案整理

    交叉表是一种常用的分类汇总查询.使用交叉表查询,可以显示表中某个字段的汇总值,并将它们分组,其中一组列在数据表的左侧,另一组列在数据表的上部.行和列的交叉处可以对数据进行多种汇总计算,如:求和.平均值 ...

  8. 【JSTL】<c:if test=“”>没有else的解决方法

    后台封装了一个对象 放在model里: model.addAttribute("notice", notice); notice是个对象 然后前台如果没有公告的话,希望显示暂无公告 ...

  9. Oracle session active 和 inactive 状态 说明

    Oracle session active 和 inactive 状态 说明 原创 2011年06月12日 13:08:00 标签: session / oracle / database / ser ...

  10. zxing生成二维码和读取二维码

    当然,首先要导入zxing的jar包. 生成二维码代码: package com.imooc.zxing; import java.io.File; import java.nio.file.Path ...