本章较长,我会尽可能地讲详细,可根据文章目录检索
接上回
接续上回,我们实现了将数字保存为高精度数字,让我们一起回忆一下代码
#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:
| index | 0 | 1 | 2 | 3 |
| number | 4 | 3 | 2 | 1 |
b:
| index | 0 | 1 | 2 | 3 |
| number | 1 | 2 | 3 | 4 |
二者的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--;
}
}
评论