算法:大数除法

要理解进制转换中的大数除法 ,核心是先想明白:它本质就是模拟我们小学时"手工列竖式做除法"的过程------只不过把"10进制的数字"换成了"M进制的字符串",把"写在纸上的步骤"翻译成了代码。

我会先从你最熟悉的10进制手工除法讲起,再扩展到M进制的大数除法,最后对应到代码,保证一步一步看懂。

一、先回忆:我们手算是怎么算除法的?

以题目样例 11(10进制)÷ 2 为例,我们手工列竖式的步骤是这样的:

复制代码
    5  (商)
  ______
2 | 1  1
    1  0
  ______
      1  (余数)
→ 商=5,余数=1

然后继续算 5÷2:
    2  (商)
  ______
2 | 5
    4
  ______
    1  (余数)

再算 2÷2:
    1  (商)
  ______
2 | 2
    2
  ______
    0  (余数)

最后算 1÷2:
    0  (商)
  ______
2 | 1
    0
  ______
    1  (余数)

最终得到余数序列 1、1、0、1,反转后就是二进制 1011------这和代码要做的事完全一样!

手工除法的核心步骤(划重点):

  1. 从高位到低位,逐位处理被除数;
  2. 每一步:当前被除数 = 上一步的余数 × 进制基数 + 当前位的数字
  3. 算当前位的商:当前被除数 ÷ 除数
  4. 算新的余数:当前被除数 % 除数
  5. 重复直到商为0,余数逆序就是结果。

二、把手工除法"翻译"成M进制的大数除法

现在把场景换成:M进制的字符串(比如16进制"A",也就是10进制的10) ÷ N(比如2),核心逻辑完全不变,只是"进制基数"从10换成了M(比如16)。

例子:16进制"A"(值为10) ÷ 2(目标进制)

手工模拟M进制(16)的除法步骤:

  • 被除数:"A"(16进制,对应数值10),除数=2,基数=16(原进制M);
  • 第一步:
    • 上一步余数=0(初始);
    • 当前被除数 = 0 × 16 + 10 = 10;
    • 当前商 = 10 ÷ 2 = 5(16进制的"5");
    • 余数 = 10 % 2 = 0(这是N进制的第一位);
  • 第二步:处理商"5"(16进制)÷2:
    • 当前被除数 = 0 × 16 + 5 = 5;
    • 当前商 = 5 ÷ 2 = 2(16进制的"2");
    • 余数 = 5 % 2 = 1(N进制第二位);
  • 第三步:处理商"2"(16进制)÷2:
    • 当前被除数 = 1 × 16 + 2 = 18;
    • 当前商 = 18 ÷ 2 = 9?不,纠正:商是 18÷2=9?不对,重新来------
      (其实更简单的方式:不管M是多少,手工步骤的核心是"基数固定为M",计算逻辑和10进制完全一致)

三、大数除法的代码逻辑(对应手工步骤)

下面把 big_divide 函数拆成和手工步骤对应的部分,每一行都对应你手算的动作:

c 复制代码
// 大数除法:M进制字符串 ÷ N,返回余数,商存入quotient
int big_divide(const char *dividend, int N, int base, char *quotient) {
    int remainder = 0; // 对应手工除法中"上一步的余数",初始为0
    char temp_quotient[1024] = {0}; // 存手工除法中"写在上面的商"
    int q_idx = 0; // 记录商的位数
    
    int len = strlen(dividend);
    for (int i = 0; i < len; i++) { // 手工除法:从高位到低位逐位处理
        char c = dividend[i]; // 当前位的字符(比如"A")
        // 步骤1:把字符转成数值(比如"A"→10)
        int digit = isdigit(c) ? (c-'0') : (c-'A'+10);
        
        // 步骤2:核心!手工除法的"当前被除数 = 上步余数 × 基数 + 当前位数字"
        // base就是原进制M(比如16),这一步和你手算10进制时×10完全一样
        int current = remainder * base + digit;
        
        // 步骤3:算当前位的商(写在竖式上面)
        int q_digit = current / N;
        // 步骤4:算新的余数(竖式下面的余数,也是N进制的一位)
        remainder = current % N;
        
        // 步骤5:把当前位的商存起来(比如5→'5',10→'A')
        temp_quotient[q_idx++] = (q_digit < 10) ? (q_digit+'0') : (q_digit-10+'A');
    }
    temp_quotient[q_idx] = '\0';
    
    // 步骤6:移除商的前导零(比如商是"005"→"5",手工算时也不会写前导零)
    remove_leading_zeros(temp_quotient, quotient);
    
    return remainder; // 返回这一轮的余数(N进制的一位)
}

