一、概述
输入一个字符串,判断其是否是一个合法的数字。
孤儿题目,边界条件极其多,需要不断submit来一个一个找出来,我错了二十多次,简直气死。
之前没用过DFA,DFA还是很好用的,Debug的时候很直观很容易,打补丁就好了。但是打补丁并不是一个好主意,会让代码可读性变得很差,不优美。因此如何设计状态,让状态与条件判断达到一个平衡,是一个技术活。
二、分析
1、我的方法
咱们先来看看,最初我想法很简单,因此状态转移图也很简单。
直到我发现.5时正确的,事情开始不对了:
然后开始疯狂打补丁:
为什么要这么多补丁呢?
主要是因为.5是正确的,3.是正确的,但是.自己是错误的,我原来就认为3.5这种是正确的,前面两者都是错误的——所以才弄错。另外,没有考虑空格也是一个原因——但是空格的状态是不能省略的。
然后呢?没考虑.3.5是错误的,e3e5是错误的,3.e5是正确的,3e3.5是错误的,3e3.是错误的——反正就一大堆错。最后弄出这么个丑陋的代码:
class Solution {
public:
bool isNumber(string s) {
bool res=true;
int flag=0;
int point=0;
int e=0;
for(int i=0;i<s.size();++i)
{
if(flag==0)
{
if((s[i]=='+'||s[i]=='-')&&i!=s.size()-1)
flag=1;
else if(s[i]>='0'&&s[i]<='9')
flag=2;
else if(s[i]=='.'&&i!=s.size()-1)
{
flag=6;
point=1;
}
else if(s[i]==' '&&i!=s.size()-1)
flag=0;
else
return false;
}
else if(flag==1)
{
if(s[i]>='0'&&s[i]<='9')
flag=2;
else if(s[i]=='.'&&point!=1&&i!=s.size()-1)
{
if(point==0)
{
flag=4;
point=1;
}
else
return false;
}
else
return false;
}
else if(flag==2)
{
if(s[i]>='0'&&s[i]<='9')
flag=2;
else if(s[i]=='e'&&i!=s.size()-1)
{
if(e==0)
{
flag=3;
e=1;
}
else
return false;
}
else if(s[i]=='.'&&point!=1&&e!=1)
{
if(point==0)
{
flag=4;
point=1;
}
else
return false;
}
else if(s[i]==' ')
flag=5;
else
return false;
}
else if(flag==3)
{
if((s[i]=='+'||s[i]=='-')&&i!=s.size()-1)
flag=1;
else if(s[i]>='0'&&s[i]<='9')
flag=2;
else
return false;
}
else if(flag==4)
{
if(e>0)
return false;
else if(s[i]>='0'&&s[i]<='9')
flag=2;
else if(s[i]==' ')
flag=5;
else if(s[i]=='e'&&i!=s.size()-1)
{
if(e==0)
{
flag=3;
e=1;
}
else
return false;
}
else
return false;
}
else if(flag==5)
{
if(s[i]==' ')
flag=5;
else
return false;
}
else if(flag==6)
{
if(s[i]>='0'&&s[i]<='9')
flag=2;
else
return false;
}
}
return true;
}
};
总结起来就是:
+和-不能在最后一个;
.可以在最后一个,但是.不能单独出现,.只能有一个,.不能在e后面;
空格只有在首尾出现才是合法的;
e只能有一个,e不能是最后一个。
这些题设中都没说!!!
2、较好的方法
我的方法是先判断当前状态,再判断该位是什么,然后转移状态;较好的方法是先判断该位是什么,再判断当前状态,然后转移状态。其实这本身没什么好不好,关键是人家对状态数和条件判断做了很好的平衡。
该方法首先将首尾的空格去掉,那么遇到空格就可以直接返回false,这样来讲,我的状态就只有6个,而他有八个状态,那么就比我多了两个状态,相应的,他的条件判断就简洁了很多。简而言之,就是用两个状态代替了我的e和point。状态转移如下图:
该方法与图片均来自该网址。
仔细观察可知,在状态s3,即e的后面,出现数字的时候,他并没有选择返回s2,而是走向s5,同理,出现+或-的时候,并没有返回s1,而是走向s4。然后,在s6的时候,出现数字,转向s7而不是s2。最后,他指定了四个状态为最后的正确状态。只有循环结束时状态为这四个才是正确的。
PS:s6转向s3仅在.之前有数字的时候才是正确的。下文中的flag就是为了这种情况。
代码如下:
class Solution {
public:
bool isNumber(string str) {
int state=0, flag=0; // flag to judge the special case "."
while(str[0]==' ') str.erase(0,1);//delete the prefix whitespace
while(str[str.length()-1]==' ') str.erase(str.length()-1, 1);//delete the suffix whitespace
for(int i=0; i<str.length(); i++){
if('0'<=str[i] && str[i]<='9'){
flag=1;
if(state<=2) state=2;
else state=(state<=5)?5:7;
}
else if('+'==str[i] || '-'==str[i]){
if(state==0 || state==3) state++;
else return false;
}
else if('.'==str[i]){
if(state<=2) state=6;
else return false;
}
else if('e'==str[i]){
if(flag&&(state==2 || state==6 || state==7)) state=3;
else return false;
}
else return false;
}
return (state==2 || state==5 || (flag&&state==6) || state==7);
}
};
三、总结
DFA原理不难,难的是把所有转移情况都想到,同时保证状态数和条件判断的平衡。
虽然debug很方便,但要是不注意,就会需要很多次试错才能AC,在有罚时的情况下是很吃亏的。因此一定要注意全面思考。