LeetCode之路:258. Add Digits

来源:互联网 发布:清镇市各乡镇人口数据 编辑:程序博客网 时间:2024/05/18 03:50

一、引言

这是一道比较难的题,之所以下这个定义,是因为要做出来这道题的正确答案,需要一点点数学知识。

当然,能看到这篇博客的人,大部分都是程序员,那么作为程序员,我们自然有自己的方法来实现它,这样看来,这道题其实也不算太难。

直接上题目信息吧:

Given a non-negative integer num, repeatedly add all its digits until the result has only one digit.

For example:

Given num = 38, the process is like: 3 + 8 = 11, 1 + 1 = 2. Since 2 has only one digit, return it.

Follow up:
Could you do it without any loop/recursion in O(1) runtime?

题目信息非常简单,大概翻译一下:

给你一个正数 num,反复的相加这个正数的各个数位的值,直到最后结果只有一个数位为止。

比如:

给出数字 num = 38,运算过程如下: 3 + 8 = 11, 1 + 1 = 2。由于 2 只有 1 位数字,则 2 为最终的结果

扩展:
你不使用任何的循环和递归并且达到 O(1) 的时间复杂度完成这道题吗?

这道题的信息非常短小,但是要求非常高,要求我们不能使用循环或者递归,并且在时间复杂度 O(1) 下完成这道题。

说实话,难度真的太大了,如果你和我一样,都是一个萌萌的新手的话,不如让我们简化题目,使用循环和递归来完成这道题看看。

二、使用递归:编程模拟自己的运算过程

如果要选择使用递归来实现这道题,那么 O(1) 的时间复杂度是肯定满足不了的了。不过,就当作是练习,让我们尝试下。

那么如何站在递归的角度上来看待这个问题呢?

首先,让我们着眼于这个例子:

the process is like :
3 + 8 = 11
1 + 1 = 2

我们来仔细分析下这个例子,我们得到了一个 num 值为 38,这个 38 有两位数,很明显并不能直接返回(直接返回需要只有一位数),那么需要进行各个位数相加,所以进行了第一个算式的相加;

第一个算式结束后,我们发现结果是 11,然而这个 11 依然不满足直接返回的条件,于是我们继续进行各位数相加的计算,进行了第二个算式的相加;

最后,我们发现结果为 2,只有一个位数,可以直接返回,于是得到最后的结果。

经过我们分析的一步步跟进,我们发现:

  1. 返回条件:结果只有一位数
  2. 运算过程:各位相加
  3. 递归原因:各位相加的结果不满足只有一位数的条件则需要继续进行各位相加的运算过程

根据上述的分析,代码已出:

// my solution use recursion , runtime = 3 msclass Solution {public:    int addDigits(int num) {        if (num < 10) return num;        int sum = 0;        do {            sum += num % 10;            num /= 10;        } while (num != 0);        return addDigits(sum);    }};

这里对这段代码稍作解释:

首先,判断 num 是否小于 10,如果小于 10 (表示只有一位数位),则直接返回;

然后,如果 num 大于或等于 10,则进行各位数位的相加,这里使用的是 % 10 来取数位值和 /= 10 来消去一位数位的做法;

最后,我们将计算出来的 sum 值递归投入到下一个 addDigits() 方法中去,并且返回(addDigits() 方法的返回必然是一个满足条件的值)。

我们可以看到,递归的方法代码量虽少,但是理解起来稍微有些困难,接下来让我们看看使用循环的方法应该怎么编写代码。

三、使用循环:更加接近人脑的思维过程

如何站在循环的角度上来看待这个问题呢?

其实循环更加简单一些,递归毕竟会有些让人难以理解的地方,比如上面代码中最后的一处 return 。但是循环就不存在这个问题了,循环的代码就如同干净的立交桥一样,尽管循环交错,但是只要跑上去绝对是一目了然。

这里直接贴上我的代码:

// my solution use loop , runtime = 3 msclass Solution {public:    int addDigits(int num) {        int sum = 0;        while (true) {            do {                sum += num % 10;                num /= 10;            } while (num != 0);            if (sum >= 10) num = sum, sum = 0;            else           break;        }        return sum;    }};

