数据结构——串的模式匹配算法(BF算法、KMP算法)

串的模式匹配:就是子串的定位操作。

一、BF算法

(一)算法原理

从主串S的第pos个字符起和子串的第一个字符比较之,若相等,则逐个比较后续字符;若不相等,则从主串的下一个字符起再重新和子串的字符比较之。依次类推,直至子串T中的每一个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数值为子串T中的第一个字符在主串S中的序号,否则称匹配不成功,函数值为零。

(二)C语言实现

cpp 复制代码
int BF_Search(const char* MainStr, const char* SubStr)
{
    //1. 申请两个指针i和j, 分别指向主串和子串的起始位置
	int i = 0;
	int j = 0;

    //2. 进入while循环, 循环条件是i和j合法(主串指针i没有遍历完主串 并且 子串指针j没有遍历完子串)
	while (i < strlen(MainStr) && j < strlen(SubStr))
	{
        //3. 比较i和j指向的字符, 如果相同, 则让i和j同步向后移动一个位置
		if (MainStr[i] == SubStr[j])
		{
			i++;
			j++;
		}
        //4. 如果不相同, 匹配失败,需要回退指针i和j
		else
		{
			i = i - j + 1; //主串指针i回退到本次匹配起始位置的下一位置
			j = 0; //字串指针j回退到开始位置(0下标)
		}
	}
    //5. 判断子串是否全部匹配完成
	if (j < strlen(SubStr)) //子串没有遍历完,说明主串没有找到子串,返回-1
	{
		return -1;
	}
	else //子串全部匹配成功
	{
		return i - j;
	}
}

(三)复杂度分析

普遍情况 :时间复杂度为O(n+m),其中n为主串长度,m为要匹配的字串长度;

最好情况 :就是一开始就匹配成功,比如"googlegood"中去找"google",时间复杂度为O(1)

最坏情况 :就是每次不成功的匹配都发生在子串的最后一个字符,比如"000000001"中去找"00001",时间复杂度为O((n-m+1)*m)

二、KMP算法

(一)算法原理

1.为什么需要KMP算法

BF算法,每次字符匹配失败时,主串指针i需要回退,字串指针j重置为0,从头开始匹配;这种算法会导致大量重复的字符比较,在长字符串匹配中效率很低。

2.KMP的核心思想

主串指针i只向前移动,永远不回退;匹配失败时,只调整子串指针j到合适的位置,避免重复比较。它的实现完全依赖于子串自身的结构信息,具体来说就是利用子串的最长相等前后缀。

相较于BF算法,KMP算法的改进在于:每当一趟匹配过程中出现字符比较不等时,不需要回溯 i 指针,而是利用已经得到的"部分匹配"的结果将模式向右"滑动"尽可能远的一段距离后,继续进行比较。

(二)next数组值推导

1.推导过程

(1)j=0时,next0=-1

(2)j=1时,next1=0

(3)j>1时,观察下标由0到j-1的字符串,如果前后缀没有相等字符,next值为0;如果前后缀一个字符相等,next值为1;两个字符相等,next值为2;k个字符相等,next值就是k

2.举例

(前缀用下划线表示,后缀用斜体表示)

(1)以"abcdex"为例

|------|----|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 |
| T | a | b | c | d | e | x |
| next | -1 | 0 | 0 | 0 | 0 | 0 |

a)默认next0 = -1,next1 = 0

b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0

c)j=3时,由下标0到j-1的字符串是"abc",前后缀没有相等字符,next3=0

d)此后同理,最终此串的nextj为-100000

(2)以"abcabx"为例

|------|----|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 |
| T | a | b | c | a | b | x |
| next | -1 | 0 | 0 | 0 | 1 | 2 |

a)默认next0 = -1,next1 = 0

b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0

c)j=3时,由下标0到j-1的字符串是"abc",前后缀没有相等字符,next3=0

d)j=4时,由下标0到j-1的字符串是"++a++ bca ",前缀字符"++a++ "与后缀字符"a"相等,next4=1

