算法学习----Python数据结构--kmp字符串
来自acwing
核心优化思想在于通过一个lps数组来反映部分重复匹配的位置信息,能够从头前进好几位达到快捷搜索,减少传统暴力从头只前进一位遍历带来的冗余。难点主要在于lps数组的构建思路
当我们要在ababa中找aba时
当下标遍历到2,找到第一个匹配点,再寻找第二个匹配点时暴力方法是将母串的下标进一位,也就是在babca中再遍历查找aba,此时发现第一个字母就不匹配,再将母串下标进一位。这只是一个便于理解的情况,倘若数据量巨大,这种遍历的时间复杂度是相当高的。
对此,我需要在第一次匹配成功,直接定位到母串的第二个a,也就是穿越到下标为2进行开头遍历,对比暴力方法这一方式无疑是更快得多的
对此我需要记录这些完全相同的类似于‘a’这种重复出现的部分
也就来到了构建lps数组的环节,它的出现是为了记录子串中每个位置的最长公共前后缀长度
对于aba,我们发现第一个a和自己能匹配但是作为开头无意义,对于b,因为a!=b,匹配不到,对于第三个a,能够和开头a匹配,我们记录为1,同时引入长度概念,对于子串的这种单个字符的匹配,长度length记录为1,此时我们的lps数组为[0,0,1]
当我们通过这个数组和长度信息再进行遍历时,如何将子串的开头定位到母串的2下标位置呢,而且这里存在一个问题就是我们已经找到第一个能够匹配的位置了,我们下一次匹配又要怎么设置呢
当我们找到其中一个字串位置时,母串下标j是需要返回的,我们如何将j直接移动到a第二次出现的位置呢。
事实上kmp并不是这样的返回思路,而是通过lps了解到子串的前后缀的相同性,不通过重新遍历母串下标而是将子串下标回退进行继续检索。
具体分析 "ababa" 中查找 "aba"
让我们仔细分析这个例子:
第一次匹配完成后:
母串指针 i = 3 (指向第4个字符 'b')
子串指针 j = 3 (表示已完全匹配)
记录匹配位置 0
KMP的智能移动:
KMP通过LPS数组知道子串"aba"的前缀和后缀有重叠:
前缀"a"和后缀"a"相同
LPS[2] = 1 表示最长公共前后缀长度为1
所以算法将子串向右移动 3 - 1 = 2 位:(也就是将子串从b开始与母串进行比对)
文本: a b a b a 模式: a b a
KMP算法的移动量是由LPS数组决定的:
移动量 = 已匹配长度 - LPS[已匹配长度-1]
这确保了不会错过任何可能的匹配
在"aba"的例子中:
已匹配长度 = 3
LPS[2] = 1
移动量 = 3 - 1 = 2
KMP算法的精妙之处就在于它利用模式串本身的信息(LPS数组)来做出最优的移动决策,既保证了正确性(不会错过任何匹配),又保证了效率(移动尽可能远但不会太远)。
def lpsestablish(parttern):m=len(parttern)lps=[0]*mlength=0i=1while i<m:if parttern[i]==parttern[length]:length+=1lps[i]=lengthi+=1else:if length!=0:length=lps[length-1]else:lps[i]=0i+=1return lpsdef kmp_search(text,parttern):n=len(text)m=len(parttern)if m==0:return []if n==0 or m>n:return []lps=lpsestablish(parttern)matches=[]i=0j=0while i<n:if parttern[j]==text[i]:i+=1j+=1if j==m:matches.append(i-j)j=lps[j-1]else:if j!=0:j=lps[j-1]else:i+=1return matchesdef main():import sysdata=sys.stdin.read().splitlines()if not data:return n=int(data[0].strip())parttern=data[1].strip()m=int(data[2].strip())text=data[3].strip()matches=kmp_search(text,parttern)print(' '.join(map(str,matches)))if __name__=='__main__':main()