《剑指Offer》学习笔记--面试题12:打印1到最大的n位数

题目:输入数字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的取值范围,或者是输入任意大小的整数,那么这个题目很有可能是需要考虑大数问题的,字符串是一个简单、有效的表示大数的方法。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值