实验描述
-
问题描述
大整数运算是现代密码学算法实现的基础,重要性不言而喻。大整数我们指的是二进制位 512、1024 和 2048 的数,一般的语言不支持。
-
基本要求
以类库头文件的形式实现。
-
实现提示
在选择了大整数的存储结构之后,主要实现以下运算:
- 模加;
- 模减;
- 模乘;
- 模整除;
- 模取余。这五种运算模拟手算实现。
- 幂模:利用“平方-乘法”算法实现。
- GCD:利用欧几里得算法实现。
- 乘法逆: 利用扩展的欧几里得算法实现
- 素数判定与生成
实验分析
共九种运算,其实幂模、GCD、乘法逆都是基于前五种运算来实现的,Miller Rabin 的素性检测算法我会另写一篇博客进行介绍,免得内容太过繁杂。
前五种运算的代码来源于 https://github.com/faheel/BigInt ,我认为这是非常不错的参考项目,我个人在写大整数包的时候有几种情况没有考虑到,导致后面的乘法逆出现了很大的问题,github 很多大整数项目也可以综合参考一下,不过我在借鉴时发现有些犯了跟我同样的错误,所以请读者自行甄别。
这个项目实现了非常多的大数函数,比起简单的”完成作业“地去做实验,不如让我们思考如何在实际生活中去做出一个”真正有用且方便“的大整数包,这才是我把这些代码放在博客中进行分析的目的。
类定义
那么,大整数要用什么来存储呢?
当然是 string 字符串,但负数呢?
这就需要有一个变量来存储符号位。
std::string value;
char sign;
既然要求是以类库头文件的形式实现,我们就定义一个 BigInt 类。
类的构造函数我们考虑空(默认)、BigInt
、 long long
以及 string
来定义大整数的情形。
// 默认情况下正零 value="0";sign='+';
BigInt();
// 赋值
BigInt(const BigInt &);
// 第一步判断数是否小于 0 ,小于则 sign='-';value=value.substr(1); 取符号位后的值
BigInt(const long long &);
// 首先判断是否有符号位,我们在输入的时候正数不会带 '+' 来表示,没有的 sign='+'
// 再而检测字符串的每一个字符是否都是数字,是否是有效输入
// 最后去除可能输入进来的前导零
BigInt(const std::string &);
与此对应的,我们在运算符重载时也要考虑三种类型定义的大整数进行运算的情况,为了简便,只在此讨论 BigInt
作为对象重载的情况,其他都可以类推出。
大数加法
加法一般有三种情况:
- 正 + 正
- 正 + 负(负 + 正)
- 负 + 负
第一种和第三种情况都是符号不变,值相加。
而第二种实际上是在做减法,所以这里不予讨论。
接下来我们以 正 + 正 作为示例来模拟一下加的过程,这里有几种情况要考虑:
-
位数不同
在手算的时候,比如 1234 + 12 ,我们都是从低位加起,最后两位可以算出没有进位,是 46 ,那 1234 多出的前两位怎么办?直接抄下来,也就是相当于 1234 + 0012 。
所以在位数不同时,我们要对长度短的数高位补零,使其变成 位数相同 的情况。
-
位数相同
这就要考虑进位,举个特殊情况,最高位是 9 + 9 ,计算得 18 ,这一位的结果肯定是 8 然后进 1,进位就表示成整除 10,8 就是模 10 的结果了,因为是最高位,进位的 1 直接抄下来加在最前面。
BigInt BigInt::operator+(const BigInt &num) const {
// 正 + 负 做减法
// 把加法化为减法 左 - 右 的形式
if (this->sign == '+' and num.sign == '-') {
BigInt rhs = num;
rhs.sign = '+';
return *this - rhs;
}
// 这是 负 + 正
else if (this->sign == '-' and num.sign == '+') {
BigInt lhs = *this;
lhs.sign = '+';
return -(lhs - num);
}
// 标识数字大小,长度不一高位补零,长度相同判断大小
std::string larger, smaller;
std::tie(larger, smaller) = get_larger_and_smaller(this->value, num.value);
BigInt result;
result.value = "";
// carry 是进位,sum 是每一位相加最后的结果
short carry = 0, sum;
// 数值相加,从低位开始
for (long i = larger.size() - 1; i >= 0; i--) {
sum = larger[i] - '0' + smaller[i] - '0' + carry;
result.value = std::to_string(sum % 10) + result.value;
carry = sum / (short) 10;
}
// 最高位产生进位的情况
if (carry)
result.value = std::to_string(carry) + result.value;
// 负 + 负
if (this->sign == '-' and result.value != "0")
result.sign = '-';
return result;
}
大数减法
三种情况:
- 正 - 正
- 正 - 负(负 - 正)
- 负 - 负
同理,第二种情况其实就是加法。
在位数不同的情况下,同样需要我们高位补零,位数相同时就要注意借位,借位过程大家可以以 102 - 9 手算模拟一下,实际上是一个循环借位的过程。
BigInt BigInt::operator-(const BigInt &num) const {
// 化为加法
if (this->sign == '+' and num.sign == '-') {
BigInt rhs = num;
rhs.sign = '+';
return *this + rhs;
} else if (this