题目:Last digit of a large number
语言:C
难度:5 kyu
分析日期:2026-10-18
题目接口:
int last_digit(const char *a, const char *b);
1. 题意
给定两个可能非常长的非负整数字符串 a 和 b,返回:
a^b 的十进制个位数
题目明确规定:
0^0 = 1
示例:
a |
b |
结果 |
|---|---|---|
"4" |
"1" |
4 |
"4" |
"2" |
6 |
"9" |
"7" |
9 |
"10" |
"10000000000" |
0 |
"2^200" 的十进制结果 |
"2^300" 的十进制结果 |
6 |
由于 a、b 可能有极多位,不能把它们转换成普通的 int、long 或 long long,也不能直接计算幂。
2. 一句话抓住核心
个位只由 a 的个位决定;指数 b 只需要知道它除以 4 的余数。
例如:
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 6
2^5 = 2
从 2^1 开始,每 4 次循环一次。因此不需要知道完整的 b,只需计算 b % 4。
3. 为什么只看个位
设:
a = 10q + r
其中 r 是 a 的个位。因为含有因子 10 的项不会影响结果的个位,所以:
a^b 的个位 = r^b 的个位
例如:
1234567^3 的个位
= 7^3 的个位
= 343 的个位
= 3
所以 a 只需要读取最后一个字符:
base = a 的最后一位 - '0';
4. 为什么指数只需要模 4

