字符串查找
string对象的读写
string对象的读写可以通过两个方式:
- 通过cin从标准输入中读取,cin忽略开题所有的空白字符,读取字符直至再次遇到空白字符,读取终止。
- 用getline读取整行文本,getline函数接受两个参数:一个输入流对象和一个string对象。
getline函数从输入流的下一行读取,并保存读取的内容到string中,但不包括换行符。和输入操作符不一样的是,getline并不忽略开头的换行符。即便它是输入的第一个字符,getline也将停止读入并返回。如果第一个字符就是换行符,则string参数将被置为空string。
string自带的成员函数find
参考:http://cplusplus.com/reference/string/string/find/
C++中的find函数使用的是朴素匹配算法,最坏时间复杂度 O(MN),空间复杂度 O(1)。
string中find()返回值是字母在母串中的位置(下标记录),如果没有找到,那么会返回一个特别的标记npos。(返回值可以看成是一个int型的数)
使用示例
- 基础用法
#include<bits/stdc++.h>
using namespace std;
int main()
{
find函数返回类型 size_type
string s("1a2b3c4d5e6f7jkg8h9i1a2b3c4d5e6f7g8ha9i");
string flag="jk";
//find 函数 返回jk 在s 中的下标位置
//string::size_type position = s.find(flag);
//从字符串s 下标5开始,查找字符串b ,返回b 在s 中的下标
string::size_type position = s.find(flag,5);
if (position != s.npos) //如果没找到,返回一个特别的标志c++中用npos表示,我这里npos取值是4294967295,
{
printf("position is : %d\n" ,position);
}
else
{
printf("Not found the flag\n");
}
}
- 查找s 中flag 出现的所有位置。
flag="a";
position=0;
int i=1;
while((position=s.find(flag,position))!=string::npos)
{
cout<<"position "<<i<<" : "<<position<<endl;
position++;
i++;
}
- 反向查找子串在母串中出现的位置,通常我们可以这样来使用,当正向查找与反向查找得到的位置不相同说明子串不唯一。
//反向查找,flag 在s 中最后出现的位置
flag="3";
position=s.rfind (flag);
printf("s.rfind (flag) :%d\n",position);
例题1:
给出一个字符串,串中会出现有人名,找到一个只有一个人名的字符串。
#include <bits/stdc++.h>
using namespace std;
vector<string> s;
int main()
{
s.push_back("Danil");
s.push_back("Olya");
s.push_back("Slava");
s.push_back("Ann");
s.push_back("Nikita");///建立动态数组
string a;
cin>>a;
int res = 0;
for(int i = 0; i < 5; i++)
{
if(a.find(s[i]) != a.npos)
{
res++;
if(a.rfind(s[i]) != a.find(s[i]))///一个字符中出现多个一样的名字
{
res++;
}
}
}
if(res == 1)
{
cout<<"YES"<<endl;
}
else
{
cout<<"NO"<<endl;
}
return 0;
}
例题2:
你有n个字符串。 每个字符串由小写英文字母组成。 重新排序给定的字符串,使得对于每个字符串,在它之前的所有字符串都是它的子串。
#include<string>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
bool cmp(string a, string b)
{
//按长度排序,长度相同时按字典序排序
if (a.length() == b.length())
return a < b;
return a.length() < b.length();
}
int main()
{
int n;
string s[111];
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
cin >> s[i];
}
sort(s, s + n, cmp);
int flag = 1;
for (int i = 1; i < n; i++)
{
if (s[i].find(s[i-1]) == string::npos)
{
flag = 0;
break;
}
}
if (flag)
{
cout << "YES" << endl;
for (int i = 0; i < n; i++)
{
cout << s[i] << endl;
}
}
else
{
cout << "NO" << endl;
}
return 0;
}
标准库中的函数search
参考:http://www.cplusplus.com/reference/algorithm/search/?kw=search
//默认查找
ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,
ForwardIterator2 first2, ForwardIterator2 last2);
//自定义相等方式的比较
ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,
ForwardIterator2 first2, ForwardIterator2 last2,
BinaryPredicate pred);
使用示例
例题1 :忽略大小写查找子串出现次数
#include<bits/stdc++.h>
using namespace std;
//忽略大小写的字符比较
bool cmp(char c1,char c2){
return toupper(c1)==toupper(c2);
}
int main(){
string txt="Smith, where Jones had had \"had\", had had \"had had\"."" \"Had had\" had had the examiners\' approval.";
cout<<txt<<endl;
string pat="had had";
int count=0;
string::iterator start=txt.begin();
while((start=search(start,txt.end(),pat.begin(),pat.end(),cmp))!=txt.end()){
count++;
start++;
}
cout<<"\n\""<<pat<<"\""<<" was found "<<count<<" times";
return 0;
}
补充:
find_end() 会在一个序列中查找最后一个和另一个元素段匹配的匹配项,也可以看作在一个元素序列中查找子序列的最后一个匹配项。
这个算法会返回一个指向子序列的最后一个匹配项的第一个元素的迭代器,如果没有发现返回txt.end()。
下面是一个示例:
例题1 :忽略大小写查找子串出现次数
#include<bits/stdc++.h>
using namespace std;
//忽略大小写的字符比较
bool cmp(char c1,char c2){
return toupper(c1)==toupper(c2);
}
int main(){
string txt="Smith, where Jones had had \"had\", had had \"had had\"."" \"Had had\" had had the examiners\' approval.";
cout<<txt<<endl;
string pat="had had";
int count=0;
string::iterator end=txt.end();
string::iterator tmp=end;
while((tmp=find_end(txt.begin(),end,pat.begin(),pat.end(),cmp))!=end){
count++;
end=tmp;
}
cout<<"\n\""<<pat<<"\""<<" was found "<<count<<" times";
return 0;
}
暴力匹配
时间复杂度O(MN),空间复杂度O(1)
完整代码
int search(string txt,string pat){
int M=pat.size();
int N=txt.size();
for(int i=0;i<=N-M;i++){
int j=0;
for(;j<M;j++){
if(pat[j]!=txt[i+j])
break;
}
//pat全都匹配了
if(j==M)
return i;
}
//txt中不存在pat子串
return -1;
}
KMP算法
来自 https://segmentfault.com/a/1190000008575379
参考KMP算法教程
先介绍如下概念:真前缀和真后缀。
“真前缀”指除了自身以外,一个字符串的全部头部组合;
“真后缀”指除了自身以外,一个字符串的全部尾部组合。
参考:字符串匹配的KMP算法
1.先看看在txt串中搜索pat串的过程
当空格与D不匹配是,你其实已经知道前面六个字符是“ABCDAB”。KMP算法的思想就是想法设法地利用这个已知信息,不要把“搜索位置”移回已经比较过的位置,而是继续把他向后移。
为了做到这一点,可以针对模式串,设置一个跳转数组int next[],这个数组怎么计算出来的,先不管,后面再看。
已知空格与D不匹配时,前面六个字符“ABCDAB”是匹配的。根据跳转数组可知,不匹配处D的next为2,因此接下来从模式串下标为2的位置开始匹配
因为空格与C不匹配,C处的next值为0,因此接下来模式串从下标为0开始匹配
因为空格与A不匹配,A处的next值为-1,表示模式串的第一个字符就不匹配,那么直接往后移一位。
逐位比较,直到发现C与D不匹配。于是,下一步从下标为2的地方开始匹配。
逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成
2.next数组是如何求出来的
next数组的求解是基于“真前缀”和“真后缀”,即next[i]等于P[0]…P[i-1]最长的相同真前后缀的长度(暂时忽视i等于0时的情况,下面会有解释)。
来复习一下真前缀和真后缀。
“真前缀”指除了自身以外,一个字符串的全部头部组合;
“真后缀”指除了自身以外,一个字符串的全部尾部组合。
1.i=0,对于模式串的首字符,我们统一为next[0]=-1;
2.i=1,前面的字符串为A,其最长相同真前后缀长度为0,即next[1]=0;
3.i=2,前面的字符串为AB,其最长相同真前后缀长度为0,即next[2]=0;
4.i=3,前面的字符串为ABC,其最长相同真前后缀长度为0,即next[3]=0;
5.i=4,前面的字符串为ABCD,其最长相同真前后缀长度为0,即next[4]=0;
6.i=5,前面的字符串为ABCDA,其最长相同真前后缀为A,即next[5]=1;
7.i=6,前面的字符串为ABCDAB,其最长相同真前后缀为AB,即next[6]=2;
6.i=7,前面的字符串为ABCDABD,其最长相同真前后缀长度为0,即next[7]=0;
那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?
举个代表性的例子︰假如i= 6时不匹配,此时我们是知道其位置前的字符串为ABCDAB,仔细观察这个字符串,首尾都有一个AB,既然在i = 6处的D不匹配,我们为何不直接把i = 2处的C拿过来继续比较呢,因为都有一个AB啊,而这个AB就是ABCDAB的最长相同真前后缀,其长度2正好是跳转的下标位置。
有的读者可能存在疑问,若在i = 5时匹配失败,按照我讲解的思路,此时应该把i = 1处的字符拿过来继续比较,但是这两个位置的字符是一样的啊,都是B,既然一样,拿过来比较不就是无用功了么?
其实不是我讲解的有问题,也不是这个算法有问题,而是这个算法还未优化,关于这个问题在下面会详细说明,不过建议读者不要在这里纠结,跳过这个,下面你自然会恍然大悟。
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;
next[i]=j;
}else
j=next[j];
}
return next;
}
一脸懵逼,是不是。。。上述代码就是用来求解模式串中每个位置的next[]值。
下面具体分析,我把代码分为两部分来讲:
(1) : i和j的作用是什么?
i和j就像是两个“指针”,一前一后,通过移动它们来找到最长的相同真前后缀。
(2) : if…else…语句里做了什么?
假设i和j的位置如上图,由next[i] = j得,也就是对于位置i来说,区段[0,i -1]的最长相同真前后缀分别是[0,j-1]和[i - j, i -1],即这两区段内容相同。
按照算法流程,
if (P[i]==P[j]),则i++; j++; next[i]= j;
若不等,则j = next[j],见下图:
next[j]代表[0,j-1]区段中最长相同真前后缀的长度。如图,用左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以else语句就是利用第一个椭圆和第四个椭圆内容相同来加快得next[j]代表[0,j-1]区段中最长相同真前后缀的长度。
细心的朋友会问if语句中j ==-1存在的意义是何?
第一,程序刚运行时,j是被初始为-1,直接进行P[1]==P[j]判断无疑
会边界溢出;
第二,else语句中j= next[j],j是不断后退的,若j在后退中被赋值为-1(也就是j = next[0]),在P[i]==P[j]判断也会边界溢出。
综上两点,其意义就是为了特殊边界判断。
KMP完整代码
3.完整代码
#include<bits/stdc++.h>
using namespace std;
/*
//未优化前的getNext
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;//j前移
next[i]=j;//更新next[i]
}else //j回退
j=next[j];
}
return next;
}
*/
//优化后的getNext
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;
//不相同,则i处的真前缀位置就是j
if(Pat[i]!=Pat[j])
next[i]=j;
else//既然相同,就继续往前找真前缀
next[i]=next[j];
}else
j=next[j];
}
return next;
}
//别人的
int KMP(string& txt,int start,string& pat){
vector<int> next=getNext(pat);
int i=start;
int j=0;
int N=txt.size();
int M=pat.size();
while(i<N&&j<M){//因为末尾'\0'的存在,所以不会越界
//pat的第一个字符不匹配或者txt[i]==pat[j];
if(j==-1||txt[i]==pat[j]){
i++;
j++;
}else
{//当前字符匹配失败,进行跳转
j=next[j];
}
}
if(j==M)//匹配成功
return i-j;
return -1;
}
//我的
int MyKMP(string& txt,int start,string& pat){
vector<int> next=getNext(pat);
int N=txt.size();
int M=pat.size();
for(int i=start;i<=N-M;i++){
int j=0;
while(j!=-1&&j<M&&i<N){
if(txt[i]!=pat[j]){
//j处字符不匹配,就与j处字符的最长相同前后缀长度处的字符去匹配
j=next[j];
}else{
j++;
i++;
}
}
//如果找到就返回首字符下标
if(j==M){
return i-M;
}
//如果没有找到,则i++,继续下一轮
}
//循环结束都没有找到
return -1;
}
int main(){
string txt="Smith, where Jones had had \"had\", had had \"had had\"."" \"Had had\" had had the examiners\' approval.";
cout<<txt<<endl;
string pat="had had";
int count=0;
int res=0;
//找出txt中所有的pat子串位置
while((res=KMP(txt,res,pat))!=-1){
count++;
//下次从res+1位置开始查找
res++;
cout<<res<<" ";
}
cout<<"\n\""<<pat<<"\""<<" was found "<<count<<" times";
return 0;
}
4.KMP优化
以3.2的表格为例(已复制在上方),若在i = 5时匹配失败,按照3.2的代码,此时应该把i = 1处的字符拿过来继续比较,
但是这两个位置的字符是一样的,都是8,既然一样,拿过来比较不就是无用功了么?
这我在3.2已经解释过,之所以会这样是因为KMP 还未优化。
那怎么改写就可以解决这个问题呢?很简单。
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;
//不相同,则i处的真前缀位置就是j
if(Pat[i]!=Pat[j])
next[i]=j;
else//既然相同,就继续往前找真前缀
next[i]=next[j];
}else
j=next[j];
}
return next;
}
labuladong解释KMP算法
伪代码框架
class KMP{
vector<int> dp;
string pat;
public:
KMP(string p):pat(p){
//通过pat构造dp数组
//需要O(M)时间
}
int search(string txt){
//借助dp数组去匹配txt
//需要O(N)时间
}
}
KMP 算法最关键的步骤就是构造这个状态转移图。 要确定状态转移的⾏为, 得明确两个变量, ⼀个是当前的匹配状态, 另⼀个是遇到的字符; 确定了这两个变量后, 就可以知道这个情况下应该转移到哪个状态。
下⾯看⼀下 KMP 算法根据这幅状态转移图匹配字符串 txt 的过程:
为了描述状态转移图, 我们定义⼀个⼆维 dp 数组, 它的含义如下:
dp[j][c]=next
0<=j<M,代表当前的状态
0<=c<256,代表遇到的字符(ASCII码)
0<=next<=M,代表下一个状态
dp[4]['A']=3表示:
当前状态4,遇到字符A,
pat则转移到状态3
dp[1]['B']=2表示:
当前状态1,遇到字符B,
pat应该转移到状态2
根据我们这个 dp 数组的定义和刚才状态转移的过程, 我们可以先写出 KMP算法的 search 函数代码:
int search(string txt){
int M=pat.size();
int N=txt.size();
//pat的初始状态为0
int j=0;
for(int i=0;i<N;i++){
//当前是状态j,遇到字符txt[i],
//pat应该转移到那个状态
j=dp[j][txt[i]];
//如果到达终止态,返回匹配开头的索引
if(j==M)
return i-M+1;
}
//没到达终止态,匹配失败
return -1;
}
构建状态转移图
回想刚才说的: 要确定状态转移的⾏为, 必须明确两个变量, ⼀个是当前的匹配状态, 另⼀个是遇到的字符, ⽽且我们已经根据这个逻辑确定了 dp数组的含义, 那么构造 dp 数组的框架就是这样:
这个 next 状态应该怎么求呢? 显然, 如果遇到的字符 c 和 pat[j] 匹配的话, 状态就应该向前推进⼀个, 也就是说 next = j + 1 , 我们不妨称这种情况为状态推进:
如果字符 c 和 pat[j] 不匹配的话, 状态就要回退(或者原地不动) , 我们不妨称这种情况为状态重启:
那么, 如何得知在哪个状态重启呢? 解答这个问题之前, 我们再定义⼀个名字: 影⼦状态(我编的名字) , ⽤变量 X 表⽰。 所谓影⼦状态, 就是和当前状态具有相同的前缀。 ⽐如下⾯这种情况:
当前状态 j = 4 , 其影⼦状态为 X = 2 , 它们都有相同的前缀 “AB”。 因为状态 X 和状态 j 存在相同的前缀, 所以当状态 j 准备进⾏状态重启的时候(遇到的字符 c 和 pat[j] 不匹配) , 可以通过 X 的状态转移图来获得最近的重启位置。
为什么这样可以呢? 因为: 既然 j 这边已经确定字符 “A” ⽆法推进状态,只能回退, ⽽且 KMP 就是要尽可能少的回退, 以免多余的计算。 那么 j就可以去问问和⾃⼰具有相同前缀的 X , 如果 X 遇⻅ “A” 可以进⾏「状态推进」 , 那就转移过去, 因为这样回退最少。
当然, 如果遇到的字符是 “B”, 状态 X 也不能进⾏「状态推进」 , 只能回退, j 只要跟着 X 指引的⽅向回退就⾏了:
你也许会问, 这个 X 怎么知道遇到字符 “B” 要回退到状态 0 呢? 因为 X永远跟在 j 的⾝后, 状态 X 如何转移, 在之前就已经算出来了。 动态规划算法不就是利⽤过去的结果解决现在的问题吗?
这样, 我们就细化⼀下刚才的框架代码:
int X # 影⼦状态
for 0 <= j < M:
for 0 <= c < 256:
if c == pat[j]:
# 状态推进
dp[j][c] = j + 1
else:
# 状态重启
# 委托 X 计算重启位置
dp[j][c] = dp[X][c]
KMP完整代码
完整代码实现
关键在于看懂影子状态X怎么求的
其中的原理⾮常微妙, 注意代码中 for 循环的变量初始值, 可以这样理解:
X的状态转移理解为在pat[1…end]中匹配pat, 状态X 总是落后状态 j ⼀个状态, 与 j 具有最⻓的相同前缀。
所以我把 X⽐喻为影⼦状态, 似乎也有⼀点贴切。
class KMP{
vector<vector<int>> dp;
string pat;
public:
KMP(string p):pat(p){
//通过pat构造dp数组
//需要O(M)时间
int M=pat.size();
//dp[状态][字符]=下一个状态
dp=vector<vector<int>>(M,vector<int>(256));
//base case
dp[0][pat[0]]=1;
//影子状态X初始化为0
int X=0;
//当前状态j从1开始
for(int j=1;j<M;j++){
for(int c=0;c<256;c++){
if(pat[j]==c)
dp[j][c]=j+1;
else
dp[j][c]=dp[X][c];
}
//更新影子状态的值
//这里可以看出:影子状态X的更新可以理解为在pat[1...end]中匹配pat
X=dp[X][pat[j]];
}
}
int search(string txt){
//借助dp数组去匹配txt
//需要O(N)时间
int M=pat.size();
int N=txt.size();
//pat的初始状态为0
int j=0;
for(int i=0;i<N;i++){
//当前是状态j,遇到字符txt[i],
//pat应该转移到那个状态
j=dp[j][txt[i]];
//如果到达终止态,返回匹配开头的索引
if(j==M)
return i-M+1;
}
//没到达终止态,匹配失败
return -1;
}
};
KMP算法的时间复杂度O(N),空间复杂度为 O(256M) = O(M)。如果按照渐进复杂度的估计,我们自然是应该使用KMP算法了。
测试效果:
#include<bits/stdc++.h>
using namespace std;
class KMP{
vector<vector<int>> dp;
string pat;
public:
KMP(string p):pat(p){
//通过pat构造dp数组
//需要O(M)时间
int M=pat.size();
//dp[状态][字符]=下一个状态
dp=vector<vector<int>>(M,vector<int>(256));
//base case
dp[0][pat[0]]=1;
//影子状态X初始化为0
int X=0;
//当前状态j从1开始
for(int j=1;j<M;j++){
for(int c=0;c<256;c++){
if(pat[j]==c)
dp[j][c]=j+1;
else
dp[j][c]=dp[X][c];
}
//更新影子状态的值
//这里可以看出:影子状态X的更新可以理解为在pat[1...end]中匹配pat
X=dp[X][pat[j]];
}
}
//利用pat生成的dp数组,从txt中start下标开始查找pat子串的位置并返回
int search(string txt,int start){
//借助dp数组去匹配txt
//需要O(N)时间
int M=pat.size();
int N=txt.size();
//pat的初始状态为0
int j=0;
for(int i=start;i<N;i++){
//当前是状态j,遇到字符txt[i],
//pat应该转移到那个状态
j=dp[j][txt[i]];
//如果到达终止态,返回匹配开头的索引
if(j==M)
return i-M+1;
}
//没到达终止态,匹配失败
return -1;
}
};
int main(){
string txt="121231123";
string pat="1123";
KMP kmp(pat);
int res=kmp.search(txt,0);
if(res==-1){
cout<<"not find"<<endl;
}else{
cout<<"find it at: "<<res<<endl;
}
return 0;
}
那么为什么C++中的find函数不使用KMP算法呢?
我个人认为是以下几点原因:
- 在大多数常见场景下,需要匹配的字符是比较短的,朴素的蛮力匹配算法速度以及很快了。
- 在使用KMP算法的时候,需要进行预处理,初始化的时间开销、额外的空间开销在很多场景下反而会更消耗资源。
find函数实际不是使用普通的朴素匹配算法,内部也进行了优化,因为STL库有很多的实现版本,这里就不贴代码了。
总的来说,在常用场景下find函数使用的朴素算法就已经能满足需要了,没有必要使用KMP算法,在特殊应用场景下,可以手撸一个KMP算法来代替。
所以,不仅是C++,其他编程语言的string.find函数都没有使用KMP算法。