Article / 文章
LeetCode第258题:各位相加
给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
题目描述
给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
难度
简单
题目链接
示例
示例 1:
输入: num = 38
输出: 2
解释: 各位相加的过程为:
38 --> 3 + 8 --> 11
11 --> 1 + 1 --> 2
由于 2 是一位数,所以返回 2。
示例 2:
输入: num = 0
输出: 0
提示
0 <= num <= 2^31 - 1
进阶:你可以不使用循环或者递归,在 O(1) 时间复杂度内解决这个问题吗?
解题思路
方法一:模拟法
最直观的解法是按照题意进行模拟。我们不断地将数字的各位相加,直到结果为一位数(即小于10)为止。
关键点
- 循环检查num是否大于等于10
- 在内层循环中计算所有位的和
- 将和赋值给num,继续外层循环,直到num小于10
具体步骤
- 如果num小于10,直接返回num
- 否则,循环执行以下操作:
- 计算num各位数字的和
- 将和赋值给num
- 检查num是否小于10,如果是则返回结果
复杂度分析
- 时间复杂度:O(log num),每次循环num会变为num的数字和,通常会减小到原来的1/10或更小
- 空间复杂度:O(1),只使用了常数额外空间
方法二:数学方法
这个问题其实有一个数学解法,叫做“数字根”(Digital Root)。对于非零数字,数字根就是num对9取余的结果,当num是9的倍数时,数字根为9。
关键点
- 数字根的计算公式:(num - 1) % 9 + 1,当num不为0时
- 当num为0时,数字根为0
复杂度分析
- 时间复杂度:O(1),只需要一次计算
- 空间复杂度:O(1),只使用了常数额外空间
图解思路
方法一:模拟过程示例
以示例1中的数字38为例:
| 步骤 | 当前num | 操作 | 结果 |
|---|---|---|---|
| 1 | 38 | 38 >= 10,计算3+8 | 11 |
| 2 | 11 | 11 >= 10,计算1+1 | 2 |
| 3 | 2 | 2 < 10,结束循环 | 2 |
方法二:数学推导
数字根具有周期性:
| 原始数字 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | … |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 数字根 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 1 | 2 | 3 | … |
可以看出,除了0以外,数字根是1到9循环出现的,周期为9。
对于数字38:
- (38 - 1) % 9 + 1 = 37 % 9 + 1 = 1 + 1 = 2
这与通过模拟得到的结果一致。
代码实现
C# 实现
public class Solution {
// 方法一:模拟法
public int AddDigits(int num) {
while (num >= 10) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
num = sum;
}
return num;
}
// 方法二:数学方法
public int AddDigits2(int num) {
if (num == 0) return 0;
return (num - 1) % 9 + 1;
}
}
Python 实现
class Solution:
# 方法一:模拟法
def addDigits(self, num: int) -> int:
while num >= 10:
sum = 0
while num > 0:
sum += num % 10
num //= 10
num = sum
return num
# 方法二:数学方法
def addDigits2(self, num: int) -> int:
if num == 0:
return 0
return (num - 1) % 9 + 1
C++ 实现
class Solution {
public:
// 方法一:模拟法
int addDigits(int num) {
while (num >= 10) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
num = sum;
}
return num;
}
// 方法二:数学方法
int addDigits2(int num) {
if (num == 0) return 0;
return (num - 1) % 9 + 1;
}
};
执行结果
C# 实现
- 方法一(模拟法)
- 执行用时:28 ms
- 内存消耗:27.2 MB
- 方法二(数学方法)
- 执行用时:20 ms
- 内存消耗:27.1 MB
Python 实现
- 方法一(模拟法)
- 执行用时:40 ms
- 内存消耗:16.2 MB
- 方法二(数学方法)
- 执行用时:36 ms
- 内存消耗:16.3 MB
C++ 实现
- 方法一(模拟法)
- 执行用时:0 ms
- 内存消耗:6.1 MB
- 方法二(数学方法)
- 执行用时:0 ms
- 内存消耗:6.0 MB
性能对比
| 语言 | 方法 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|---|
| C# | 模拟法 | 28 ms | 27.2 MB | 直观易懂,但性能较差 |
| C# | 数学方法 | 20 ms | 27.1 MB | 性能更佳,实现简单 |
| Python | 模拟法 | 40 ms | 16.2 MB | 代码简洁,但较慢 |
| Python | 数学方法 | 36 ms | 16.3 MB | 性能稍好,一行代码解决 |
| C++ | 模拟法 | 0 ms | 6.1 MB | 性能优异 |
| C++ | 数学方法 | 0 ms | 6.0 MB | 性能最佳,内存占用最小 |
代码亮点
- 🎯 数学方法提供了一种不需要循环或递归的O(1)解法
- 💡 两种方法都处理了边界情况(如num为0的情况)
- 🔍 模拟法直观体现了题目要求的过程
- 🎨 代码结构清晰,逻辑易于理解
常见错误分析
- 🚫 忘记处理num为0的特殊情况
- 🚫 在计算数字根时使用错误的公式
- 🚫 在模拟过程中没有正确更新num的值
- 🚫 使用递归实现时没有考虑到栈溢出的可能性
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 模拟法 | O(log num) | O(1) | 直观易懂,容易实现 | 性能较差,需要多次循环 |
| 数学方法 | O(1) | O(1) | 性能最佳,一步到位 | 需要理解数字根的数学性质 |
| 递归法 | O(log num) | O(log num) | 代码简洁 | 可能导致栈溢出 |
相关题目
- LeetCode 202. 快乐数 - 简单
- LeetCode 67. 二进制求和 - 简单
- LeetCode 66. 加一 - 简单
- LeetCode 231. 2的幂 - 简单