正在加载...

C语言算法之高精度问题-2-高精度加减法

技术杂货铺  ·  2025-10-24

预计阅读时间: 66.5 分钟
16614 字250 字/分
本文内容较新·3周前更新
最后更新: 2025年10月25日

本章较长,我会尽可能地讲详细,可根据文章目录检索

接上回

接续上回,我们实现了将数字保存为高精度数字,让我们一起回忆一下代码

#include <stdio.h>  
#define MAX_SIZE 500
#define ll long long

typedef struct titan_number{
    int num[MAX_SIZE];
    int is_negative; //1表示负数,0表示正数
    int size;
} tn;
void save_number(tn* number,short input){
    number->size = 0; //初始化数字长度为0
    if (input < 0){ //如果输入为负数,记录正负状态
        number->is_negative = 1;
        input = -input;
    } else{
        number->is_negative = 0;
    }
    while (input > 0){ //循环倒序插入数字
        number->num[number->size] = input % 10;
        input /= 10;
        number->size++;
    }

}

接下来,我们存入两个大数 a和b,我们先设定a==1234 , b==4321

一起看看它们在我们的结构体中是什么样的

a:

index0123
number4321

b:

index0123
number1234


二者的is_negative属性均为0,size为4

同号相加

因为加法会涉及到进位的问题,也根据小学学过的竖式计算,我们需要从各位开始计算,那就是二者的index[0]作为第一次循环的加数,得到5。当然,如果说二者的index[0]处的数相加大于等于10呢?

那我们就需要进位了,而进位的数就是sum / 10的值(利用了整数只保留整数位的特性),而需要留下来的那一位,就是sum % 10得到的余数.

For example: 

a->index[0] = 8;
b->index[0] = 9;
int sum = a->index[0] + b->index[0];
//此时sum = 17,意味着需要进1留7
int save_num = sum % 10; //即为7
int carry = sum / 10; //即为1
//我们可以创建一个新的titan_number用于储存结果,此处我们用c表示
//那么,我们就可以添加结果
c->index[0] = save_num
//接下来,在新的计算中,我们就需要带上carry
c->index[1] = a->index[1] + b->index[1] + carry;

事已至此,我们可以开始部分的代码实现了

void add_numbers(tn* a, tn* b, tn* result) { //输入两个加数a b和一个结果result
    result->size = 0; //初始化结果的数字长度为0
    result->is_negative = 0; //初始化结果的正负号属性
   
    if (a->is_negative == b->is_negative) {  // 我们先进行同号的计算,即同正或同服
        short carry = 0; //在加法中,最大值为18,所以用short储存可以节约点内存
        int max = a->size > b->size ? a->size : b->size; //找出最大的那个titan_number的长度
        result->is_negative = a->is_negative; //因为是同号,所以可以继承一下正负号
       
        for (int i = 0; i < max; i++) { //正式开始计算
        
            //如果超出了索引值,那就记录它为0
            /*这里需要多解释一下,为什么呢?
            比如我们记录两个titan_number分别为888和8888
            我们刚刚判断了长度最大的数字的长度,在这里很显然是8888的长度也就是4,
            那么,在for循环中,我们从0开始,到max - 1结束,总共循环max次
            也就是4次,那么,问题就出现了!
            对于前面的888,从0开始,到索引2结束(三位数0 1 2)
            而对于8888,从0开始,到索引3结束 (四位数0 1 2 3)
            此时我们再读取888的index[3]的时候,会出现越界的情况,这是一个Runtime Error!
            所以说,当循环的次数大于这个数组的最大索引时,我们就需要
            默认这里的数字为0。
            即,在888+8888中,我们的处理办法是
            0888 + 8888,这样可以保证长度相同,避免了越界问题*/
            
            int digit_a = (i < a->size) ? a->num[i] : 0; 
            int digit_b = (i < b->size) ? b->num[i] : 0;
            
            //计算总和,并加上前一位的进数
            short sum = digit_a + digit_b + carry;
             //计算保留的数并储存到result中
            result->num[result->size] = sum % 10;
            //计算进位的数保存到carry中,留给下一次循环计算
            carry = sum / 10;
            //给result的长度+1
            result->size++;
        }
        //当然,仔细分析代码你会发现一个Bug,如果在最后一次计算的时候
        //有进位的话,它是不会再进行处理的,即四位数+四位数永远得到四位数
        //所以我们需要在循环结束的时候再次进行判断
        //如果进位数>0,那就给index[size]的位置继续加上carry
        if (carry > 0) {
            result->num[result->size] = carry;
            result->size++;
        }
    }
}

异号相加

两个异号的高精度数相加呢?对于-a + b 或者a + (-b),其实我们可以直接用减法的逻辑去实现它,假定我们有一个减法逻辑的函数 minus_numbers(被减数,减数,结果) ,我们可以直接将其转换为一个减法过程,调换一下位置即可

