给出问题:在字符串(主串)中查找子串(模式串),如给定长度为m字符串A =“aababcaabaacdcbab”,长度为n的模式串B=“aabaacd”,给出B在A中的位置。
暴力解法的时间复杂度为O(n*m),而KMP算法的时间复杂度为O(n+m)
一、基本概念
子序列:字符串中任意个相对位置不变的字符组成的字符串,如“ab”,“ac”,“a”均为“abc”的子序列。
子串:字符串中任意连续个字符组成的子序列,如“ab”,“bc”,“abc”,“”均为“abc”的子串。
字符串前缀:包含第一个字符,但不包含最后一个字符的子串,如“a”,“ab”,“abc”均为“abcd”的前缀。
字符串后缀:不包含第一个字符,但包含最后一个字符的子串,如“d”,“cd”,“bcd”均为“abcd”的后缀。
部分匹配值:字符串前缀集合和字符串后缀集合中最长公共元素的长度。如字符串“abab”的前缀集合为{“a”,“ab”,“aba”},后缀集合为{“b”,“ab”,“bab”},公共元素仅有“ab”,因此其为最长公共元素,长度为2。
二、逻辑思路
从主串起始位置的字符(i=0,S[i]='a')开始与模式串起始位置字符比较(j=0,P[j]='a'),相等则继续比较主串和模式串中的下一字符,直到完全匹配或某一位置不匹配为止。当不匹配时,如主串i=4,S[i]='b'与模式串j=4,P[j]='a'不匹配,则已匹配字符串为“aaba”,其部分匹配值为1,即字符串“aaba”从起始位置数起的前1个字符和从末尾位置数起的前1个字符相同。那么i指向的位置不需回退,只需将j指向位置的序号改为部分匹配值1即可(若部分匹配值为-1,则i++),然后重新开始匹配。
next数组的大小与模式串相同,其next[j]的含义是P[j]之前的字符串P[0..j-1]的部分匹配值。
根据定义求得next数组,如表
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
模式串 | a | a | b | a | a | c | d |
当前位置的最长前缀 | 空 | a | aa | aab | aaba | aabaa | aabaac |
最长前缀的部分匹配值 | 0 | 0 | 1 | 0 | 1 | 2 | 0 |
next数组值 | -1 | 0 | 1 | 0 | 1 | 2 | 0 |
例:
S[4]!=P[4],则j=next[j]=next[4]=1,即
S[4]!=P[1],则j=next[j]=next[1]=0,即
S[4]!=P[0],且因为next[0]=-1,则i=i+1,即
不断重复上面的过程,直到完全匹配或i超过主串长度。
三、求解next数组
(1)j=0,1时,根据定义任意模式串的next值是相同的,均为-1和0。
(2)j=2时,next值=”aa”的部分匹配值
而”aa”的部分匹配值的求解可转换为求解pattern[next[j-1]]= pattern[j-1]是否相等。
next[j-1] = 0,表示序号为j-1的字符”a”的部分匹配值为0,即 黄色部分的字符串相同,且长度为0。
pattern[j-1] = ‘a’ 表示序号为2的字符的前一个字符。
pattern[next[j-1]] = ‘a’表示第一个黄色字符串后紧挨着的一个字符。
因此,pattern[next[j-1]]== pattern[j-1]?即 两括号部分的字符串是否相等?
若两部分相等,则next[j] = next[j-1]+1
若两部分不相等,则分两种情况讨论:
- 黄色部分字符串长度next[j-1]==0,那么next[j]=0。即黄色部分字符串不存在,而pattern[j-1]与pattern[next[j-1]] 对应的字符又不相等,则当前的next值为0。
- 黄色部分字符串长度next[j-1]!=0,那么问题就进一步转换为求解pattern[next[next[j-1]]]= pattern[j-1]是否相等。此种情况见j=3。
这里,两部分相等,因此next[j] = next[j-1]+1 = 1
(3)j=3时,next值=”aab”的部分匹配值
同理,转换为求解pattern[next[j-1]]= pattern[j-1]是否相等。
next[j-1] = 1
pattern[j-1] = ‘b’
pattern[next[j-1]] = ‘a’
因此,pattern[next[j-1]]== pattern[j-1]?即两括号部分的字符相等?(黄色部分为字符串"aa"的部分匹配值对应的相同字符串)
这里不相等,因此进一步转换为求解pattern[next[next[j-1]]]= pattern[j-1]是否相等?即两括号部分的字符串是否相同?
蓝色区域表示黄色区域字符的部分匹配值的长度,因为两黄色区域的字符串相同,因此两蓝色区域的字符串也相同。
这里两括号部分的字符串不同,且next[next[j-1]]=0.,因此next[j] = 0。
(4)同理可得j=4、5、6下的next值1、2和0。