室友去面試,問了一個字符串轉成數字的算法題,室友沒搞出來,我心想,這個不是很簡單的嗎?於是動手在紙上畫了畫代碼。畫完后,總感覺哪里不對,最后一個個挖掘,才發現,尼瑪,這到處都是坑啊~~~特此記錄一下中坑心路。
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:

