字符串匹配算法BF和KMP

Aki 发布于 2022-12-22 252 次阅读


BF(暴力算法)、

正如其名字相同,该算法就是一个暴力求解的算法,不断遍历字符串每一个字符,如果该字符于匹配模式串的第一个字符相等,就进行匹配,如果在匹配的过程中发现有一个字符不匹配,就退出匹配,继续下一轮。时间复杂度是O(n*m),n,m分别代表两个字符串的长度,效率很低。

//暴力匹配
int64_t BF(const string& str, const string& pattern)
{
	size_t s1 = str.size();
	size_t s2 = pattern.size();
	if (s1 < s2 || str.empty() || pattern.empty())
	{
		return -1;
	}

	size_t j = 0;
	size_t i = 0;
	for (; i < s1 && j < s2;)
	{
		if (str[i] == pattern[j])
		{
			++j; 
			++i;
		}
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}
	return j >= s2 ? i - j : -1;
}

KMP算法、

KMP算法详解_

有句话可以这么形容KMP:一个人能走的多远不在于他在顺境时能走的多快,而在于 他在逆境时多久能找到曾经的自己。KMP算法是一个字符串匹配算法,取得是三个发明人的名字首字母。

KMP算法一种改进的模式匹配算法,它的改进在于:每当从某个起始位置开始一趟比较后,在匹配过程中出现失配,不回溯,而是利用已经得到的部分匹配结果,将一种假想的位置定位“指针”在模式上 向右滑动尽可能远的一段距离到某个位置后,继续按规则进行下一次的比较。

KMP算法的时间复杂度是O(n),其中n是主串的长度。

KMP算法中一些重要的概念、

最长公共前后缀:前缀是指包含第一个字符的子串,后缀是指包含最后一个字符的子串。最长公共就是前缀和后缀要相等,且长度要为最长,但是这个长度不能等于字符串的长度,否则就没有意义了。

next数组:首先next数组是针对较小的字符串而言的,换句话说就是需要匹配的字符串。其次,该数组的每个位置的值(或者叫信息)其实是跟该位置没有关系,它其实表示的是它前一位置的信息,这个信息是什么呢?就是该位置的前一位置的最长公共前后缀的长度。

给定义一个字符串,如下所示:

string str = "AABCACD";

需要定义一个next数组来辅助KMP算法
int *next = new int[str.size()]{};

我们人为的规定,next[0]=-1,next[1]=0;
size_t i = 0;
next[i++] = -1; //i=0
next[i++] = 0;  //i=1
next[i++] = 1;  //i=2,表示pattern[i]之前的字符的最长公共前后缀,不包括pattern[i]
next[i++] = 0;  //i=3
next[i++] = 0;  //i=4
next[i++] = 1;  //i=5
next[i++] = 0;  //i=6
 

KMP完整实现、

// 计算next数组
int64_t* _get_next_(const string& pattern)
{
	
	size_t size = pattern.size();
	int64_t* next = new int64_t[size] {};
	next[0] = -1;    //人为规则为-1
	next[1] = 0;     //人为规则为0

	size_t i = 2;
	size_t cn = 0;

	while (i < size)
	{
		if (pattern[i - 1] == pattern[cn])
		{
			next[i++] = ++cn;
		}
		else if (cn > 0)
		{
			cn = next[cn];
		}
		else
		{
			next[i++] = 0;
		}
	}
	return next;
}


//KMP匹配,不适用于单个字符匹配!!!
int64_t KMP(const string& str, const string& pattern)
{
	size_t s1 = str.size();
	size_t s2 = pattern.size();

	if (s1 < s2 || s2 == 1 || str.empty() || pattern.empty())
	{
		return -1;
	}

        int64_t* next = _get_next_(pattern);

	if (!next)
	{
		return -1;
	}

	size_t i = 0;  //指向str的首元素指针
	size_t j = 0;  //指向pattern的首元素指针

	while (i < s1 && j < s2)
	{
		if (str[i] == pattern[j])
		{
			i++;
			j++;
		}
		else if( j != 0)
		{
			j = next[j];
		}
		else
		{
			++i;
		}
	}

	delete[]next;
	next = nullptr;

	return j == s2 ? i - j : -1;
}

