Codewars:Last digit of a large number(C)解题分析

题目: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. 结论

你的总体思路是正确的,尤其是以下两个关键点:

  1. 不能把 *a、*b 直接强制转换成整数,因为它们只是数字字符串的首字符;

  2. 指数可以边扫描边对 4 取模,不需要保存完整的大数。

改进方向主要是:

  • 把 b == 0 与 b % 4 == 0 明确区分;

  • 正指数的余数 0 映射到周期的第 4 项;

  • 使用二维表取代运行时乘法;

  • 判断数值 0 时扫描整个字符串,而不是只看首字符。

推荐提交第 8 节的查表版本。

来源

  1. Codewars 题目页面

  2. Codewars Challenge API

  3. 本文中的代码评审对象:用户在本次对话中提供的 last_digit 初版、改进版和最终查表设计。

相关推荐
码途行者1 小时前
处理超时未支付订单:两套轻量化方案,附 PHP 极简实现
开发语言·redis·php
云和数据.ChenGuang1 小时前
LangChain 实战指南:从入门到构建智能应用
开发语言·人工智能·python·langchain
const_qiu1 小时前
第三章 04:3.6 基础调用链的构建
开发语言·python
向上的车轮1 小时前
Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南
开发语言·python
Nebula_g1 小时前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
想做小南娘,发现自己是女生喵2 小时前
第二周学习文档 Linux C 文件操作
linux·c语言·学习
程序猿乐锅2 小时前
【从0-1学习Python | 第六篇】 Python 函数:定义、参数与返回值
开发语言·python·学习
G018_star sky♬2 小时前
java中的字符串常见方法
java·开发语言
Bluetooth7302 小时前
c语言变量的存储类别
c语言