e)j=5时,由下标0到j-1的字符串是"++ab++ cab ",前缀字符"++ab++ "与后缀字符"ab"相等,next5=2

最终此串的nextj为-100012

(3)以"ababaaaba"为例

|------|----|---|---|---|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | b | a | b | a | a | a | b | a |
| next | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |

a)默认next0 = -1,next1 = 0

b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0

c)j=3时,由下标0到j-1的字符串是"++a++ ba ",前缀字符"++a++ "与后缀字符"a"相等,next3=1

d)j=4时,由下标0到j-1的字符串是"++ab++ ab ",前缀字符"++ab++ "与后缀字符"ab"相等,next4=2

e)j=5时,由下标0到j-1的字符串是"++aba++ ba ",前缀字符"++aba++ "与后缀字符"aba"相等,next5=3

f)j=6时,由下标0到j-1的字符串是"++a++ babaa ",前缀字符"++a++ "与后缀字符"a"相等,next6=1

g)j=7时,由下标0到j-1的字符串是"++a++ babaaa ",前缀字符"++a++ "与后缀字符"a"相等,next7=1

h)j=8时,由下标0到j-1的字符串是"++ab++ abaaab ",前缀字符"++ab++ "与后缀字符"ab"相等,next8=2

最终此串的nextj为-100123112

(4)以"aaaaaaaab"为例

|------|----|---|---|---|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | a | a | a | a | a | a | a | b |
| next | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

a)默认next0 = -1,next1 = 0

b)j=2时,由下标0到j-1的字符串是"++aa++ ",前缀字符"++a++ "与后缀字符"a"相等,next2=1

c)j=3时,由下标0到j-1的字符串是"++aa++ a ",前缀字符"++aa++ "与后缀字符"aa"相等,next3=2

d)j=4时,由下标0到j-1的字符串是"++aa++ ++a++a ",前缀字符"++aaa++ "与后缀字符"aaa"相等,next4=3

e)j=5时,由下标0到j-1的字符串是"++aaa++ ++a++a ",前缀字符"++aaaa++ "与后缀字符"aaaa"相等,next5=4

f)j=6时,由下标0到j-1的字符串是"++aaaa++ ++a++a ",前缀字符"++aaaaa++ "与后缀字符"aaaaa"相等,next6=5

g)j=7时,由下标0到j-1的字符串是"++aaaaa++ ++a++a ",前缀字符"++aaaaaa++ "与后缀字符"aaaaaa"相等,next7=6

h)j=8时,由下标0到j-1的字符串是"++aaaaaa++ ++a++a ",前缀字符"++aaaaaaa++ "与后缀字符"aaaaaaa"相等,next8=7

最终此串的nextj为-101234567

(三)C语言实现

cpp 复制代码
int* Get_Next(const char* SubStr)
{
	//1.申请一个next数组,长度和子串长度一致
	int* next = (int*)malloc(sizeof(int) * strlen(SubStr));
	if (next == NULL) exit(EXIT_FAILURE);

	//2.将next数组前两个格子直接赋值-1和0
	next[0] = -1;
	next[1] = 0;

	//3.申请一个变量j指向当前最新的已知其next值的字符(初始化为1号下标)
	int j = 1;

	//4.再申请一个变量NEX用来保存当前字符失配时,向前可以看到的最长公共前后缀的共有长度(换句话说,就是可以有多少个字符可以不用比较)
	int NEX = 0;

	//5.进入while循环,循环条件是j合法(j最多指向倒数第二个字符)
	while (j + 1 < strlen(SubStr))
	{
		//6.用当前j指向的字符和其NEX保存的回退位置的字符进行比较
		//7.如果相同,则让NEX+1,然后赋值给下一个字符的next数组位置
		if (SubStr[j] == SubStr[NEX] || NEX == -1) //9.如果一直比较失败,也就是NEX==-1,则此时将NEX+1变成0赋值给下一个字符的next数组的位置
		{
			next[++j] = ++NEX;
		}

		else //8.如果不相同,则让NEX去保存其回退位置字符的回退位置,接着比较,直到比较成功,或者NEX触底(触底就是NEX==-1)
		{
			NEX = next[NEX];
		}
	}

	//10.当while循环退出,则next数组全部赋值完毕
	return next;
}

