1.串的定义
- 串(string)的定义:串是由零个或者多个字符组成的有限序列,又叫做字符串。串中的字符数目称为串的长度,零个字符的串叫做空串,它的长度为0,可以用一对双引号
""
表示。有限序列是指串的长度是一个有限值,并且串的相邻字符之间有前驱后继的关系。 - 子串:串中任意个数的连续字符组成的子序列称为子串。
- 主串:包含子串的串称为主串。
2.串的基本操作
- 字符串拷贝
- 长度获取
- 字符串拼接
- 模式匹配等
3.串的存储结构
串的存储结构和线性表一样,分为顺序存储结构和链式存储结构。
1.串的顺序存储结构
在顺序存储结构实现的串中,需要预分配好一组地址连续的存储单元来存储串中的字符序列的。因此串的一些操作比如两串的连接strconcat()和字符串插入strinsert()和字符串替换strreplae()都很可能超出预分配的存储单元的大小。
2.串的链式存储结构
在链式存储结构实现的串中,串中的每个元素数据是一个字符。如果也简单的应用链表存储串值,一个结点对应一个字符就会存在很大的空间浪费。因此我们考虑一个结点存放多个字符,最后一个结点没有占满时,用“#”或者其他非串值字符来补全。一个结点存放多少个字符会影响串处理的效率,这个视情况而定。
4.朴素的模式匹配算法,亦称Brute-Force(暴力)算法
- 模式匹配:在主串中查找子串的位置。
- 我们用很容易想到的思维穷举法来实现朴素的模式匹配算法,如下:
#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;
}
- 我们来分析以上算法匹配成功时的时间复杂度:记作mainLength为n,subLength为m。
- 最好的情况:当pos为1时,就匹配成功。比如在主串“HelloWorld”中匹配子串“Hello”,此时时间复杂度为O(1).
- 稍差的情况:pos为一时,每次主串的首字母不匹配,当pos为主串的长度减去子串的长度时才匹配成功。比如在主串“HelloWorld”中匹配子串“World”,此时时间复杂度为O(n + m).
- 最坏的情况:pos为1时,每次不成功的匹配都发生在子串的最后一个字符,当pos为主串的长度减去子串的长度时才匹配成功。比如在主串“00001”中匹配子串“01”,此时在到主串的第4个位置进行两次判断操作才成功。期间共进行了
(5-2+1)*2
次判断,时间复杂度为O((n - m + 1)* m).
5.KMP模式匹配算法
- 由来:KMP算法是由D.E.Knuth,J.H.Morris,V.R.Pratt三人共同提出的算法,简称KMP算法。为什么三位研究者提出KMP算法呢?当然是提高之前的暴力匹配字符串算法效率了。
- 核心思想:消除主串中匹配位置指针的回溯,利用前面比较中的有用信息,改变模式串中匹配位置指针,减少不必要的主串和模式串的比较次数。举个例子来说:现在有主串
'abcdefgab'
,子串'abcdex'
,按照朴素模式匹配算法来讲它会历经123456六个比较步骤,但是对于KMP算法来将,它直接从1跳到6步骤进行比较,减少了比较次数。那么它是怎么知道在f与x失配时直接将j的位置跳到i=6进行比较呢?
- 算法关键:假设主串记为S,模式串记为T。i指向主串S待匹配位置的指针,j指向子串T中待匹配位置的指针。假设模式串中第j个位置与主串第i个位置发生不匹配时,应从模式串第next[j]个位置与主串第i个位置重新开始比较,也就是说i没有回溯,移动的是J的位置,相当于模式串向右移动了j-next[j]位。因此算法的关键是next数组的求解
- 前缀集:前缀就是以字符串开头字母打头并且除了字符串本身的字符集合;
- 后缀集:后缀就是以字符串的最后一个字母结束除了字符串本身的字符集合。比如说一个字符串:“Hello”,则它的前缀集为{"H","He","Hel","Hell"},它的后缀集为{"llo","lo","ello"}.
- next数组:存储子串每个位置失配时的下一次j指针移动位置。其本质是前缀集和后缀集的交集最大长度.即next[j]的值为第j位字符前面j-1个字符组成的字串的最大前后缀长度。
- 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;
}