比如在上述的例子中,我们可以用一个titan_number temp来储存一个取反号的高精度数,比如在“-a + b”中,我们可以令temp = *a,然后设置temp的is_negative属性为0,这样,我们就直接将这个问题简化为了一个同号相减 (b - temp) 的代码

               } else {  // 异号相加,转换为减法
                    tn temp;
                    if (a->is_negative == 1) {  // a是负数
                        temp = *a;
                        temp.is_negative = 0;
                        minus_numbers(b, &temp, result);
                    } else {  // b是负数
                        temp = *b;
                        temp.is_negative = 0;
                        minus_numbers(a, &temp, result);
                    }
                }

接下来,我们就要实现同号相减的代码了

同号相减

为了实现简化代码,我们可以先考虑同负相减(-a)- (-b) ,因为这样的算式可以简化为b - a,和前面提到的异号相加的逻辑相同

所以我们先判断入参的两个数的正负性,当他们都为负数时,继续设定两个temp,继承入参的a b的全部属性,然后修改is_negative为0,即将两个负数相减转化为两个正数相减。

void minus_numbers(tn* a, tn* b, tn *result) {
    result->size = 0;
    short carry = 0;
   
    // 处理负数减负数: (-a) - (-b) = b - a
    if (a->is_negative == 1 && b->is_negative == 1) {
        tn temp_a = *a, temp_b = *b;
        temp_a.is_negative = 0;
        temp_b.is_negative = 0;
        minus_numbers(&temp_b, &temp_a, result);  // 计算 b - a
        return;
    }

接下来,我们开始实现两个正数相减的代码。这时候出现了一个问题,两个正数相减得到的结果可能是正数也可能是负数,需要先判断被减数和减数谁大谁小。

我们先用最简单的判断,谁的长度更大谁的大小就更大

当然,如果长度相同时,我们就需要从高位向低位遍历,判断大小。如果两数完全相等,直接返回结果为0即可

  
    // 处理正数减正数的情况
    int is_a_larger = 0;
    if (a->size > b->size) {
        is_a_larger = 1;
    } else if (a->size < b->size) {
        is_a_larger = 0;
    } else {
        // 位数相同,逐位比较
        int i = a->size - 1;
        while (i >= 0) {
            if (a->num[i] > b->num[i]) {
                is_a_larger = 1;
                break;
            } else if (a->num[i] < b->num[i]) {
                is_a_larger = 0;
                break;
            }
            i--;
        }
        if (i < 0) {  // 两数相等
            result->is_negative = 0;
            result->num[0] = 0;
            result->size = 1;
            return;
        }
    }


紧接着,我们开始正式实现我们的减法逻辑,同前面的加法类似,先初始化进位(在减法中说借位更好一些,后面也会说借位),并初始化结果长度为0

    // 执行减法操作
    carry = 0;
    result->size = 0;

如果被减数更大,设定结果的is_negative属性为0,反之为1。

从低位到高位遍历,并进行减法,在is_a_larger为true时,我们很确信a的长度一定大于或等于b的长度,所以b可能会出现索引越界的情况,故对a我们可以直接设定索引为i,而b就需要判断i是否在b->size的范围之内,如果不在,将此处的数设定为0.

然后开始计算两数相减,接下来就需要思考一下了。如果二者的差小于0,那么,根据减法本身的规律,我们会向前一位借1,当作这一位的10使用,故当二者差小于0的时候,为了得到正确的结果,我们需要给差的结果+10。比如在7-9中,我们得到的是-2. 如果前面有位可借的话,他其实应当是17-9,结果为8,所以设定借位1

反之则不需要设置借位,直接设置结果即可。

    if (is_a_larger) {
        // a >= b, 结果为正
        result->is_negative = 0;
        for (int i = 0; i < a->size; i++) {
            int digit_a = a->num[i];
            int digit_b = (i < b->size) ? b->num[i] : 0;
            int diff = digit_a - digit_b - carry;
           
            if (diff < 0) {
                diff += 10;
                carry = 1;
            } else {
                carry = 0;
            }
           
            result->num[result->size++] = diff;
        }
    }

结果为负数的代码大部分类似。

             else {
        // a < b, 结果为负
        result->is_negative = 1;
        for (int i = 0; i < b->size; i++) {
            int digit_b = b->num[i];
            int digit_a = (i < a->size) ? a->num[i] : 0;
            int diff = digit_b - digit_a - carry;
           
            if (diff < 0) {
                diff += 10;
                carry = 1;
            } else {
                carry = 0;
            }
           
            result->num[result->size++] = diff;
        }
    }

当然,在减法中,最高的几位可能会全部变为0,比如在123-120中,最终的结果为3,而在我们的代码实现中,因为每次循环我们都会将size++,所以结果实际上是003,因而我们需要消除所有的前导0,并缩短长度值

     // 去除前导零
    while (result->size > 1 && result->num[result->size - 1] == 0) {
        result->size--;
    }
}


评论
正在加载验证组件