什么是子串,什么是子序列?
明确定义子序列不必是连续的。子串(数组)必须连续。笔试题中说让我们求子序列但往往他的含义就是子串,这样说是非常不好的。
KMP用途
解决子串包含问题,str1中是否包含str2,如果包含就返回str2开始的角标,否则返回-1。
int getIndexof(str1, str2)这个函数其实就是使用的KMP的内容。但是在C++或者java中可能该方法是改进过的,但是基于kmp,但是无论如何改进复杂度都是O(N)。只是常数的差别。
暴力解,我们可以拿着str2去str1中遍历,如果发现某一个位置不能匹配出str2,那么就向下移动一个位置。如果arr1.length()<arr2.length()就不用查了。反之arr1.length()>arr2.length(),如果我们通过遍历来找到答案时间复杂度是O(M*N)。这样做的缺点是前一次匹配不会对后一次匹配产生指导意义,但是KMP可以做到前一次的匹配对后一次的匹配产生指导意义。所以KMP快。
一个字符串中一个字符之前这个字符串最长前缀和最长后缀
最长前缀和最长后缀的匹配中前缀不能包括最后一个字母,后缀不能包含第一个字母。我们可以使用暴力的方法来解决这个问题,但是对于一个字符串中每个字符来求最长前缀和最长后缀是可以加速的。
判断str1中是否包含str2步骤
-
对str2求最长前缀和最长后缀,即next数组。对于str2第一个元素,我们认为其next数组对应的值为-1。
现在我们通过暴力方法求解的,但是请相信可以有函数可以又快有好的实现 -
这里我们的前提是,str1的i到x-1与 str2的0到y-1都是相同的,而str1 的x位置与str2的y位置是不同的。所以我们需要计算的就是str2每个元素对应的最长前缀和最长后缀,从而实现加速。
图中绿色部分表示最长前缀和最长后缀,一旦满足上述的条件,那么图中红色部分是不可能作为匹配的起点的,否则将会违反最长前缀和
最长后缀的前提
如上图所示,我们下一步比较的就是str1最长后缀的起始元素与str2的起始元素的匹配,因为绿色部分是完全一样的,所以实现的时候我们直接比较x与str2最长前缀终止位置的后一个元素(z)
举例
第一步:由于t不等于f,所以乙后退到next对应位置上的值所代表的角标(5)上,如下图
第二步:由于t不等于a,所以乙后退到next对应位置上的值所代表的角标(2)上,如下图
第三步:由于t不等于k,所以乙后退到next对应位置上的值所代表的角标(0)上,如下图
第三步:由于t不等于a,但是由于乙后退到角标0元素对应的next数组位置为-1,所以让甲向后退一个位置
为什么可以确保从i到j之前的元素是不会完成匹配str2的?
这里我们可以假设存在一点k位于i到j之前的位置使得str2可以完全匹配,如下图
前提是图中绿色区域是我们通过计算得到的str2中位于y点的最长前缀和最长后缀。因为从k位置开始完全匹配,那么在str1中从k到x-1的区间的元素一定和str2中从0开始到某个位置结束(大于最长前缀)区间内的元素一定相等,并且由于在第一次比较过程中是到x和y的位置上才不相等的,那么也就是说str2中从k位置到y的前一个位置和str1中从k到x前一个位置是相同的。即三条红线对应的内容是完全相同的,那么就违背了我们推导的前提。所以我们认为在i到j-1的区间中是不可能完全匹配str2的。
计算最长前缀和最长后缀
数学归纳法,计算第i个位置上的最长前缀和最长后缀的时候,只可能是i-1位置时的最长前缀和最长后缀+1,否则我们就可以反推计算i-1的最长前缀和最长后缀是有错误的。
i-1位置和最长前缀首次匹配成功
我们比对最长前缀的下一个元素与i-1位置的元素看看是否相等,上图中两者相等,因此i位置的最长前缀就是i-1位置最长前缀(4)+1。
i-1位置和最长前缀中途匹配成功
在以上情况下,我们发现i-1位置(a)和n位置(i-1位置的最长前缀的下一个位置)上元素©不相等,这时我们让n向前跳,跳到n位置的最长前缀的下一个位置。如图所示
因此我们比较i-1位置(a)和位置n的元素(a),因为两者相等所以此时i位置的最长前缀为c元素的最长前缀加1。
i-1位置和最长前缀匹配不成功
在以上情况下,我们发现i-1位置(a)和n位置(i-1位置的最长前缀的下一个位置)上元素©不相等,这时我们让n向前跳,跳到n位置的最长前缀的下一个位置。如图所示
因此我们比较i-1位置(a)和位置n的元素(a),因为两者不相等所以n继续向前跳,跳到了0位置。
这时我们可以确定k元素的最长前缀为0。
应用
应用1:给你一个字符串,让你向该串的后面添加元素,使其包含两个原始串,并且长度最小。
我们的想法是首先计算该原始串的各个元素的最长前缀和最长后缀,并且计算数组之外的那个元素的最长前缀和最长后缀,这样我们就可以复用最长后缀,只需要将最长前缀的下一个元素到最后一个元素添上就可以了。
为什么要计算数组中每个元素的最长前缀和最长后缀,我只需要用数组之外的那个就行了?
因为没有前面的,计算数组之外的那个需要O(N^2),如果依次计算就是O(N)的。
问题2:如何判断一个树中是否完全包含另一棵子树,要求结构和数据完全相同。
先把一棵树序列化,变成string。即T1->S1,T2->S2,如果S2是S1的子串,那么T2一定是T1的子树。
问题3:你怎么确定一个字符串不是另一个字符串重复得到的。
示例代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;
vector<int> getNextVector(string str2)
{
if (str2.size() == 1)
{
return vector<int>{-1};
}
vector<int>next;
next.resize(str2.size());
next[0] = -1;
next[1] = 0;
int pos = 2;
int cn = 0;
while (pos < next.size())
{
if (str2[pos - 1] == str2[cn])
{
next[pos++] = ++cn;
}
else if (cn > 0)
{
cn = next[cn];
}
else
{
next[pos++] = 0;
}
}
return next;
}
/*
数学归纳法,计算第i个位置上的最长前缀和最长后缀的时候,
只可能是i-1位置时的最长前缀和最长后缀+1,否则我们就可以反推计算i+1的最长前缀和最长后缀是有错误的。
*/
int getIndexOf(string str1, string str2)
{
if (str2.size() < 1 || str1.size() < str2.size())
{
return -1;
}
int srcIndex = 0;
int desIndex = 0;
vector<int> next = getNextVector(str2);
while (srcIndex < str1.size() && desIndex < str2.size())
{
if (str1[srcIndex] == str2[desIndex])
{
srcIndex++;
desIndex++;
}
else
{
// 实际上如果不是-1就要向前跳,0位置认为规定的-1
if (next[desIndex] != -1)
{
desIndex = next[desIndex];
}
else //乙不能再往前跳了,位于开头的位置了
{
srcIndex++;
}
}
}
return desIndex == str2.size() ? srcIndex-desIndex: -1;
}
int main()
{
string str = "abcabcababaccc";
string match = "ababa";
cout << getIndexOf(str, match) << endl;
system("pause");
return 0;
};