题目:输入数字n,按顺序打印出从1最大的n位十进制数。比如输入3,则打印出1,、2、3一直到最大的3位数及999.
跳进面试官的陷阱
这个题目看起来很简单。我们看到这个问题之后,最容易想到的办法是先求出最大的n位数,然后用一个循环从1开始逐个打印。于是我们很容易就能写出如下的代码:
void Print1ToMaxOfDigits(int n)
{
int number = 1;
int i = 0;
while(i++ < n){
number *= 10;
}
for(i = 1; i < number; ++i){
printf("%d\t", i);
}
}
初看之下好像没有什么问题,但是如果仔细分析这个问题,我们就能注意到面试官没有规定n的范围。当输入的n很大的时候,我们求最大的n位数是不是用整行(int)或者长整型(long long)都会溢出?也就是说我们需要考虑大数问题。这是面试官在这道题里设置的一个大陷阱。
在字符串上模拟数字加法的解法,绕过陷阱才能拿到offer
经过前面的分析,我们很自然地想到解决这个问题需要表达一个大数。最常用的也是最容易的方法是用字符串或者数字表达大数。接下来我们用字符串来解决大数问题。
用字符串表示数字的时候,最直观的方法就是字符串里每个字符都是‘0’到‘9’之间的某一个字符,用来表示数字中的一位。因为数字最大是n位的,因此我们需要一个长度为n+1的字符串(字符串最后一个是结束符号‘\0’)。当实际数字不够n位的时候,在字符串的前半部分补0.
首先我们把字符串中每一个数字都初始化为‘0’没然后每一次为字符串表示的数字加1,再打印出来。因此我们只需要做两件事:
一是在字符串表达的数字上模拟加法
二是把字符串表达的数字打印出来
基于上面的分析,我们可以写出如下代码:
void Print1ToMaxOfDigits(int n)
{
if(n <= 0)
return;
char *number = new char[n + 1];
memset(number, '0', n);
number[n] = '\0';
while(!Increment(number)){
PrintNumber(number)
}
delete [] number;
}
在上面的代码中,函数Increment实现在表示数字的字符串number上增加1,而函数PrintNumber打印出number。这两个看似简单的函数都隐藏着小小的玄机。
我们需要知道什么时候停止在number上加1,即什么时候到了最大的n位数“999...999”(n个9)。一个最简单的办法是每次递增之后,都调用库函数stcmp比较数字的字符串number和最大的n位数,如果相等则表示已经到了最大的n位数并终止递增。虽然调用strcmp很简单,但对于长度为n的字符串,它的时间复杂度是O(n)。
我们注意到只有对"999...99"加1的时候,才会在第一分字符(下标为0)的基础上产生进位,而其他所有情况都不会在第一个字符上产生进位。因此当我们发现在加1时第一个字符产生了进位,则已经是最大的n位数,此时Increment返回true,因此函数Print1ToMaxOfDigits中的while循环终止。如何在每一次增加1之后快速判断是不是达到了最大的n位数,是本题的一个小陷阱。下面是Increment函数的参考代码,它实现了用O(1)时间来判断是不是已经到了最大的n位数:
bool Increment(char *number)
{
bool isOverflow = false;
int nTakeOver = 0;
int nLength = strlen(number);
for(int i = nLength - 1; i >= 0; i--){
int nSum = number[i] - '0' + nTakeOver;
if(i == nLength -1)
nSum ++;
if(nSum >= 10){
if(i == 0) //如果最高位有进位则说明已经达到最大值了
isOverflow = true;
else{
nSum -= 10;
nTakeOver = 1;
number[i] = '0' + nSum;
}
}
else{
number[i] = '0' + nSum; //如果没有进位,这个循环就结束
break;
}
}
return isOverflow;
}
接下来我们再考虑如何打印用字符串表示的数字。虽然printf可以很方便就能打印出一个字符串,但在本题中并不合适。前面我们提到,当数字不够n位的时候,我们在数字的前面补0,打印的时候这些补位的0不应该打印出来。比如输入3的时候,数字98用字符串表示成“098”。如果直接打印出098,就不符合我们的习惯。为此我们定义了函数PrintNumber,在这个函数里,我们只有在碰到第一个非零的字符才看是打印,直至字符串的结尾。这也是一个小陷阱。
实现代码如下:
void PrintNumber(char *number)
{
bool isBeginning0 = true;
int nLength = strlen(number);
for(int i = 0; i < nLength; ++i){
if(isBeginning0 && number[i] != '0')
isBeginning0 = false;
if(!isBeginning0)
printf("%c", number[i]);
}
printf("\t");
}
把问题转换成数字排列的解法,递归让代码更简洁
上述思路虽然比较直观,但由于模拟了整数的加法,代码有点长。要再面试短短几十分钟时间里完整正确地写出这么长的代码,对很多应聘者而言不是一件容易的事情。接下来我们换一种思路来考虑这个问题。如果我们在数字面前补0的话,就会发现n位所有十进制数其实就是n个从0到9的全排列。也就是说,我们把数字的每一位都从0到9排列一遍,就得到了所哟的十进制数。只是我们在打印的时候,数字排在前面的0不打印出来罢了。
全排列用递归很容易表达,数字的每一位都从0~9中的一个数,然后设置下一位。递归结束的条件是我们已经设置了数字的最后一位。
void Print1ToMaxOfDigits(int n)
{
if(n <= 0)
return;
char *number = new char[n+1];
number[n] = '\0';
for(int i = 0; i < 10; ++i){
number[0] = i + '0';
Print1ToMaxOfDigitsRecursively(number, n, 0);
}
delete [] number;
}
void Print1ToMaxOfDigitsRecursively(char* number, int length, int index)
{
if(index == length - 1){
PrintNumber(number);
return;
}
for(int i = 0; i < 10; ++i){
number[index + 1] = i + '0';
Print1ToMaxOfDigitsRecursively(number, length, index + 1);
}
}
在前面的代码中,我们都是用一个char型字符表示十进制数字的一位。8个bit的char型字符最多能表示256个字符,而十进制数字只有0~9的10个数字。因此用char型字符串来表示十进制的数字并没有充分利用内存,有一些浪费。有没有更搞笑的方式来表示大数?
相关题目:
定义一个函数。在该函数中可以实现任意两个整数的加法。由于没有限定输入两个数的大小范围,我们也要把它当做大数问题来处理,在前面的代码的第一个思路中,实现了在字符串表示的数字上加1的功能,我们可以参考这个思路实现两个数字的相加功能。另外还有一个需要注意的问题:如果输入的数字中有负数,我们应该怎么处理?
总结
如果面试题是关于n位的整数并且没有限定n的取值范围,或者是输入任意大小的整数,那么这个题目很有可能是需要考虑大数问题的,字符串是一个简单、有效的表示大数的方法。