数字(int)转字符串和字符串转数字(int)
室友去面试,问了一个字符串转成数字的算法题,室友没搞出来,我心想,这个不是很简单的吗?于是动手在纸上画了画代码。画完后,总感觉哪里不对,最后一个个挖掘,才发现,尼玛,这到处都是坑啊~~~特此记录一下中坑心路。
1. 数字转字符串
首先看一下数字转成字符串。输入一个整型数字,写一个函数,返回整型数字对应的字符串形式。如:
输入:345
输出:"345"
这个问题第一思路应该是:对整型数字每次求最高位数字,如3,将其转换为对应字符 '3' ,然后将此整型值取下面的数,直到整型值为0,输出字符串。这个问题是,怎么求最高位的数字,可以用一个循环将number累除10,直到number小于10,即为最高位。解法如下:
- 代码A
char * int2Str(int nb){
char * str= new char[12];//整型最长11位
str[11]='\0';
unsigned int nindex=0;
if(nb<0){ //负数时的情况
str[0]='-';
nb*=-1; //转换为正数
++nindex; //第0位记录了符号'-',所以下移一位开始记录数字
//如果是正数和0的情况,直接从第0位开始记录数字
}else if(0==nb){ //0时的情况
str[0]='0';
str[1]='\0';
return str;
}
int tmpNum,len;
while(nb!=0){
len=1;
tmpNum=nb;
while(tmpNum>=10){
len*=10; //记录最高位的位度
tmpNum/=10; //循环累除10求最高位数字
}
str[nindex++]=tmpNum+'0';
nb=nb-tmpNum*len; //nb 减去最高位
if(nb==0){ //判断最后一位是否为 0 的情况
str[nindex++]='0';
break;
}
}
str[nindex]='\0'; //添加结束符号
return str;
}
在代码A中考虑了所有可能出现的情况,如:整数nb为0或负数时的情况,整数nb的最后一位为0时的情况(仅限此种思路的情况)。可以看到,算法主要的计算部分在两个while循环中,假设nb共有M位。则第一个循环需要循环M次,第二个循环总循环次数为:\((M-1)+(M-2)+...+3+2+1=M(M-1)/2\) ,总共的循环次数为\((M-1)M/2\),时间复杂度为:\(O(M^2)\)。当然,由于整型数字,最长才只有10位(不包含正负号),所以常数时间内即可解决。但有没有其他解法了呢?
如果我们从低位开始转换进字符数组里,而不是从高位开始,那不就能省掉第二个while循环了嘛!问题是,我们一开始并不知道整数nb有多少位!所以我们转换的低位放到字符数组str的哪里呢?这里有两种思路:
- 先求出整型数字nb的位数M,然后将转换后的字符直接存进str对应的位置。如代码B
- 将nb从低位开始转换的字符依次从前向后存入str,即str中存入的其实是要输出字符串的逆串,然后再将字符串给正过来,即求逆串。如代码C
- 代码B
char * int2StrB(int nb){
char * str= new char[12];//整型最长带上符号共11位
str[11]='\0';
unsigned int nindex=0;
if(nb<0){
str[0]='-';
nb*=-1; //转换为正数
nindex=1;
}else if(0==nb){
str[0]='0';
str[1]='\0';
return str;
}
unsigned int len=0; //记录nb的位数
int tmpNb=nb;
while(0!=tmpNb){
++len;
tmpNb/=10;
}
if(nindex==0)--len; //在str中定位最后一位数字应该在的位置
str[len+1]='\0'; //设置结束符号
while(0!=nb){
str[len--]=nb%10+'0';
nb/=10;
}
return str;
}
代码B中,首先一个循环求出整数nb(如果nb为负数,此时已经转换为对应的正数)总共位数M,需要循环M次,时间复杂度为\(O(M)\);第二个循环依然是依次遍历整数nb的每一位,时间复杂度依然为\(O(M)\),所以总时间复杂度为:\(O(M)+O(M)=O(M)\)。当然,其实M是有最大数限制的。
- 代码C
char * int2StrC(int nb){
char * str= new char[12];//整型最长带上符号共11位
str[11]='\0';
unsigned int nindex=0;
if(nb<0){
str[0]='-';
nb*=-1; //转换为正数
nindex=1;
}else if(0==nb){
str[0]='0';
str[1]='\0';
return str;
}
unsigned int nstar=nindex; //记录要逆序的初始位置
while(0!=nb){
str[nindex++]=nb%10+'0';
nb/=10;
}
str[nindex]='\0';
//字符串逆序
--nindex;
while(nstar<nindex){
char tmp=str[nstar];
str[nstar]=str[nindex];
str[nindex]=tmp;
++nstar;
--nindex;
}
return str;
}
代码C中也有两个循环,第一个循环完成将整数nb从低位到高位逆序转换进str中,需要时间复杂度为\(O(M)\);第二个循环将对应数字的部分进行逆序,时间复杂度为\(O(M/2)=O(M)\),所以总时间复杂度也为:\(O(M)\)。
至于B和C哪个更好,我是建议用B的,简洁明了。至于谁更快写,肯定都比A快,其次由于M是一常数,自然(M+M)>(M+M/2)的,但是在C中比B中的循环多出三条赋值操作,因为M不会大于11,所以,这个谁更好,就难说了~~(不知道这个分析的是否有问题~~~)
总结一下主要的坑:
- 负数时返回的字符串第一位要有 '-' 号,正数从人的角度上考虑不该加 '+'
- 转换的字符串在结尾要有 '\0',否则可能出错
字符串转数字
这个其实就是实现一下C库函数的atoi函数。当然我们有个简单的处理,就是使用C++的stringstream
类,代码如下:
- 代码D
#include <sstream>
typedef long long dlong;
enum Status{kInvalid=0,kValid};
int g_status=kValid; //合法输入
int str2IntA(const char* str){
g_status=kInvalid;
if(str==nullptr || *str=='\0')return 0; //指针不为空,字符串不为空
if(*str!='+' && *str!='-' && (*str<'0' && *str>'9'))return 0;
if(*str=='+' || *str=='-'){ //只有 "+" 和 "-" 或 '+'/'-'后跟的不是数字
if(*(str+1)=='\0' || (*(str+1)<'0' && *(str+1)>'9'))return 0;
}
stringstream stream(str);
dlong num=INT_MAX+10; //大于整型最大数,判断溢出
stream>>num;
if(num>INT_MAX || num<INT_MIN)return 0;
g_status=kValid;
return (int)num;
}
代码D中,首先我们要处理的一个问题就是,当输入的字符串非法时,返回什么?返回0吗?但返回0怎么区分这个0不是合法输入返回的呢,所以要引入一个全局变量,当输入非法的时候将全局变量置为非法,否则置为合法。注释中已经说明了可能的非法输入情况,这里就不在多说。但是stringstream流也能将下面的输入正确输出:
输入: "234dsdf"
输出:234
而我们知道,这其实输入一个非法的输入。当然我们可以更改代码进行遍历去判断这个,但其实如果在面试中,我觉得考官应该不是希望我们使用这个库函数的。而应该是去重新实现C版本的那个atoi函数。所以上代码E:
- 代码E
typedef long long dlong;
enum Status{kInvalid=0,kValid};
int g_status=kValid; //合法输入
int str2IntB(const char* str){
g_status=kInvalid;
if(str==nullptr || *str=='\0')return 0; //指针不为空,字符串不为空
char const* pstr=str;
int flag=1; //判断正负数,默认为正数
dlong num=0; //防溢出
if(*pstr=='-')flag=-1; //负数
else if(*pstr=='+') flag=1; //正数
else if(*pstr<'0' && *pstr>'9')return 0;//非法输入
else num=(*pstr-'0')*flag; //上来就是数字,为正数
++pstr;
if(*pstr!='\0'){ //防止字符串为"+",或 "-"的情况
while(*pstr!='\0'){ //循环求数字
if(*pstr>='0' && *pstr<='9'){
num=num*10+(*pstr-'0')*flag;
if((flag==1 && num>INT_MAX) ||
(flag==-1 && num<INT_MIN)) //防溢出
return 0;//溢出
++pstr;
}else return 0; //非法输入
}
if(*pstr=='\0') g_status=kValid; //直达字符串末尾,说明输入合法
}
return (int)num; //将 dlong 型强制转换为 int
}
至于思路,就是先去除非法输入部分,然后一个数字一个数字的转换为对应字符。注释里说的都已经很清楚了,所以就不多说了。
总结一下主要的坑:
- 怎么返回非法输入的结果,并且怎么进行区分
- 转换为整型后,可能会溢出,怎么进行溢出判断
- 非法输入:
- 空指针,空字符串
- 只有一个正负号,正负号不是出现在第一个位置
- 字符串中含有非正负号和数字的其他字符
3. 最后上代码文件
附录
- GitHub-Blog
- 关注微信订阅号 LomperWay: