从零开始学数据结构系列之第二章《字符串kmp匹配》

文章目录


前言

​    KMP算法是我们数据结构串中最难也是最重要的算法。 难是因为KMP算法的代码很优美简洁干练,但里面包含着非常深的思维。真正理解代码的人可以说对KMP算法的了解已经相当深入了。而且这个算法的不少东西的确不容易讲懂,很多正规的书本把概念一摆出直接劝退无数人。开始我先对KMP算法的三位创始人Knuth,Morris,Pratt致敬,懂得这个算法的流程后你真的不得不佩服他们的聪明才智。

解决的问题类型

​   KMP算法的作用是在一个已知字符串中查找子串的位置,也叫做串的模式匹配 。比如主串s="university",子串t="sit"。现在我们要找到子串t 在主串s 中的位置。大家肯定觉得这还不简单,不就在第七个嘛,一眼就看出来了。

​ 当然,在字符串非常少时,"肉眼观察法"不失为一个好方法。但如果要你在一千行文本里找一个单词,我想一般人都会数得崩溃吧。这就让我想起来考试的时候,如果一两道选择题不会。这时候,"肉眼观察法"可能效果不错,但是如果好几道大题不会呢?"肉眼观察法"就丝毫不起效了。所以打铁还需自身硬,我们把这种枯燥的事以一定的算法交给计算机处理。

第一种我们容易想到的就是暴力求解法。

这种方法也叫朴素的模式匹配:

​   简单来说就是:从主串s 和子串t 的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对...一直到子串字符全部匹配成功。

大家可能会想:这个方法也太慢了吧!求一个子串位置需要太多的步骤。而且很多步骤根本不必要进行。

这个想法非常好!!很多伟大的思想都是在一步步完善更正已有方法中诞生的。这种算法在最好情况下时间复杂度为O(n)。即子串的n个字符正好等于主串的前n个字符,而最坏的情况下时间复杂度为O(m*n)。相比而言这种算法空间复杂度为O(1),即不消耗空间而消耗时间。

下面就开始进入我们的正题:KMP算法是怎样优化这些步骤的。其实KMP的主要思想是:"空间换时间"。

大家打起精神,认真看下面的内容。

首先,为什么朴素的模式匹配这么慢呢?

你再回头看一遍就会发现,哦,原来是回溯的步骤太多了。所以我们应该尽量减少回溯的次数。

怎样做呢?比如上面第一个图:当字符'd'与'g'不匹配,我们保持主串的指向不变,

主串依然指向'd',而把子串进行回溯,让'd'与子串中'g'之前的字符再进行比对。

如果字符匹配,则主串和子串字符同时右移。

至于子串回溯到哪个字符,这个问题我们先放一放。

我先提出一个概念:一个字符串最长相等前缀和后缀。

教科书常用的手段是:在此处摆出一堆数学公式让大家自行理解。

这也是为什么看计算机学科的书没有较好的数学基础会很痛苦。

大家先不要强行理解数学公式,且听我慢慢道来:

我给大家个例子:

字符串 abcdab

前缀的集合:{a,ab,abc,abcd,abcda}

后缀的集合:{b,ab,dab,cdab,bcdab}

那么最长相等前后缀不就是ab嘛.

做个小练习吧:

字符串:abcabfabcab中最长相等前后缀是什么呢:

对就是abcab

好了我们现在会求一个字符串的前缀,后缀以及最长相等前后缀了。

这个概念很重要。到这里如果都看懂了,可以鼓励一下自己,然后回想一遍,再往下看。

之前留了一个问题,子串回溯到哪个字符,现在可以着手解决了。

​   KMP算法对朴素匹配算法进行了改进,利用匹配失败时失败之前的已知部分时匹配的 这个有效信息,保持主串的 i 指针不回溯 ,通过修改模式串(子串)的 j 指针,使模式串尽量地移动到有效的匹配位置该算法的时间复杂度为 O(n+m)

视频讲解

总体详解:

最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili

总代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define	size 300