四、为什么大数除法能解决"溢出"问题?

普通除法(decimal / N)依赖数值类型(比如unsigned long long),而大数除法:

  • 被除数是字符串(不是数值),长度只受缓冲区限制,能存任意长的M进制数;
  • 每一步计算的current = remainder × base + digit
    • remainder 是除以N的余数(≤35),base 是M(≤36),digit 是M进制位值(≤35);
    • 计算结果 35×36+35=1295,远小于int的范围,完全不会溢出。

五、一句话总结大数除法的核心

把M进制的字符串当成"数字",像手算除法一样逐位处理:每一步用"上轮余数×M+当前位值"算出当前被除数,再除以N得到商和余数------商继续当被除数,余数就是N进制的一位,直到商为0,余数逆序就是结果。

六、再用样例串一遍(10进制11→2进制)

  1. 初始被除数:"11"(M=10),N=2;
  2. 第一次调用big_divide
    • 处理第一位'1':current=0×10+1=1 → 商0,余数1;
    • 处理第二位'1':current=1×10+1=11 → 商5,余数1;
    • 商="5",余数=1(存到结果);
  3. 第二次调用big_divide(被除数="5"):
    • current=0×10+5=5 → 商2,余数1;
    • 商="2",余数=1(存到结果);
  4. 第三次调用big_divide(被除数="2"):
    • current=0×10+2=2 → 商1,余数0;
    • 商="1",余数=0(存到结果);
  5. 第四次调用big_divide(被除数="1"):
    • current=0×10+1=1 → 商0,余数1;
    • 商="0",余数=1(存到结果);
  6. 余数序列:[1,1,0,1] → 反转后1011,就是最终结果。

这样一步步对应下来,是不是就明白大数除法其实就是"把手工除法写进代码里"了?核心没有新东西,只是把我们熟悉的10进制除法,扩展到了任意M进制,并用字符串替代数值存储而已。

没关系!咱们彻底抛开"大数除法"这个抽象名词,用最笨的手工步骤+逐行对应代码的方式,只讲"怎么做",不讲"为什么理论对"------你只要跟着我的步骤算一遍,马上就懂了。

第一步:先拿你熟悉的10进制"11÷2",拆成"最小步骤"

我们不用列竖式,把"11÷2"拆成逐位处理的最小动作(这就是代码要做的事):

手工最小步骤(10进制,被除数=11,除数=2,基数=10)
步骤 处理位 上轮余数 当前被除数(余数×基数+当前位) 当前商 新余数 商字符串
初始 - 0 - - -
1 第一位1 0 0×10 + 1 = 1 1÷2=0 1 "0"
2 第二位1 1 1×10 + 1 = 11 11÷2=5 1 "05"
结束 - 1 - - - "05"

→ 这一轮:商是"05"(去前导零后"5"),余数是1(二进制的一位)。

接下来处理商"5"÷2(还是10进制,基数=10):

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
初始 - 0 - - -
1 第一位5 0 0×10+5=5 5÷2=2 1 "2"
结束 - 1 - - - "2"

→ 商是"2",余数是1(二进制第二位)。

再处理商"2"÷2:

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位2 0 0×10+2=2 2÷2=1 0 "1"

→ 商是"1",余数是0(二进制第三位)。

最后处理商"1"÷2:

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位1 0 0×10+1=1 1÷2=0 1 "0"

→ 商是"0"(停止),余数是1(二进制第四位)。

最终余数序列:1、1、0、1 → 反转=1011(就是答案)。

核心发现 :手工算的时候,我们是"把上一轮的余数和下一位结合"(比如第一步余数1,和第二位1结合成11)------这个"结合"的动作,就是代码里的 remainder × 基数 + 当前位

第二步:把"手工最小步骤"翻译成"人话伪代码"

不管是10进制还是M进制,伪代码只有5步,完全不变:

复制代码
// 对任意进制的被除数(字符串)÷除数N,基数=原进制M
函数 大数除法(被除数字符串, N, M):
    上轮余数 = 0
    商字符串 = 空
    遍历被除数的每一位字符:
        1. 把字符转成数字(比如"A"→10)
        2. 当前被除数 = 上轮余数 × M + 这个数字  // 就是"余数和下一位结合"
        3. 当前商的一位 = 当前被除数 ÷ N
        4. 上轮余数 = 当前被除数 % N
        5. 把"当前商的一位"拼到商字符串里
    给商字符串去前导零(比如"05"→"5")
    返回:上轮余数(N进制的一位)、商字符串

