05.一文详解KMP算法(一)

  • 空格串:是只包含空格的串,空格串有内容和长度,而且可以不止一个空格。

  • 空串:零个字符的串。

  • 子串:串中任意个数的连续字符组成的子序列。

  • 串的比较:

    • 取决于它们挨个字母的前后顺序
    • 通过组成串的字符之间的编码来进行(字符的编码指的是字符在对应字符集中的序号)
    • ASCII编码:8位二进制表示一个字符,总共可表示256个字符。( 2 8 2^8
    • Unicode编码:16位二进制表示一个字符,总共6.5万个字符( 2 16 2^{16}
    • 线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素。
    • 串中更多的是查找子串位置、得到指定位置子串、替换子串等操作。

5.1朴素的模式匹配算法

子串的定位操作通常称做串的模式匹配

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?

如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:

  • 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
  • 如果失配(即S[i]! = P[j]),令i = i - (j - 1)+1,j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
  • 最坏时间复杂度为 O ( ( n m + 1 ) m ) O((n-m+1)*m)
//假设文本串S和模式串P的长度存在S[0]与P[0]中
//下标从[1]开始,这点要注意

int Index(char* s, char* p, int pos)
{
 
	int i = pos; //i用于文本串S中当前位置的下标
	int j = 1; //j用于模式串P中当前位置下标值
	while (i <= s[0] && j <= p[0]) //若i小于s长度且j小于p的长度时,循环
	{
		if (s[i] == p[j])
		{
			//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
			i++;
			j++;
		}
		else //指针后退,重新开始匹配
		{
			//②如果失配(即S[i]! = P[j]),令i = i - (j - 1)+1,j = 0    
			i = i - j + 2; //i退回到上次匹配首位的下一位
			j = 1; //j退回到子串T的首位
		}
	}
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
	if (j > p[0]) //遍历完模式串,匹配
		return i - p[0];
	else
		return -1;
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

5.2KMP算法

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

  • 如果j = 0,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
  • 如果j != 0,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
    • 换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
  • 让i值不回溯,j值回溯多少取决于模式串p当前字符之前的串的前后缀的相似度
int KmpSearch(char* s, char* p, int pos)
{
	int i = pos; 
	int j = 1;
    int next[255]
    get_next(p, next) //对p进行分析,得到next数组

	while (i <= s[0] && j <= p[0])
	{
		//①如果j = 0,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    
		if (j == 0 || s[i] == p[j])
		{
			i++;
			j++;
		}
		else //指针后退重新开始匹配
		{
			//②如果j != 0,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
			//next[j]即为j所对应的next值      
			
            j = next[j]; //j退回到合适的位置,i值不变
		}
	}
	if (j > p[0])
		return i - p[0];
	else
		return -1;
}

在这里插入图片描述

5.3求next数组

  • 字符串本身不是自己的前缀、后缀
  • PMT前缀集合、后缀集合的交集中最长元素的长度
  • 下标从1开始的next值:如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1
j 1 2 3 4 5 6 7 8 9
模式串p a b a b a a a b a
最大长度值(包含自己) 0 0 1 2 3 1 1 2 3
next(不包含自己)(下标从0开始) -1 0 0 1 2 3 1 1 2
next(不包含自己)(下标从1开始) 0 1 1 2 3 4 2 2 3

在这里插入图片描述

void GetNext(char* p,int next[])
{
	next[1] = 0;
	int k = 1;
	int j = 0;
	while (j < p[0])
	{
		//p[k]表示前缀,p[j]表示后缀
		if (k == 0 || p[j] == p[k]) //k==0,失配后,给失配处的字符赋值next值
		{
			++k;
			++j;
			next[j] = k;
		}
		else 
		{
			k = next[k]; //若字符不相同,则k值回溯,寻找次大的匹配,使得k值回溯程度最小
		}
	}
}

5.4next数组的优化

在这里插入图片描述

当中的2、3、4、5步骤,其实都是多余的判断。由于p串的第二、三、四、五位置的字符都与首位的"a"相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,这是个很好的办法。

//优化过后的next 数组求法
void GetNextval(char* p, int next[])
{

	next[1] = 0;
	int i = 1;
	int j = 0;
	while (i < p[0])
	{
		//p[j]表示前缀,p[i]表示后缀  
		if (j == 0 || p[i] == p[j])
		{
			++i;
			++j;
			//较之前next数组求法,改动在下面4行
			if (p[i] != p[j])
				next[i] = j;   //若当前字符与前缀字符不同,则当前的j为next在i位置的值
			else
				//因为不能出现p[i] = p[ next[i]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
				next[i] = next[j]; //如果与前缀字符相同,则将前缀字符的next值赋值给next在i位置的值
		}
		else
		{
			j = next[j];
		}
	}
}
j 1 2 3 4 5 6 7 8 9
模式串p a b a b a a a b a
最大长度值(包含自己) 0 0 1 2 3 1 1 2 3
next(不包含自己)(下标从0开始) -1 0 0 1 2 3 1 1 2
next(不包含自己)(下标从1开始) 0 1 1 2 3 4 2 2 3
改进的next值 0 1 0 1 0 4 2 1 0

如果某字符A的next值指向的字符B=这个字符A,则将B的next赋值给A的next值

如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。

发布了58 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Miracle_520/article/details/101076614