int KMP_Search(const char* MainStr, const char* SubStr)
{
	//1.申请两个指针i和j,分别指向主串和子串的开始位置
	int i = 0;
	int j = 0;

	//2.提供子串对应的next数组
	int* next = Get_Next(SubStr);

	//3.进入while循环,循环的条件是i和j合法(换句话说就是还没有越界)
	while (i < (int)strlen(MainStr) && j < (int)strlen(SubStr))
	{
		//4.比较i和j指向的字符,如果相同,则让i和j同步向后移动一个位置
		if (j == -1 || MainStr[i] == SubStr[j])
		{
			i++;
			j++;
		}

		//5.如果不相同,则让指向子串的j变量,回退到开始位置(0下标)
		else
		{
			//i永远不回退
			j = next[j];//j回退到一个合适的位置
		}
	}

	//6.如果while循环进不去,则说明查找结束了,只需要对j来做判断,就可以知道是否查找成功(j走出自身范围则找到,相反j未走出自身范围则没找到)
	if (j < strlen(SubStr))
	{
		return -1;
	}

	return i - j;
}

(四)复杂度分析

时间复杂度为O(n+m),其中n为主串长度,m为字串长度。

注意: KMP算法仅当子串与主串之间存在许多"部分匹配"的情况下才体现出它的优势,否则两者差异并不明显。

三、KMP算法改进

(一)改进原理

以主串"aaaabcde",子串"aaaaax"为例,可以发现,我们使用KMP算法时,过程中的②③④⑤步骤,其实是多余的判断。由于子串的第二、三、四、五位置的字符都与首位的"a"相等,那么可以用首位next值去取代它相等字符的next值。

(二)nextval数组值推导

1.推导过程

(1)先算出next数组

(2)j=0时,nextval0=-1

(3)j>0时,若j指向的字符与其next值所对应的回退位置字符相同,则说明next值保存的回退位置无效,直接将其回退位置字符的nextval值誊抄过来(nextvalj = nextvalnext\[j]);若j指向的字符与其next值所对应的回退位置字符不相同,则说明next值保存的回退位置有效,直接将next值誊抄过来(nextvalj=nextj)

2.举例

(1)以"ababaaaba"为例

|---------|----|---|----|---|----|---|---|---|----|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | b | a | b | a | a | a | b | a |
| next | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |
| nextval | -1 | 0 | -1 | 0 | -1 | 3 | 1 | 0 | -1 |

a)默认nextval0 = -1

b)j=1时,j指向的字符是"b",next值是0,0下标的字符是"a",它们不相同,所以nextval1=next1=0

c)j=2时,j指向的字符是"a",next值是0,0下标的字符是"a",它们相同,所以nextval2=nextval0=-1

d)j=3时,j指向的字符是"b",next值是1,1下标的字符是"b",它们相同,所以nextval3=nextval1=0

e)j=4时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval4=nextval2=-1

f)j=5时,j指向的字符是"a",next值是3,3下标的字符是"b",它们不相同,所以nextval5=next5=3

g)j=6时,j指向的字符是"a",next值是1,1下标的字符是"b",它们不相同,所以nextval6=next6=1

h)j=7时,j指向的字符是"b",next值是1,1下标的字符是"b",它们相同,所以nextval7=nextval1=0

i)j=8时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval8=nextval2=-1

最终此串的nextj为-1 0 -1 0 -1 3 1 0 -1

(2)以"aaaaaaaab"为例

|---------|----|----|----|----|----|----|----|----|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | a | a | a | a | a | a | a | b |
| next | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| nextval | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | 7 |

a)默认nextval0 = -1

b)j=1时,j指向的字符是"a",next值是0,0下标的字符是"a",它们相同,所以nextval1=nextval0=-1

c)j=2时,j指向的字符是"a",next值是1,1下标的字符是"a",它们相同,所以nextval2=nextval1=-1

d)j=3时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval3=nextval2=-1