0 到 9 的个位循环如下:
| 底数个位 | 循环 |
|---|---|
0 |
0 |
1 |
1 |
2 |
2, 4, 8, 6 |
3 |
3, 9, 7, 1 |
4 |
4, 6 |
5 |
5 |
6 |
6 |
7 |
7, 9, 3, 1 |
8 |
8, 4, 2, 6 |
9 |
9, 1 |
所有循环长度都不超过 4,所以对指数求模 4 就足够了。
对字符串形式的 b,可以边读边取模:
mod = (mod * 10 + (b[i] - '0')) % 4;
这个过程不需要保存完整的指数。即使 b 有一百万位,mod 仍然只会是 0、1、2、3。
5. 特殊情况必须先处理
5.1 指数为 0
按题目约定:
a^0 = 1
包括:
0^0 = 1
因此必须先判断 b 是否为数值 0。
5.2 底数为 0
当 b > 0 时:
0^b = 0
因此判断顺序应是:
1. b == 0,返回 1
2. a == 0,返回 0
3. 查表
6. 你的代码思路
你的代码经历了三个阶段。
6.1 初版:扫描字符串并直接做幂
你的核心变量含义是:
int i = 0; // a 的下标
int j = 0; // b 的下标
int d = 0; // a 的最后一位
int temp = 0; // b % 4
int out = 1; // 最终幂运算结果
扫描过程中:
d = a[i] - '0';
temp = (temp * 10 + (b[j] - '0')) % 4;
这两步的思路是正确的:
-
d最后留下a的个位; -
temp最后留下b % 4; -
不需要把完整的大整数转换成 C 的整数类型。
6.2 初版中容易忽略的边界
初版的主要问题是把 temp == 0 混合成了两种情况:
指数 b = 0
指数 b 是正数且为 4 的倍数
这两个情况在数学上不同:
a^0 = 1
a^(4k) 需要使用循环中的第 4 项
例如:
2^0 = 1
2^4 = 6
2^8 = 6
你的初版通过:
if (temp == 0) return 1;
处理了 b % 4 == 0,但这依赖于前面的 b == 0 特判。逻辑可以成立,但表达不够清晰;更好的方式是先完整判断 b 是否为 0,再把正指数的 b % 4 == 0 映射为查表下标 4。
6.3 改进版:把正指数的余数 0 映射为 4
你的改进版加入了:
if (temp == 0) {
temp = 4;
}
并把循环写成:
for (int i = 1; i <= temp; i++) {
out = d * out;
}
这等价于:
指数余数为 1,计算 d^1
指数余数为 2,计算 d^2
指数余数为 3,计算 d^3
指数余数为 0,计算 d^4
在题目没有前导零的前提下,这个版本的核心逻辑是正确的。
7. 对你的最终思路的评价
你给出的查表结构:
static const int T[10][4] = {
/* T[个位][b%4] */
{0, 0, 0, 0},
{1, 1, 1, 1},
{6, 2, 4, 8},
{1, 3, 9, 7},
{6, 4, 6, 4},
{5, 5, 5, 5},
{6, 6, 6, 6},
{1, 7, 9, 3},
{6, 8, 4, 2},
{1, 9, 1, 9}
};
这个存储方式是推荐的。它把循环规律直接固化为数据,避免运行时循环乘法。
需要特别说明两点:
7.1 表的列下标
如果表的列是:
T[base][0] = 指数余数 1
T[base][1] = 指数余数 2
T[base][2] = 指数余数 3
T[base][3] = 指数余数 0
那么查表时应使用:
int column = (b_mod_4 == 0) ? 3 : b_mod_4 - 1;
return T[base][column];
也可以把表设计成 T[10][5],直接使用 1 到 4 作为列下标,但会浪费第 0 列。你现在的 T[10][4] 更紧凑。
7.2 不要只检查第一个字符
你的改进版使用了:
if ((b[0] - '0') == 0) return 1;
else if ((a[0] - '0') == 0) return 0;
如果题目保证没有前导零,这通常可以通过测试。但从函数设计上看,更稳妥的判断应该是:
"0"、"00"、"0000" 都表示数值 0
例如:
a = "0003"
它的数值不是 0,不能因为第一个字符是 '0' 就返回 0。
因此推荐扫描整个字符串判断是否全为 '0'。
8. 推荐提交代码:查表法
下面的版本保留了你的接口、字符串取模思路和二维查表思路,并补足了前导零边界:
static const int T[10][4] = {
/* columns: exponent mod 4 = 1, 2, 3, 0 */
{0, 0, 0, 0},
{1, 1, 1, 1},
{2, 4, 8, 6},
{3, 9, 7, 1},
{4, 6, 4, 6},
{5, 5, 5, 5},
{6, 6, 6, 6},
{7, 9, 3, 1},
{8, 4, 2, 6},
{9, 1, 9, 1}
};
static int is_zero_number(const char *s)
{
int i = 0;
while (s[i] != '\0') {
if (s[i] != '0') {
return 0;
}
++i;
}
return 1;
}
int last_digit(const char *a, const char *b)
{
const char *last = a;
int base;
int b_mod_4 = 0;
int column;
int i;
/* 题目约定:0^0 = 1;这里也兼容带前导零的 "000"。 */
if (is_zero_number(b)) {
return 1;
}
if (is_zero_number(a)) {
return 0;
}
while (last[1] != '\0') {
++last;
}
base = *last - '0';
for (i = 0; b[i] != '\0'; ++i) {
b_mod_4 = (b_mod_4 * 10 + (b[i] - '0')) % 4;
}
column = (b_mod_4 == 0) ? 3 : b_mod_4 - 1;
return T[base][column];
}
9. 代码为什么正确
9.1 a 的高位不会影响个位
只取 a 的最后一位 base,因为:
a ≡ base (mod 10)
所以:
a^b ≡ base^b (mod 10)
9.2 b 只需要模 4
个位循环长度最多为 4,因此 b 与 b % 4 在查表时具有相同作用。
9.3 b % 4 == 0 不是指数 0
对正指数而言:
4、8、12、16 ...
都应该使用循环的第 4 项。因此代码把余数 0 映射到列下标 3:
column = (b_mod_4 == 0) ? 3 : b_mod_4 - 1;
而指数确实为 0 时,在查表前直接返回 1。
10. 三个版本的比较
| 版本 | 核心方法 | 时间复杂度 | 主要问题 | 评价 |
|---|---|---|---|---|
| 初版 | 扫描字符串,再循环相乘 | `O( | a | + |
| 改进版 | 指数余数为 0 时改成 4 | `O( | a | + |
| 查表版 | a 个位 + b mod 4 查表 |
`O( | a | + |
这里的 O(|a| + |b|) 是不可避免的:至少要读取输入字符串。查表版的额外计算是常数级,额外空间为:
O(1)
11. 推荐测试集
a |
b |
期望 | 覆盖点 |
|---|---|---|---|
"4" |
"1" |
4 |
基本情况 |
"4" |
"2" |
6 |
循环第 2 项 |
"9" |
"7" |
9 |
周期 2 |
"10" |
"10000000000" |
0 |
底数个位为 0 |
"0" |
"0" |
1 |
0^0 特判 |
"0" |
"5" |
0 |
0^正数 |
"2" |
"4" |
6 |
b % 4 == 0 |
"2" |
"8" |
6 |
更大的 4 的倍数 |
"7" |
"3" |
3 |
周期为 4 的底数 |
"0003" |
"2" |
9 |
前导零 |
"3" |
"0000" |
1 |
带前导零的零指数 |
"2" |
"2000000000000000000000000000000000000001" |
2 |
超长指数,只保留模 4 |
12. 复杂度与工程取舍
| 指标 | 结果 |
|---|---|
| 时间复杂度 | `O( |
| 额外空间 | O(1) |
| 是否转换大整数 | 否 |
| 是否计算完整幂 | 否 |
| 是否需要浮点数 | 否 |
| 是否需要大整数库 | 否 |
这道题的关键不是"如何快速计算一个巨大幂",而是先改变问题:
巨大整数幂
↓
只求模 10
↓
只看底数个位
↓
只看指数模 4
↓
二维表查找
13. 结论
你的总体思路是正确的,尤其是以下两个关键点:
-
不能把
*a、*b直接强制转换成整数,因为它们只是数字字符串的首字符; -
指数可以边扫描边对
4取模,不需要保存完整的大数。
改进方向主要是:
-
把
b == 0与b % 4 == 0明确区分; -
正指数的余数 0 映射到周期的第 4 项;
-
使用二维表取代运行时乘法;
-
判断数值 0 时扫描整个字符串,而不是只看首字符。
推荐提交第 8 节的查表版本。
来源
-
本文中的代码评审对象:用户在本次对话中提供的
last_digit初版、改进版和最终查表设计。