测试、

#include<iostream>
using namespace std;
#define _CRTDBG_MAP_ALLOC
#include<crtdbg.h>
#include<cassert>
#include<algorithm>
#include<string>
#include<time.h>



void optimize_iostream_and_check_leak_memory()
{
	_CrtSetDbgFlag(33);
	std::ios::sync_with_stdio(false);
	std::cin.tie(0);
	std::cout.tie(0);
}

// 计算next数组
int64_t* _get_next_(const string& pattern)
{
	
	size_t size = pattern.size();
	int64_t* next = new int64_t[size] {};
	next[0] = -1;    //人为规则为-1
	next[1] = 0;     //人为规则为0

	size_t i = 2;
	size_t cn = 0;

	while (i < size)
	{
		if (pattern[i - 1] == pattern[cn])
		{
			next[i++] = ++cn;
		}
		else if (cn > 0)
		{
			cn = next[cn];
		}
		else
		{
			next[i++] = 0;
		}
	}
	return next;
}


//KMP匹配,不适用于单个字符匹配!!!
int64_t KMP(const string& str, const string& pattern)
{
	size_t s1 = str.size();
	size_t s2 = pattern.size();

	if (s1 < s2 || s2 == 1 || str.empty() || pattern.empty())
	{
		return -1;
	}

        int64_t* next = _get_next_(pattern);

	if (!next)
	{
		return -1;
	}

	size_t i = 0;  //指向str的首元素指针
	size_t j = 0;  //指向pattern的首元素指针

	while (i < s1 && j < s2)
	{
		if (str[i] == pattern[j])
		{
			i++;
			j++;
		}
		else if( j != 0)
		{
			j = next[j];
		}
		else
		{
			++i;
		}
	}

	delete[]next;
	next = nullptr;

	return j == s2 ? i - j : -1;
}


//暴力匹配
int64_t BF(const string& str, const string& pattern)
{
	size_t s1 = str.size();
	size_t s2 = pattern.size();
	if (s1 < s2 || str.empty() || pattern.empty())
	{
		return -1;
	}

	size_t n = 0;
	for (size_t i = 0; i <= s1 - s2; ++i)
	{
		n = 0;
		if (str[i] == pattern[0])
		{
			for (size_t j = 1, k = i + 1; j < s2; ++j,++k)
			{
				if (str[k] != val[j])
				{
					break;
				}
				++n;
			}
			if (n == (s2 - 1))
			{
				return i;
			}
		}
	}
	return -1;
int64_t BF(const string& str, const string& pattern)
{
	size_t s1 = str.size();
	size_t s2 = pattern.size();
	if (s1 < s2 || str.empty() || pattern.empty())
	{
		return -1;
	}

	size_t j = 0;
	size_t i = 0;
	for (; i < s1 && j < s2;)
	{
		if (str[i] == pattern[j])
		{
			++j; 
			++i;
		}
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}
	return j >= s2 ? i - j : -1;
}


void Test()
{
	string str{};

	// 10000000 *26 个字符 a - z
	for (size_t i = 0; i < 10000000; ++i)
	{
		for (char s = 'a'; s <= 'z'; ++s)
		{
			str.push_back(s);
		}
	}

	string pattern = "abcdeffg";   //设置查找的字符在最后面,最坏的匹配情况
	str += pattern;

	auto begin = clock();
	size_t pos = KMP(str, pattern);
	auto end = clock();
	cout << "KMP time =  " << end - begin << endl;
	cout << "pos = " << pos << endl;


	auto begin2 = clock();
	size_t pos2 = BF(str, pattern);
	auto end2 = clock();
	cout << "BF time =  " << end2 - begin2 << endl;
	cout << "pos = " << pos2 << endl;

	auto begin3 = clock();
	size_t pos3 = str.find(pattern);
	auto end3 = clock();
	cout << "Find time =  " << end3 - begin3 << endl;
	cout << "pos = " << pos3 << endl;

}

int main()
{
	optimize_iostream_and_check_leak_memory();

	Test();

	return 0;
}

在我测试的情况下KMP和BF的差别不大,但与内置string的find方法就是一个天一个地了。

所以自己写的算法也就图一乐,使用时还得是用内置的算法。