问题:
手机上的按键每个数字对应多个字母,比如2,对应“abc”。若按下一串数字,长度为N,要求把所有可能的字母序列输出出来。比如“23”,就可能对应ad,ae,af,bd,be,bf,cd,ce,cf。
解法一:
刚接触这个问题,感觉这非常简单。事实也是这样,只要你对递归足够熟的话,很容易给出一个递归算法。但是有一点要注意,有人因为每个数字对应三个字符,想从后往前用两次迭代的方式是行不通的。我当场没答出来的原因就是纠结于这个方法,迟迟没有给出结果。其实采用将前面的字符序列缓存的方式,很容易就给出结果了,以下是算法:
char g_telephone[10][3];
void printX(std::string strPrefix, char* source, int nLen, int x )
{
std::string strTemp ;
if ( x <= nLen -1 )
{
int cTemp = source[x] - 48 ;
if ( cTemp <= 9 && cTemp >= 0 )
{
strTemp+= strPrefix + g_telephone[cTemp][0] ;
printX(strTemp, source, nLen, x+1) ;
strTemp+= strPrefix + g_telephone[cTemp][1] ;
printX(strTemp, source, nLen, x+1) ;
strTemp+= strPrefix + g_telephone[cTemp][2] ;
printX(strTemp, source, nLen, x+1) ;
}
}
else
{
std::cout<< strPrefix.c_str() << std::endl ;
}
}
int main(int argc, char** argv)
{
//input, for simplicity, we only use it directly
char source[] = "1234567" ;
//length of the string, except '/0'
int size = sizeof(source) / sizeof(char) - 1;
printX( std::string(""), source, size, 0) ;
}
时间复杂度:考虑到函数迭代调用的压栈出栈只会增加复杂度的系数,复杂度还是由产生实际作用的打印输出语句决定;对于长度为N的输入串,肯定对应3^N的输出,所以复杂度为O(3^N)。
空间复杂度:考虑迭代调用最深处的内存使用情况,每次迭代除了3个参数还有函数的返回地址,所以有16个字节;每层有一个strTemp的临时变量,长度分别为1、2…N。所以,总的内存消耗量为O(16*N+N*(N+1)/2)。
其实,面试题到此就已经结束了。或许是对方对内存占用情况不敏感,或许因为我连递归的都没答出来就没继续问。从我实际的项目经验,在工程中能用非递归还是尽量用非递归;递归算法虽然简单易懂,容易想到,但往往是内存占用的瓶颈。因此,虽然面试早就结束,我还是在考虑非递归算法的实现。
非递归算法还是挺容易想到的,下面三个方法是在位数组上不断优化的结果,很可惜的是,我找到的最优解,虽然空间复杂度能降到O(N)的量级,但时间复杂度在O(4^N)的量级。
解法二:
考虑用两位来表示每个数字代表的三个字符,比如“2”对应于a,b,c;那么用00表示a,01表示b,10表示c,11废弃不用。这样,我们用一个长度为2N的位数组B[2N]来表示长度为N的字符串,位数组唯一确定该字符串。为讨论方便,给出数字到字符的映射表,如下:
char g_telephone[10][3] =
{ //1
{ '0','1','2'},
//2
{ 'a','b','c' },
//3
{ 'd','e','f' },
//4
{ 'g','h','i' },
//5
{ 'j','k','l' },
//6
{ 'm','n','o'},
//7
{ 'p','q','r' },
//8
{ 's','t','u' },
//9
{ 'v','w','x'},
//0
{ 'y','z',' ' }
};
长度为3的输入序列 “123”,如果对应的 B[1..6] = 000000, 则结果为 “0ad”;如果B[1..6] = 010101, 结果就是“0be”。
对于长度为N的输入,将对应的B[2N]从全为0遍历到全为1,除去“11”的特殊情况,剩下的每个B[2N]将对应一个输出。直接给出算法:
void NonRecursive1( char* source, int N )
{
BitSet B[2*N] = {0} ;
for (; B <= {11111...} ; B ++ )
{
std::string strTemp ;
int indexX = 0 ;
int indexY = 0 ;
for ( int i=0; i < N; i++)
{
//if 00,01,10
if ( B[2*i, 2*i+1] != {11} )
{
indexX = source[i] - 48 ;
indexY = B[2*i, 2*i+1] ;
strTemp += g_telephone[ indexX ][ indexY ] ;
}
//otherwise, jump to the next iteration
else
{
strTemp = "" ;
break ;
}
}
//output the results
if ( strTemp != "")
{
std::cout<< strTemp<<std::endl;
}
}
}
时间复杂度:总的遍历次数为2^2N=4^N;每次输出有N次遍历,所以复杂度为O(N*4^N)。有人会有这个疑问循环判断条件” B <= {11111...}”,每次判断都需要遍历每位判断是否为1的。事实上一个比较好的解决方法时,在2N位外面增加一个进位,只要判断该进位是否为1就可判断是否已经大于{1111….}了。
空间复杂度:空间占用分析很简单,如果不考虑临时变量,内存占用就只有位数组的长度,占用的字节数为2N/8,所以是O(N)。