AI (人工智能不还是人写出来的吗)
本题要求你实现一个简易版的 AI 英文问答程序,规则是:
- 无论用户说什么,首先把对方说的话在一行中原样打印出来;
- 消除原文中多余空格:把相邻单词间的多个空格换成 1 个空格,把行首尾的空格全部删掉,把标点符号前面的空格删掉;
- 把原文中所有大写英文字母变成小写,除了 I;
- 把原文中所有独立的 I 和 me 换成 you;
- 把原文中所有的问号 ? 换成惊叹号 !;
- 把原文中所有独立的 can you 换成 I can —— 这里“独立”是指被空格或标点符号分隔开的单词;
- 在一行中输出替换后的句子作为 AI 的回答。
这是一道又老师带着写得一道天梯赛的题目,课堂上听了之后回来自己重新整理思路发现错误还是很多的,有了解题的思路但也要细心地去敲代码,每一个括号,if else都要想清楚它的范围作用域,下面就讲讲自己的分析过程及遇到的各种问题
首先按照老师讲的做题的思路,先想好大的框架,输入n个字符串(用string保存),输出正确的格式(用一个函数实现),所以主函数其实只是两件简单的事,具体的操作再通过函数来实现
int main() { string s; int n; cin>>n; getchar(); //接收N的回车否则第一个string就结束了 for(int i=1;i<=n;i++) { getline(cin,s); cout<<s<<endl; cout<<"AI: "; function(s); } return 0; }
下面再来写function函数
如果我们直接在原来的S串上直接操作并直接输出比较复杂,每一个字符要经过很多次的检验可能会出现超时,所以我们不妨用一个新的数组存放处理的串再有选择地输出,因此我们先定义一个串string t;但是这样就出现问题了,因为我们给t分配一个放地址的空间却没有给它初始一个地址,它就只能放一个野地址了,后面我们在操作t[j]时是要用这个t的地址进行偏移,但是没有初始化连首地址都不知道要怎么操作呢,除非先随便给个字符给他个足够长的空间,所以还是定义一个字符数组吧,这个字符数组的长度应该开多大呢,我们看到题目中如果遇到I就会全部转换为you所以最坏的情况就是全部(1000)个全是I,长度就为3000,定义时多加一个放结尾符。然后再把S中的元素复制过来(当然没那么简单啦,复制过程中再进行判断)
void function(string s) { char t[3005]; int i,j; t[j]=a[i]; }
处理输入时前面的空格
方法:用一个空循环让指针指向s的第一个非空元素,再来存入T数组
for(i=0; s[i]!='\0'&&s[i]==' ';++i); //在for循环里重复定义了int x=0导致后面的全部没输出但是第一个代码又可以输出 //cout<<s[i]; 检验得确实到了第一个非零 //制作一个空循环,让i到达第一个非空的字符,结束时a[i]为字符
接着处理串中的连续空格与结尾的连续空格
方法:先看来连续空格的特点就是当前空格的前一个还是空格,如果当前元素为空格且前一个也是空格就跳过空格不放进t数组并且进行下一个s串的判断,结尾的连续空格就被保留了一个存进T数组了
if(s[i]==' '&&s[i-1]==' ') { //处理后面和中间的空格ABD____WE 判断是否为连续的空格,如果用I+1==' '的话 ,但后面还留有一个空格 ++i; continue; //跳出本次的循环继续下一个元素而不是跳出整个循环,有连续空格时会死循环 }
处理问号
方法:先判断问号,是的话就改成!存进t数组并进行下一个s串判断,注意i的值要进行更改,这是如果想着快捷直接在判断时i++就会出问题,因为如果判断了不是问号就直接跳过这个元素,把下一个元素存进t数组了
if(s[i]=='?') { //如果当前元素是?就吧它转化为!存入T数组 t[j++]='!'; ++i; //要先判断了才可以i++否则就算不是?在判断后就已经自动跳到下个元素,当前的元素就没有处理 continue; }
处理非I大写换小写
方法:如果不是I的大写就换成小写,当然这里可以先判断大小写 1.islower(ch) 2.ch>='A'&&ch<='Z' 再转换ch+='a'-'A' ,也可以直接用函数toslower(),加函数头cstdio
if(s[i]!='I') //s[j] { //如果当前的元素不是I就判断大小写并转换 t[j]=tolower(s[i]); //直接用函数判断加转换 ++i; ++j; //如果当前元素!=I且是大写的话? continue; }
把处理了空格,?,大写即一些其他的元素存进T数组,由于这个循环是以s[i]!='\0'为条件的所以数组T中是没有字符串的结尾符,手动在循环结束后加一个,由于前面存放元素时已经为下一个元素开了一个位子所以就直接 t[j]='\0'
处理到一大半先输出数组试试吧
//cout<<t<<endl; char数组
//cout<<t.data(); string类
存入T数组的是前面没有连续空格,问号转换成!,元素间没有来连续空格,结尾至少有一个空格及其他的字母(I)数字标点之类的元素,我们在输出T数组的时候再判断筛选输出
while(t[j]!='\0')
{cout<<t[j]<<endl;}
of couse 没辣么简单~~
转换输出独立I为you
方法:先判断是否为I再判断独立( 这里“独立”是指被空格或标点符号分隔开的单词),判断前一个后一个是否为空格或者标点(用一个independent(ch)函数),if中的t[j-1]和t[j+1]会越界吗?如果输入的第一个字符就是'I',t[j-1]会越界;但是t[j+1]不会越界,其边界情况是t[j+1]为'\0',如果是就输出you并且指针指向下一个T数组元素跳出本次的循环进行下一个T元素的判断
if(t[j]=='I'&& (j==0||independent(t[j-1])) && independent(t[j+1])) { //把I转换,考虑前一个后一个是否为独立,前一个注意越界用J==0判断||,后一个最坏情况就是下一个为|0 cout<<"you"; ++j; continue; }
bool independent(char ch) { //判断是否为空格或标点即非数字字母和I(小写字母 I 数字 空格 标点 |0) ch=tolower(ch); //先转换为小写比较方便而且输出也是小写 if(ch>='0'&&ch<='9' || ch>='a'&&ch<='z' || ch=='I') //前面装S时没有对I处理,输出T时独立为后面是空格或标点 return false; else return true; }
转换输出me->you
方法:判断me再判断是否独立,如果输入的第一个字符就是'm',t[j-1]会越界;但是t[j+1]不会越界,因为进行独立判断的前提是前面两个字符是me其边界情况是t[j+2]为'\0',如果是就输出you,指针指向下下个并跳出对于这个元素的判断进行一个T元素的判断
if(t[j]=='m'&&t[j+1]=='e'&& (j==0||independent(t[j-1])) && independent(t[j+2])) { //把me转换,判断前一个和后两个是否独立,后两个最坏就是下一个为|0但能判断独立的前提是有me cout<<"you"; j+=2; //J往后移两个 continue; }
处理标点前的空格
方法:如果当前元素为空格且下一个为标点(用ispunctuation(ch)函数)就跳过空格,指针后移并跳出对于这个元素的判断进行一个T元素的判断
if(t[j]==' '&& ispunctuation(t[j+1])) { //处理标点前的空格abv .dc ++j; continue; }
bool ispunctuation(char a) { //判断后一个是否为标点符号!(字母 数字 空格 I) a=tolower(a); //先转换为小写比较方便而且输出也是小写 if(a>='0'&&a<='9' || a>='a'&&a<='z' || a=='I' || a==' ') //前面装S时没有对I处理,输出T时独立为后面是空格或标点 return false; else return true; }
转换输出I can
方法:如果当前的7个元素是can you就输出I can(为保证函数的封装性就用函数iscanyou(ch [],j)判断,开始是想着有没有可以复制从M到N的函数呢,在遇到c时就把7个元素放进一个串或数组再用strcmp来比较字符串,于是看到了一些比较字符数组及字符串的方法,c++基础不牢的看了会有一些收获
char a[] = "aaa",b[]="aaa"; string A = "AAA", B = "AAA"; cout <<"*a和*b的值分别是:" <<*a << "," << *b << endl; a,a cout <<"*“aaa”的值是:"<< *"aaa" << endl; a cout <<"利用 == 比较a,b两个字符串,结果是(相等为1,不等为0):" <<(a==b) << endl; 0 比较的是首地址 cout << "利用strcmp()比较a,b两个字符串,结果是(相等为0,不等非0):"<<strcmp(a,b) << endl; 0 cout << "利用 == 比较A,B两个string,结果是(相等为1,不等为0):"<<(A==B) << endl; 1 cout << "利用compare()比较A,B两个string,结果是(相等为0,不等非0):" << A.compare(B) << endl; 0
发现好像没有这样的函数,就想着说用这种方法,但是进入了死循环,这样的话如果C后面没有6个字符长度就会死循环
int flag=j; char b[7]; for(int k=0;k<7;k++) b[k]=t[j++]; j=flag; if(!strcmp(b,"can you") &&(j==0||independent(t[j-1])) &&independent(t[j+7])) { cout<<"I can"; j+=7; continue; }
处理删除结尾剩余的空格
方法:如果当前元素为空格且下一个为'\0'就跳过,输出就结束啦,但是这里我有个疑问就是为什么还要用continue呢,不是都已经结束了么就不需要进行下一轮的判断呀,但后来想了想如果不跳出循环就会执行下一句输出T[j],输出结尾符了,但是在PTA上也是过的了的,上网看了一下也没有特别准确的解释
if(s[j]==' '&&s[j+1]=='\0') {//处理结尾剩余得一个空格 ++j; continue; }
结束~~
总的过程如下
#include <iostream> #include <cstring> #include <cstdio> using namespace std; bool independent(char ch) { //判断是否为空格或标点即非数字字母和I(小写字母 I 数字 空格 标点 |0) ch=tolower(ch); //先转换为小写比较方便而且输出也是小写 if(ch>='0'&&ch<='9' || ch>='a'&&ch<='z' || ch=='I') //前面装S时没有对I处理,输出T时独立为后面是空格或标点 return false; else return true; } bool ispunctuation(char a) { //判断后一个是否为标点符号!(字母 数字 空格 I) a=tolower(a); //先转换为小写比较方便而且输出也是小写 if(a>='0'&&a<='9' || a>='a'&&a<='z' || a=='I' || a==' ') //前面装S时没有对I处理,输出T时独立为后面是空格或标点 return false; else return true; } bool iscanyou(char t[],int j) { if(t[j]=='c'&&t[j+1]=='a'&&t[j+2]=='n'&&t[j+3]==' '&&t[j+4]=='y'&&t[j+5]=='o'&&t[j+6]=='u') { if((j==0||independent(t[j-1]) )&&independent(t[j+7])) return true; } else //这个要放在外面,因为不是can you时判断就是错误了 return false; } void function(string s) {//处理串并输出 int j=0,i; char t[3001]; //定义一个字符数组装原来的串来进行操作与比较 for(i=0; s[i]!='\0'&&s[i]==' ';++i); ///在for循环里重复定义了int x=0导致后面的全部没输出但是第一个代码又可以输出 //cout<<s[i]; 检验得确实到了第一个非零 //制作一个空循环,让i到达第一个非空的字符,结束时a[i]为字符 while(s[i]!='\0') { // 暂时先处理连续空格和后面空格处理剩下一个 ,处理?,小写换大写 if(s[i]==' '&&s[i-1]==' ') { //处理后面和中间的空格ABD____WE 判断是否为连续的空格,如果用I+1==' '的话 ,但后面还留有一个空格 ++i; continue; //跳出本次的循环继续下一个元素而不是跳出整个循环,有连续空格时会死循环 } if(s[i]=='?') { //如果当前元素是?就吧它转化为!存入T数组 t[j++]='!'; ++i; //要先判断了才可以i++否则就算不是?在判断后就已经自动跳到下个元素,当前的元素就没有处理 continue; } if(s[i]!='I') //s[j] { //如果当前的元素不是I就判断大小写并转换 t[j]=tolower(s[i]); //直接用函数判断加转换 ++i; ++j; //如果当前元素!=I且是大写的话? continue; } t[j++]=s[i++]; } t[j]='\0'; //最后到|0前一个会停下所以不会把S的|0也复制过去,而且前面的J已将加了1 //cout<<t<<endl; cout<<t.data() string类 j=0; while(t[j]!='\0') { //输出 if(t[j]=='I'&& (j==0||independent(t[j-1])) && independent(t[j+1])) { //把I转换,考虑前一个后一个是否为独立,前一个注意越界用J==0判断||,后一个最坏情况就是下一个为|0 cout<<"you"; ++j; continue; } if(t[j]=='m'&&t[j+1]=='e'&& (j==0||independent(t[j-1])) && independent(t[j+2])) { //把me转换,判断前一个和后两个是否独立,后两个最坏就是下一个为|0但能判断独立的前提是有me cout<<"you"; j+=2; //J往后移两个 continue; } if(t[j]==' '&& ispunctuation(t[j+1])) { //处理标点前的空格abv .dc ++j; continue; } if(iscanyou(t,j)) { cout<<"I can"; j+=7; continue; } /*int flag=j; char b[7]; for(int k=0;k<7;k++) b[k]=t[j++]; j=flag; if(!strcmp(b,"can you") &&(j==0||independent(t[j-1])) &&independent(t[j+7])) { cout<<"I can"; j+=7; continue;这样的话如果C后面没有6个字符长度就会死循环 }*/ if(s[j]==' '&&s[j+1]=='\0') {//处理结尾剩余得一个空格 ++j; continue; } cout<<t[j++]; } cout<<endl; } int main() { string s; int n; cin>>n; getchar(); //接收N的回车否则第一个string就结束了 for(int i=1;i<=n;i++) { getline(cin,s); cout<<s<<endl; cout<<"AI: "; function(s); } return 0; }
这次的收获我觉得最重要的是思想,先想好一个大的框架就是先写main()函数,具体的操作处理用函数接口实现,然后在主函数前(不用函数声明)或者后面增加具体的函数内容,在函数内部最好只对那一层的数据进行操作就不要对数据,如果需要操作更具体的数据就可以再用函数来进行(函数内嵌函数)。