欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《剑指offer》面试题17:打印从1到最大的n位数

程序员文章站 2024-03-15 22:22:30
...

.更多剑指offer面试习题请点击:《剑指offer》(第二版)题集目录索引

题目:输入数字n,按顺序打印出从1到最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数999。


解题思路:
    大家第一眼看到这种题目可能觉得特别简单,直接用一个循环打印1到最大的n位数不就好了嘛。所以很快的写下如下代码:

void Print1ToMaxOfNDigits(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=10,你用的int变量还能打印出最大的10位数吗?这好办,我定义成long long型不就好了嘛,但如果此时n=100呢?这里要处理这种大数问题,最好的方法便是采用字符串

    我们可以创建一个字符串数组number,长度为n+1,先初始化为‘0’,把最后一个元素赋值成‘\0’。然后每次字符串表示的数字加1,再打印出来。因此我们只需循环做两件事:一是在字符串上模拟数字的加法;二是把字符串表达的数字打印出来。

void Print1ToMaxOfNDigits_1(int n)
{
    if (n <= 0)
        return;

    char* number = (char*)malloc(n + 1);//动态开辟数组
    assert(number != NULL);
    memset(number, '0', n + 1);

    number[n] = '\0';

    while (!Increment(number))
    {
        PrintNumber(number);
    }

    free(number); //释放堆空间
    number = NULL;
}

循环

    上面那段代码就是根据我们的思路翻译过来的。Increment函数是实现字符串number加1的功能,PrintNumber函数则实现了打印number的功能。这样我们就把程序的整体框架写好了,只要再写完这两个子函数就OK了,但就这两个子函数,里面却充满了陷阱。

    第一个陷阱,我们得知道这个while循环什么时候停下来。我们知道循环中number不断的加1,到了最大的n位数“999······99”(n个9)时循环就该结束了,所以我们可以用字符串“999······99”和number进行strcmp比较,每循环一次便比较一次,但它的时间复杂度时O(n)。我们需要效率更高的算法。

    仔细观察,我们发现只有当“999······99”加1时,最高位才会产生进位现象,这就代表循环结束。我们给Increment函数设定:当返回1时代表已经到最大数,循环结束;当返回0时,循环继续,这种方法的时间复杂度是O(1)。当然这个函数不仅仅只有这一个功能,它最重要的功能便是实现字符串number模拟数字加法。

int Increment(char* number)
{
    int isOverflow = 0; //判断最高位(number[0])是否进位
    int nTakeOver = 0;  //次高位发生进位则高位要加1。

    int nLength = strlen(number);
    int i = 0;
    for (i = nLength - 1; i >= 0; i--)
    {
        int nSum = number[i] - '0' + nTakeOver;
        if (i == nLength - 1)
            nSum++;

        /*发生进位*/
        if (nSum >= 10)
        {
            /*最高位发生进位,程序结束*/
            if (i == 0)
                isOverflow = 1;
            else
            {
                nSum -= 10; //进位后,这个位置为0
                nTakeOver = 1; //进位的1
                number[i] = '0' + nSum;
            }
        }
        else
        {
            /*没有进位,当前位加1,跳出循环*/
            number[i] = '0' + nSum;
            break;
        }
    }
    return isOverflow;
}

    写完Increment函数后这个题目就完成一大半了,但此时还有一个小陷阱在等我们。在打印字符串的时候大家可能就直接写一个循环把整个字符串打印出来就完事了,但我们注意,前面我们说把整个字符串都初始化成‘0’,当你的数字不够n位时,高位默认的都是0。比如“006”、“011”这些。数字前面带0虽然对数字本身没啥影响,但不符合我们阅读习惯,我们得把不需要的0去了。这时候可能会犯一种低级错误:打印前我先判断遇到‘0’我就不打印。但遇到“101”这种数字又怎么办呢?这里我们利用了一种很巧妙的东西——标识符

/*这个可能看得有点绕,大家用一个实际的值代进去就清楚了*/
void PrintNumber(char* number)
{
    int length = strlen(number);
    int isBeginning0 = 1;
    int i = 0;

    for (i = 0; i < length; i++)
    {

        if (isBeginning0 && number[i] != '0')
            isBeginning0 = 0;

        if (!isBeginning0)
            printf("%c", number[i]);
    }
    printf("\t");
}

全排列递归

    上面讲的这种方法,代码的效率是很高的,但代码太长了,而且思路有点复杂。下面介绍一种新的思路。我们发现从“000······00”到“999······99”,每位其实都是0~9的排列,只要我们把每位都从0~9排列一遍,就可以得到所有的数。我们用递归来实现这种排列,先排列最高位,再递归排列次高位,当最低位排列完时递归结束。

void Print1ToMaxOfNDigits_2(int n)
{
    if (n <= 0)
        return;

    char* number = (char*)malloc(n + 1);
    assert(number != NULL);

    number[n] = '\0';

    int i = 0;
    for (i = 0; i < 10; i++)
    {
        /*先把最高位的情况排列出来*/
        number[0] = i + '0';
        Print1ToMaxOfNDigitsRecursively(number, n, 0);
    }

    free(number);
    number = NULL;
}

void Print1ToMaxOfNDigitsRecursively(char* number, int length, int index)
{
    /*最低位排列完毕,递归结束*/
    if (index == length - 1)
    {
        PrintNumber(number);
        return;
    }

    int i = 0;
    for (; i < 10; i++)
    {
        number[index +1] = i + '0';
        Print1ToMaxOfNDigitsRecursively(number, length, index + 1);
    }
}

测试用例

void Test(int n)
{
    printf("Test for %d begins:\n", n);

    Print1ToMaxOfNDigits_1(n);
    Print1ToMaxOfNDigits_2(n);

    printf("\nTest for %d ends.\n\n\n", n);
}

int main(int argc, char* argv[])
{
    Test(1);
    Test(2);
    Test(3);
    Test(0);
    Test(-1);


    system("pause");
    return 0;
}