第三步:把"人话伪代码"对应到实际代码(逐行贴,逐行解释)

我们只看核心的big_divide函数,每一行都对应上面的伪代码:

c 复制代码
int big_divide(const char *dividend, int N, int base, char *quotient) {
    // 1. 对应伪代码:上轮余数 = 0
    int remainder = 0; 
    // 2. 对应伪代码:商字符串 = 空(temp_quotient存拼出来的商)
    char temp_quotient[1024] = {0}; 
    int q_idx = 0; 

    int len = strlen(dividend);
    // 3. 对应伪代码:遍历被除数的每一位字符
    for (int i = 0; i < len; i++) { 
        // 3.1 对应伪代码:把字符转成数字
        char c = dividend[i]; 
        int digit = isdigit(c) ? (c-'0') : (c-'A'+10);
        
        // 3.2 对应伪代码:当前被除数 = 上轮余数 × M + 这个数字
        int current = remainder * base + digit; 
        
        // 3.3 对应伪代码:当前商的一位 = 当前被除数 ÷ N
        int q_digit = current / N; 
        // 3.4 对应伪代码:上轮余数 = 当前被除数 % N
        remainder = current % N; 
        
        // 3.5 对应伪代码:把"当前商的一位"拼到商字符串里
        temp_quotient[q_idx++] = (q_digit < 10) ? (q_digit+'0') : (q_digit-10+'A');
    }
    temp_quotient[q_idx] = '\0';
    
    // 4. 对应伪代码:给商字符串去前导零
    remove_leading_zeros(temp_quotient, quotient);
    
    // 5. 对应伪代码:返回上轮余数(N进制的一位)
    return remainder; 
}

第四步:换M进制(比如16进制"A"÷2),只改一个数,步骤完全不变

被除数是16进制"A"(字符"A"→数字10),除数N=2,基数base=16(原进制M):

手工最小步骤(16进制,被除数="A",除数=2,基数=16)
步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位A 0 0×16+10=10 10÷2=5 0 "5"

→ 商是"5",余数是0(二进制第一位)。

再处理商"5"(16进制)÷2:

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位5 0 0×16+5=5 5÷2=2 1 "2"

→ 商是"2",余数是1(二进制第二位)。

再处理商"2"(16进制)÷2:

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位2 0 0×16+2=2 2÷2=1 0 "1"

→ 商是"1",余数是0(二进制第三位)。

最后处理商"1"(16进制)÷2:

步骤 处理位 上轮余数 当前被除数 当前商 新余数 商字符串
1 第一位1 0 0×16+1=1 1÷2=0 1 "0"

→ 商是"0"(停止),余数是1(二进制第四位)。

最终余数序列:0、1、0、1 → 反转=1010(就是16进制"A"转2进制的答案)。

最后:一句话说透大数除法

大数除法就是把你手工算除法的"逐位处理、余数结合下一位"的动作,写成代码------唯一的区别是,手工算用10当基数,代码里可以把基数换成任意M(原进制),用字符串存数字(避免溢出)。

你可以自己拿一个简单的M进制数(比如8进制的"7"转2进制),按上面的"最小步骤表"算一遍,再对照代码看------会发现代码就是在"抄"你的手工步骤,没有任何额外的复杂逻辑。

相关推荐
@小码农2 小时前
2025年12月 GESP认证 图形化编程 一级真题试卷(附答案)
开发语言·数据结构·算法
巧克力味的桃子2 小时前
让程序在读取到整数0时就终止循环
c语言·算法
_OP_CHEN2 小时前
【算法基础篇】(三十九)数论之从质数判定到高效筛法:质数相关核心技能全解析
c++·算法·蓝桥杯·埃氏筛法·acm/icpc·筛质数·欧拉筛法
算法与编程之美2 小时前
损失函数与分类精度的关系
人工智能·算法·机器学习·分类·数据挖掘
天呐草莓2 小时前
聚类(Clustering)算法
人工智能·python·算法·机器学习·数据挖掘·数据分析·聚类
m0_743106462 小时前
【基础回顾】针孔相机、深度、逆深度、与SfM的统一
人工智能·算法·计算机视觉·3d·几何学
炽烈小老头2 小时前
【每天学习一点算法 2025/12/30】最大子序和
学习·算法
Flash.kkl2 小时前
优选算法专题十八——BFS解决拓扑排序
算法·宽度优先
hetao17338372 小时前
2025-12-30 hetao1733837 的刷题笔记
c++·笔记·算法