算法笔记(2):KMP 算法

介绍

KMP算法(全称Knuth-Morris-Pratt字符串查找算法,由三位发明者的姓氏命名)是可以在文本串 SO(|S|+|P|) 查找模式串 P 的一种算法。

思想

考虑 O(|S||P|) 的暴力匹配算法,对于文本串 S每一位开始与模式串 P 逐一匹配
缺点在于每次都要重新开始逐一匹配,不能充分利用前面匹配得到的信息,导致算法效率低下

那么我们需要考虑的问题是 匹配失败后从哪一位开始重新匹配最好?

首先,我们需要引入一些概念

前缀函数

子串S[i..j] (ij),即 S[i],S[i+1],,S[j]
前缀Prefix(S,k)=S[0..k]
真前缀Prefix(S,k)=S[0..k] (k|S|1) 【不包含 S 本身】
后缀Suffix(S,k)=S[k..|S|1]
真后缀Suffix(S,k)=S[k..|S|1] (k0) 【不包含 S 本身】
公共前后缀Prefix(S,k)=Suffix(S,|S|k1)
真公共前后缀Prefix(S,k)=Suffix(S,|S|k1) (k|S|1) 【不包含 S 本身】

定义 前缀函数 π[i]S[0..i]最长真公共前后缀的长度
π[i]=maxk=0i1{k:S[0..k]=S[ik..i]}

怎么维护?

  1. 暴力:枚举前缀子串 O(n),枚举该串的前后缀 O(n),比较 O(n),总 O(n3)

  2. 注意到 S[0..i] 的真公共前后缀 移除最后一位后的串S[0..i1] 的真公共前后缀,我们可以根据这个构造递推方程

    对于 k=0i2
    如果 S[k+1]==S[i]π[i]=max(π[i],{k:S[0..k]=S[i1k..i1]}+1)
    否则 π[i]=max(π[i],0)

    因此我们只需要从大到小枚举 S[0..i1] 的所有真公共前后缀进行匹配就可以得出 S[0..i] 的最长真公共前后缀
    那么如何从大到小枚举 S[0..i1] 的所有真公共前后缀?
    S0S1kS2S3π[i1]Si4Si3Si2Si1kπ[i1]Si
    假设下一个真公共前后缀 S[0..k]=S[i1k..i1] (k<π[i]1)
    因为 S[0..π[i1]1]=S[iπ[i1]..i1]
    S[i1k..i1]=S[π[i1]1k..π[i1]1]
    那么 S[0..k]=S[π[i1]1k..π[i1]1] ①②


    假设 S0S1 = Si2Si1
    因为 S0S1S2S3 = Si4Si3Si2Si1
    Si2Si1 = S2S3
    那么 S0S1 = S2S3 ①②

    由于 S[0..k]=S[π[i1]1k..π[i1]1] 且 这是下一个真公共前后缀
    π[π[i1]1]=kπ
    那么找下标为 i1下一个真公共前后缀 找下标为 π[i1]1最长公共前后缀

    综上,从大到小枚举 S[0..k] 的所有真公共前后缀只需要不断地将 kπ[k]1π[π[k]1]1..1 不断循环直到成功匹配或者枚举到-1即可

    因此,求 S[i] ,令 k=i1,按照上面的枚举方式不断循环即可
    如果成功匹配,那么 π[i]=π[k]+1
    否则 π[i]=0

    代码

    点击查看代码
    vector<int> getSuffixFunction(string S){//0-based
    	//"aabaaab"
    	int n = S.size();
    	vector<int> pi(n);
    	for(int i = 0; i < n; i ++ ){	//递推求pi
    		int k = i-1;
    		while(k != -1 && S[pi[k]] != S[i])k = pi[k]-1;	//从大到小枚举S[0..k]的所有真公共前后缀
    		//k == -1:说明枚举过程结束
    		//S[pi[k]] == S[i]:说明匹配成功
    		if(k == -1 || S[pi[k]] != S[i])pi[i] = 0;	//匹配失败
    		else pi[i] = pi[k] + 1;		//匹配成功
    	}
    	return pi;
    	//[0,1,0,1,2,2,3]
    }
    

理解前缀函数的递推后,字符串匹配就简单了
将模式串 P 和 文本串 S 按照 P+?+S 【?为 PS 中均没有出现的字符】的形式拼接求前缀函数即可
模式串在文本串中的条件为 π[i]=|P|,首位为 i2|P|


模式串 P = "ABA"
文本串 S = "ABABA"
新串:ABA?ABABA
下标:0 1 2 3 4 5 6 7 8
π    0 0 1 0 1 2 3 2 3

代码

点击查看代码
	vector<int> KMP(string S,string P){//0-based
		S = P + "?" + S;
		int n = S.size();
		vector<int> pi(n);
		vector<int>res;
		for(int i = 0; i < n; i ++ ){
			int k = i-1;
			while(k != -1 && S[pi[k]] != S[i])k = pi[k]-1;
			if(k == -1 || S[pi[k]] != S[i])pi[i] = 0;
			else pi[i] = pi[k] + 1;
			if(pi[i] == P.size())res.push_back(i - 2*P.size());
			//返回首位
		}
		return res;
	}
posted @   Keith-  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示