考研复试系列——第十节 字符串问题
前言
关于字符串的问题可能是考试题目当中出现次数最多,涉及内容最广的内容了。主要有以下几个方面:字符串的匹配(暴力,KMP,
Sunday,DFS等等)。
求一个字符串的子串,字符串的反转,字符统计,字符查找。内容很多,但考试并不难,KMP可以不用,后缀树只在ACM中见过。另外别忘记还有C++强大的
STL给我们提供关于string的一系列操作,在本文最后,会总结下STL的string库的使用。
由例题出发
例题一
给你一个文本,里面有各种字符串标点啥的,问你是不是有符合 ab*de(*表示多个任意字符)的匹配。
思考:乍一看就是一个正则表达式的问题,那我们实际做时该怎么做呢?想想编译原理的知识,我们可以使用DFA来做,只要清楚它的状态转换图,我们就很容易
完成这道题目了。
首先做出NFA如下图所示:
(其中o表示除d和e的其他字符)
然后再求它的DFA,如下图所示:
注:上图没有验证是否为最简DFA,不过到这里已经可以编程实现了。当然如果化为最简DFA后对于编程实现来说代码会更简洁。
接下来就可以编码实现了:
#include<iostream>
#include<fstream>
using namespace std;
const int state_zero = 0, //定义四个状态
state_one = 1,
state_two = 2,
state_three = 3,
state_four = 4;
int now_state = 0;//记录当前状态
void checkStr(char *str)
{
now_state = 0;//初始化当前状态为0
int length = strlen(str);//计算字符串长度
for(int i=0;i<length;i++)
{
if(now_state == 0 && str[i] == 'a')
now_state = 1;
else if(now_state == 1 && str[i] == 'b')
now_state = 2;
else if(now_state == 2 && str[i] != 'd')
now_state = 2;
else if(now_state == 2 && str[i] == 'd')
now_state = 3;
else if(now_state == 3 && str[i] == 'd')
now_state = 3;
else if(now_state == 3 && str[i] != 'd' && str[i] != 'e')
now_state = 2;
else if(now_state == 3 && str[i] == 'e')
now_state = 4;
else if(now_state == 4 && str[i] == 'd')
now_state = 3;
else if(now_state == 4 && str[i] != 'd')
now_state = 2;
}
}
int main()
{
ifstream file("F:\\shangji\\string.in");
if(!file.is_open())//文件打开失败
exit(1);
char buff[100];
while(!file.eof())
{
file.getline(buff,100);
checkStr(buff);
if(now_state == 4)
cout<<buff<<" 匹配成功!"<<endl;
else
cout<<buff<<" 匹配失败!"<<endl;
}
file.close();
return 0;
}
例题二
一道简单的字符串匹配题目:给定两个字符串A和B判断B是否是A的子串。
我们考虑暴力搜索的方法,当你想不起来别的方法时,就用它吧,反正复试上机一般没有效率的限制。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string ss1,ss2;
cin>>ss1>>ss2;
int leng1 = ss1.length(),leng2 = ss2.length();//假设第一个字符串长
bool isSub = false;
int i,j;
for(i=0;i<=leng1-leng2;i++)
{
for(j=0;j<leng2;j++)
{
if(ss1[i+j] != ss2[j])
break;
}
if(j == leng2)
{
isSub = true;
break;
}
}
if(isSub)
cout<<ss2<<" 是"<<ss1<<" 的子串"<<endl;
else
cout<<ss2<<" 不是"<<ss1<<" 的子串"<<endl;
return 0;
}
当然也可以考虑一下效率更高的算法,比如我们在数据结构上学过的KMP。对于字符串的查找和这个实现的测率是一样的,其实也是一个字符串的匹配问题。
例题三
输入字符串带空格的分割字符串 (输入字符串,倒序输出,例如:输入 I come from China.输出 China from come I. (单
词不需倒序,只是句子倒了)
词不需倒序,只是句子倒了)
#include<iostream>
using namespace std;
char ss[20][100];
int main()
{
char str[100];
gets(str);
int leng = strlen(str);
str[leng] = ' ';//把最后也加一个空格,方便统一处理
str[leng+1] = '\0';
cout<<leng<<endl;
int j = 0,k = 0;
for(int i=0;i<=leng;i++)
{
char temp[100];
if(!isspace(*(str+i)))
{
temp[j++] = *(str+i);
}
else//遇到空格就处理一个子串
{
temp[j] = '\0';
j = 0;
strcpy(ss[k++],temp);//保存该子串
}
}
for(j=k-1;j>=0;j--)
cout<<ss[j]<<" ";
cout<<endl;
return 0;
}
string类
string作为STL中的一个重要组成部分还是很重要的,尤其是解题时可以带来很大的方便。
#include<iostream>
#include<string>
using namespace std;
/*
* string类的介绍,更详细的内容可以参照C++ primer
*/
int main()
{
//string的构造函数
char s[10] = "cumt csdn";//string str(const char *s) -----用c字符串s初始化
string str1(s);
cout<<str1<<endl;
string str2(4,'c');//string str(int n,char c) ------用n个字符初始化
cout<<str2<<endl;
//字符操作
char temp[10];
str1.copy(temp,4,5);//int copy(char *s,int n,int pos=0)将当前串中以pos开始的n个字符copy到字符数组s中
temp[4] = '\0';
cout<<temp<<endl;
//赋值操作
string str3 = str1 + str2; //字符串拼接操作
cout<<str3<<endl;
str3.assign(s);//string &assign(const char *s)用c类型字符串s赋值//还有string &assign(const string &s)
cout<<str3<<endl;
str3.assign(s,4);//string &assign(const char *s,int n)用c字符串开始的n个字符赋值给当前串
cout<<str3<<endl;
string str4;//string &assign(const string &s,int start,int n)
str4.assign(str1,5,4);//把字符串s中从start开始的n个字符赋值给当前串
cout<<str4<<endl;
//连接操作
str4.append(s);//string &append(const char *s)把c类型字符串连接到当前字符串串尾
str4.append(s,4);//string &append(const char *s,int n)把c类型字符串的前n个字符连接到当前字符串串尾
str4.append(str3,1,4);//string &append(const string &s,int pos,int n)把字符串s从pos开始的n个字符连接到当前字符串
//比较操作
/*
* 除了使用基本的逻辑符号比较外,还可以使用compare函数
* int compare(const char *s)
* int compare(int pos,int n,const char *s)
* int compare(int pos,int n,const char *s,int pos2)
* 对于string类型同上面是一样的
*/
//子串
cout<<str4.substr(4,4)<<endl;//string substr(int pos=0,int n=npos) const;返回pos开始的n个字符构成的字符串
//交换
str4.swap(str3);//void swap(string &s2) 交换当前串与s2的值
//查找
/*
* 查找操作的函数非常多,这里只例举下经常用到的
* int find(char c,int pos=0) const; //从pos开始查找字符c在当前字符串中的位置
* int find(const char *s,int pos) const; //从pos开始查找字符串s在当前串中的位置
* int find(const char *s,int pos,int n); //从pos开始查找字符串s的前n个字符在当前串中的位置
* int find(string &s,int pos=0) const; //作用同上
* 注:对于rfind是从后面开始查找,使用和find一样,find_first_of查找第一个满足条件的,还有
* find_last_of最后一个满足条件的,使用起来从语法上一样。
*/
return 0;
}
#include<iostream>
#include<string>
#include<vector>
using namespace std;
vector<string> svec;
void split(string str,char ch)
{
str += ch;//拓展字符串,方便后面统一操作
int size = str.length();
int i,pos;
for(i=0;i<size;i++)
{
pos=str.find(ch,i);
if(pos<size)
{
string s=str.substr(i,pos-i);
svec.push_back(s);
i=pos;
}
}
}
int main()
{
char ss[100];
gets(ss);
//在vc6中使用getline直接从控制台读入string需要两次回车
//所以这里先用gets函数读入再进一步转换为string
string str(ss);
split(ss,' ');
int size = svec.size();
for(int i=size-1;i>=0;i--)
cout<<svec[i]<<" ";
cout<<endl;
return 0;
}
后缀数组
后缀数组算是字符串处理中一个经典的数据解构了,对于很多问题可以很好的解决,大多数情况下直接使用后缀数组就OK了,不用使用后缀树。
所以这里也只说说后缀数组。(
本部分内容参考《算法问题实战策略》人民邮电出版社)
例如:哈利波特中的阿拉霍洞开这个词 “ alohomora ” (后缀9个)的后缀数组 A[ ] 如下 :
我们要做的就是生成这个表。代码如下:
#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
using namespace std;
struct SuffixComparator
{
const string& s;
SuffixComparator(const string &s):s(s) {}
bool operator() (int i,int j)
{
//不用s.substr()函数而使用strcmp()函数,则能够减少生成临时对象的开销
return strcmp(s.c_str() + i,s.c_str() + j) < 0;
}
};
vector<int> getSuffixArrayNaive(const string& s)
{
vector<int> perm;
for(int i=0;i<s.size();i++)
perm.push_back(i);
sort(perm.begin(),perm.end(),SuffixComparator(s));
return perm;
}
int main()
{
string str = "alohomora";
vector<int> array = getSuffixArrayNaive(str);
for(int i=0;i<array.size();i++)
cout<<i<<"\t"<<array[i]<<"\t"<<str.substr(array[i])<<endl;
return 0;
}
上面算法的思想很简单,开始时数组初始化为0,1,2,。。。8分别表示后缀(整数表示子串的开始位置),然后使用sort函数依照字典序列进行排序。
输出结果就是上表。
利用后缀树就可以求解多个字符串的最长公共子串,一个字符串中的最长重复子串等问题,在机试中貌似后缀数组的出现频率不高。