这里使用了两个循环,外层循环监控 sum 是否小于等于 10,否则就一直进行循环相加的运算过程;另外,每次计算完一轮的 sum 之后,需要将 num 值重置为 sum 的值,sum 变量需要每次循环之前重置为 0。

循环的写法,基本上只要理清楚了逻辑,控制好退出条件和每次循环的初始变量值,编写代码实在是非常惬意的事情。

可是,我们的问题并没有解决,我们并没有按照题目要求来完成这道题,那么,我们该如何做到不使用循环和递归来完成这道题目呢?

四、最高明的代码简化

是使用位操作吗?

不行,因为涉及到了十进制,基于二进制的位操作基本没有什么用场。

那么怎么办?

T_T 我也不知道……

所以我点开了最高票答案(我也是个菜鸟哈^_^)

Discuss

(ÒωÓױ)感觉受到了 10000 点伤害……

我思考了那么久的题目,这一溜的 1 line 答案,额,让我们平复下心情,继续看下去:

// perfect solution use congruence formula , runtime = 9 msclass Solution3 {public:    int addDigits(int num) {        return 1 + (num - 1) % 9;    }};

直到我看到了这么一行代码,心里是非常震惊的!

最优美的代码简化,不是语言特性的巧妙使用,也不是编程方法的极致缩减,而是数学思维的编程体现啊。

看到这一行代码,其实心里都大概有了一个原因了,肯定是用了数学公式了,那么是什么样的数学公式呢:

Congruence formula

这是最高票答案作者给出的维基百科的公式资料,想要了解的同学可以点击这里 digit root problem congruence formula。

这里简要介绍下这个公式(定义 b 为十进制的基数,那么 dr(n) 就是求 n 相对于基数 b 的数位根):

  • dr(n) = 0 if n == 0
  • dr(n) = (b - 1) if n != 0 and n % (b - 1) == 0
  • dr(n) = n mod (b - 1) if n % (b - 1) != 0

于是推出

  • dr(n) = 1 + (n - 1) % 9

我们首先来看看上面三个式子:

  • 当 n = 0 时,基于 10 的数位根肯定是 0
  • 当 n != 0 ,并且 n % (b - 1) == 0 ,也就是说 n 非零,却得出了为 0 的数位根,这个时候是算的模 b - 1 的值
  • 最后我们得到普遍性规律,只要 n % (b - 1) != 0,则 n 模 b - 1 的结果即为我们要求的数位根 n

于是乎,我们将上述的 b (基数)替换为此处的 10,得到公式:

  • dr(n) = 1 + (n - 1) % 9

这里加了一个 1,是因为当你 n 取 0 的时候,返回的结果为 -1,加了一个 1 用来调整结果。

如果这样还不能让你理解的话,试着这样去理解这道题:

我们输入的 num 值,基于 10 的数位根,输出的结果一定是周期性变化的:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 1 2 3 4 …
我们只要摸清楚了规律,给定一个 num 值我们就可以模计算出我们所需要的数位根结果

至此,一个数学公式,一行代码,简洁而优美,Perfect!

五、总结

因为这道题,刷 LeetCode 的计划顿挫了比较久,因为这道题实在是比较难(我认为)。

做出递归和循环的方法还是比较简单的,但是简化到 O(1) 的时间复杂度,不了解公式的话实在比较难,我之前还在思考如何使用二进制模拟模 10 的思维陷阱中挣扎了许久。

不过这样的问题我觉得学习意义大于做出来的意义。思考的过程获得的大于最后的答案。这样的题目我觉得更大的意义在于数学而非编程。

对于我们普通的程序员来说,一定的数学思维是要有的,一定的代码简洁和优美的追求是要有的,更加需要的,则是我们能够将我们脑海中的思维转化成代码的能力。

这段时间处于各个计划的交接处,LeetCode 之路还会继续走下去的,工作之余的学习计划仍在考虑之中,尽管最近有些懈怠,不过一切都会紧锣密鼓的走上正轨的。

最后的最后,从这道题里的感慨结尾:

最优美的代码简化,不是语言特性的巧妙使用,也不是编程方法的极致缩减,而是数学思维的编程体现。

0 0
原创粉丝点击