1.串的定义

  1. 串(string)的定义:串是由零个或者多个字符组成的有限序列,又叫做字符串。串中的字符数目称为串的长度,零个字符的串叫做空串,它的长度为0,可以用一对双引号""表示。有限序列是指串的长度是一个有限值,并且串的相邻字符之间有前驱后继的关系。
  2. 子串:串中任意个数的连续字符组成的子序列称为子串。
  3. 主串:包含子串的串称为主串。

2.串的基本操作

  1. 字符串拷贝
  2. 长度获取
  3. 字符串拼接
  4. 模式匹配等

3.串的存储结构

串的存储结构和线性表一样,分为顺序存储结构和链式存储结构。

1.串的顺序存储结构

在顺序存储结构实现的串中,需要预分配好一组地址连续的存储单元来存储串中的字符序列的。因此串的一些操作比如两串的连接strconcat()和字符串插入strinsert()和字符串替换strreplae()都很可能超出预分配的存储单元的大小。

2.串的链式存储结构

在链式存储结构实现的串中,串中的每个元素数据是一个字符。如果也简单的应用链表存储串值,一个结点对应一个字符就会存在很大的空间浪费。因此我们考虑一个结点存放多个字符,最后一个结点没有占满时,用“#”或者其他非串值字符来补全。一个结点存放多少个字符会影响串处理的效率,这个视情况而定。

4.朴素的模式匹配算法,亦称Brute-Force(暴力)算法

  1. 模式匹配:在主串中查找子串的位置。
  2. 我们用很容易想到的思维穷举法来实现朴素的模式匹配算法,如下:
#include <iostream>

/*
*	@brief 暴力算法:穷举法在主串中匹配模式串,找到则返回在主串中的位置
*	否者返回-1
*/
int index(const std::string& mainString,const std::string& subString)
{

	//i为主串中开始位置的下标,j为子串中开始位置的下标,k保留主串开始匹配的起始位置
	int i = 0, j = 0,k = i;
	while (i < mainString.size() && j < subString.size())
	{
		if (mainString[i] == subString[j])
		{
			i++;
			j++;
		}
		else
		{
			i = ++k;
			j = 0;
		}
	}

	if (j == subString.size())
	{
		return k + 1;
	}
    return -1;
}
int main()
{
	std::string str1 = "HelloWorld";
	std::string str2 = "World";
	
	int ret = index(str1,str2);
	std::cout << "pos:" << ret << std::endl;  //pos为6

	return 0;
}
  1. 我们来分析以上算法匹配成功时的时间复杂度:记作mainLength为n,subLength为m。
    1. 最好的情况:当pos为1时,就匹配成功。比如在主串“HelloWorld”中匹配子串“Hello”,此时时间复杂度为O(1).
    2. 稍差的情况:pos为一时,每次主串的首字母不匹配,当pos为主串的长度减去子串的长度时才匹配成功。比如在主串“HelloWorld”中匹配子串“World”,此时时间复杂度为O(n + m).
    3. 最坏的情况:pos为1时,每次不成功的匹配都发生在子串的最后一个字符,当pos为主串的长度减去子串的长度时才匹配成功。比如在主串“00001”中匹配子串“01”,此时在到主串的第4个位置进行两次判断操作才成功。期间共进行了(5-2+1)*2次判断,时间复杂度为O((n - m + 1)* m).

5.KMP模式匹配算法

  1. 由来:KMP算法是由D.E.Knuth,J.H.Morris,V.R.Pratt三人共同提出的算法,简称KMP算法。为什么三位研究者提出KMP算法呢?当然是提高之前的暴力匹配字符串算法效率了。
  2. 核心思想:消除主串中匹配位置指针的回溯,利用前面比较中的有用信息,改变模式串中匹配位置指针,减少不必要的主串和模式串的比较次数。举个例子来说:现在有主串'abcdefgab',子串'abcdex',按照朴素模式匹配算法来讲它会历经123456六个比较步骤,但是对于KMP算法来将,它直接从1跳到6步骤进行比较,减少了比较次数。那么它是怎么知道在f与x失配时直接将j的位置跳到i=6进行比较呢?
  3. 算法关键:假设主串记为S,模式串记为T。i指向主串S待匹配位置的指针,j指向子串T中待匹配位置的指针。假设模式串中第j个位置与主串第i个位置发生不匹配时,应从模式串第next[j]个位置与主串第i个位置重新开始比较,也就是说i没有回溯,移动的是J的位置,相当于模式串向右移动了j-next[j]位。因此算法的关键是next数组的求解
    1. 前缀集:前缀就是以字符串开头字母打头并且除了字符串本身的字符集合;
    2. 后缀集:后缀就是以字符串的最后一个字母结束除了字符串本身的字符集合。比如说一个字符串:“Hello”,则它的前缀集为{"H","He","Hel","Hell"},它的后缀集为{"llo","lo","ello"}.
    3. next数组:存储子串每个位置失配时的下一次j指针移动位置。其本质是前缀集和后缀集的交集最大长度.即next[j]的值为第j位字符前面j-1个字符组成的字串的最大前后缀长度。
  4. KMP算法的实现:
//统一约定数组位置下标从0开始
#include <iostream>

//next数组的求解
void getNext(const std::string& subString,int * next)
{
	int j = 0, t = -1;		// j为后缀末尾,t为前缀末尾
	next[0] = -1;
	while (j < subString.size())
	{
		if (t == -1 || subString[j] == subString[t])
		{	
			// 前后缀相同
			next[j + 1] = t + 1;
			++t;
			++j;
		}
		else
		{
			// 前后缀不相同
			t = next[t];
		}
	}

}

int KMP(const std::string& mainString,const std::string& subString)
{
	//i,j分别为主串,模式串匹配中的位置下标,从0开始
	int i = 0, j = 0;	
	const int length1 = subString.size();
	const int length2 = mainString.size();
	int next[length2] = {0};
	//得到子串的next数组
	getNext(subString,next);	
	while (i < length2 && j < length1)
	{
        //j为-1时,就又从子串的开始位置下标0开始匹配,
		if (j == -1 || mainString[i] == subString[j])
		{
			++i;
			++j;
		}
		else
		{
		    // 等同于next数组整体后移一位
			j = next[j];	//j != -1且mainString[i] != subString[j],为了解决i号位置的不匹配,j回溯
		}
	}
	if (j == length1)
	{
		return i - j + 1;
	}
	else
	{
		return -1;
	}
}

int main()
{
	std::string str1 = "HelloWorld";
	std::string str2 = "torld";

	int ret = KMP(str1,str2);
	std::cout << "pos:" << ret << std::endl;	//6

	return 0;
}