e)j=4时,j指向的字符是"a",next值是3,3下标的字符是"a",它们相同,所以nextval4=nextval3=-1

f)j=5时,j指向的字符是"a",next值是4,4下标的字符是"a",它们相同,所以nextval5=nextval4=-1

g)j=6时,j指向的字符是"a",next值是5,5下标的字符是"a",它们相同,所以nextval6=nextval5=-1

h)j=7时,j指向的字符是"a",next值是6,6下标的字符是"a",它们相同,所以nextval7=nextval6=-1

i)j=8时,j指向的字符是"b",next值是7,7下标的字符是"a",它们不相同,所以nextval8=next8=7

最终此串的nextj为-1 -1 -1 -1 -1 -1 -1 -1 7

(三)C语言实现

cpp 复制代码
int* Get_Nextval(const char* SubStr)
{
	//1.先完成next数组
	int* next = Get_Next(SubStr);

	//2.再申请一个nextval数组,并将其第一个格子赋值为-1
	int* nextval = (int*)malloc(sizeof(int) * strlen(SubStr));
	if (nextval == NULL) exit(EXIT_FAILURE);
	nextval[0] = -1;

	//3.申请变量j,让其从第二个字符开始向后遍历
	for (int j = 1; j < strlen(SubStr); j++)
	{
		//4.如果j指向的字符和其next数组保存的回退位置字符相同,则说明next数组保存的回退位置无效,直接将其回退位置字符的nextval对应值(最优解)照抄过来
		if (SubStr[j] == SubStr[next[j]])
		{
			nextval[j] = nextval[next[j]];
		}

		//5.如果j指向的字符和其next数组保存的回退位置字符不相同,则说明next数组保存的回退位置有效,直接照抄过来即可
		else
		{
			nextval[j] = next[j];
		}
	}

	free(next);

	return nextval;
}

int KMP_Search2(const char* MainStr, const char* SubStr)
{
	//1.申请两个指针i和j,分别指向主串和子串的开始位置
	int i = 0;
	int j = 0;

	//2.提供子串对应的next数组
	int* nextval = Get_Nextval(SubStr);

	//3.进入while循环,循环的条件是i和j合法(换句话说就是还没有越界)
	while (i < (int)strlen(MainStr) && j < (int)strlen(SubStr))
	{
		//4.比较i和j指向的字符,如果相同,则让i和j同步向后移动一个位置
		if (j == -1 || MainStr[i] == SubStr[j])
		{
			i++;
			j++;
		}

		//5.如果不相同,则让指向子串的j变量,回退到开始位置(0下标)
		else
		{
			//i永远不回退
			j = nextval[j];//j回退到一个合适的位置
		}
	}

	//5.如果while循环进不去,则说明查找结束了,只需要对j来做判断,就可以知道是否查找成功(j走出自身范围则找到,相反j未走出自身范围则没找到)
	if (j < strlen(SubStr))
	{
		return -1;
	}

	return i - j;
}
相关推荐
tachibana21 小时前
hot100 排序链表(148)
java·数据结构·算法·leetcode·链表
林泽毅1 小时前
Qwen3.5 DPO 模型对齐实战(pytrio训练版)
人工智能·python·算法
imuliuliang1 小时前
算法中的随机化思想及其复杂度收益评估的技术7
算法
不能跑的代码不是好代码1 小时前
二叉树从基础概念到LeetCode实战
算法·leetcode
凯瑟琳.奥古斯特2 小时前
二分查找解力扣1011最优运载能力
开发语言·c++·算法·leetcode·职场和发展
学计算机的计算基2 小时前
二叉树算法下篇:递归核心技巧与高频面试题详解
java·笔记·算法
水龙吟啸2 小时前
华为2026.6.24机考选择题+编程题【速刷敲黑板】
人工智能·深度学习·算法·机器学习·华为
Eloudy2 小时前
全文 - Evolution Strategies as a Scalable Alternative to Reinforcement Learning
人工智能·算法
Yang_jie_033 小时前
笔记:数据结构(链表)
数据结构·笔记·链表