typedef struct String 
{
	char *data;
	int len;
}String;


String* initString() 
{	
	String *s=(String *)malloc(sizeof(String));
	s->data = NULL;
	s->len = 0;
	return s;
}

void stringAssign(String* stu) 
{	
	char *temp=NULL;
	int len=0;
	temp =(char*)malloc(sizeof(char) * (size + 1));
	stu->data =(char*)malloc(sizeof(char) * (size + 1));
	gets(temp);
	while(*temp)
	{
		stu->data[len] = *temp;
		temp++;
		len++;
	}
	if(!len)
	{
		free(temp);
		temp = NULL;
		free(stu->data);
		stu->data=NULL;
	}
	stu->len=len;
}

void printString(String* s) 
{
	int i=0;
	for(;i<s->len;i++)
		printf(i==s->len-1?"%c":"%c->",s->data[i]);
	printf("\r\n");
}

/*
*获取next指针
*/

int* getNext(String* s) 
{
	int *next=NULL;			//防止野指针的出现
	int i=1;				//右移一位
	int prefix_len=0;		//next中当前公共前后缀的值
	char *data=NULL;		//防止野指针的出现
	next = (int*)malloc(sizeof(int) * s->len); 		//为next开辟空间
	next[0] = 0;									//前缀的公共前后缀默认为0
	data = s->data;									//将指针指向传递值的数据
	while( i < s->len)
	{
		if(data[prefix_len] == data[i])				//有前后缀的相同的情况
		{
			prefix_len++;							//当前公共前后缀的值增加
			next[i] = prefix_len;					//赋值到next数组中
			i++;									
		}
		else
		{
			if(prefix_len == 0)						//如果当前公共前后缀的值为0,则直接赋值
			{
				next[i] = prefix_len;
				i++;
			}
			else
				prefix_len = next[prefix_len-1];	//否则,则查表,可以类比左值等于右值,找最小公共端(这块建议看视频去理解)
		}

	}
	return next;
	

}

void printNext(int* next, String* s) 
{
	int i=0;
	for(;i<s->len;i++)
		printf(i==s->len-1?"%d":"%d->",next[i]);
	printf("\r\n");
}


void kmpMatch(String* master, String* sub) 
{
	int* next=NULL;
	int i=0;
	int j=0;
	int num=0;
	next=getNext(sub);

	while(i<master->len && j<sub->len)			
	{
		if(master->data[i] == sub->data[j])
		{
			j++;
			i++;
		}
		else if(j)
			j = next[j-1];
		else
			i++;
	num++;
	}

	printNext(next,sub);
	if(j==sub->len)
		printf("对比成功,在%d行对齐,共对比了%d次",i-j,num);
	else
		printf("funtion fail");

}




int main(int argc, char* argv[]) 
{
    String* s1 = initString();
	String* s2 = initString();
	stringAssign(s1);
	stringAssign(s2);
	printString(s1);
	printString(s2);
	kmpMatch(s1,s2);
	return 0;
}

往期回顾

1.【第一章】《线性表与顺序表》
2.【第一章】《单链表》
3.【第一章】《单链表的介绍》
4.【第一章】《单链表的基本操作》
5.【第一章】《单链表循环》
6.【第一章】《双链表》
7.【第一章】《双链表循环》
8.【第二章】《栈》
9.【第二章】《队》
10.【第二章】《字符串暴力匹配》

相关推荐
记录成长java41 分钟前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山41 分钟前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn44 分钟前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
睡觉谁叫~~~1 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程1 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错1 小时前
C语言扫雷小游戏
c语言·开发语言·算法
dsywws1 小时前
Linux学习笔记之vim入门
linux·笔记·学习
ClkLog-开源埋点用户分析1 小时前
ClkLog企业版(CDP)预售开启,更有鸿蒙SDK前来助力
华为·开源·开源软件·harmonyos
小屁孩大帅-杨一凡2 小时前
java后端请求想接收多个对象入参的数据
java·开发语言
m0_656974742 小时前
C#中的集合类及其使用
开发语言·c#