其他
计算机大数乘法引发的思考 | CSDN 博文精选
以下文章来源于CSDN ,作者dog250
作者 | dog250
责编 | 刘静
出品 | CSDN博客
计算201×33×707+484×636321×33×707+484×6363
原式=67×3×33×707+11×44×9×707=67×3×33×707+11×44×9×707=11×99×707=111×99×7×101=777×9999=7770000−777=7769223
以乘法竖式为例,如果我们将一次十进制一位乘法(即99乘法表的乘法)作为一个步骤,那么两个n位乘数相乘需要n的二次方个步骤,其时间复杂度就是O(n
2) ,但是如果我们采用某种“巧算”,那么计算步骤将会大大减少。// mul.c
// gcc mul.c -o mul
void inline carry_add(char *tmp, char num, int index)
{
char tmp_num = 0;
char carry;
tmp_num = tmp[index] + num;
if (tmp_num > 9) {
carry = tmp_num / 10;
tmp[index] = tmp_num%10;
carry_add(tmp, carry, index-1); // 递归进位到底
//tmp[index - 1] += carry; // 当次进位不能保证tmp[index - 1]+'0'是一个字符
} else {
tmp[index] = tmp_num;
}
}
int mul(char *mul1, char *mul2, char *result)
{
int i, j, mid;
int len1, len2, len, pos = 0;
char *tmp;
len1 = strlen(mul1);
len2 = strlen(mul2);
len = len1 + len2;
tmp = (char *)calloc(len, 1);
for (i = 0; i < len2; i++) {
for (j = 0; j < len1; j++) {
int idx = len - j - i - 1;
mid = (mul2[len2 - i - 1] - '0') * (mul1[len1 - j - 1] - '0');
// 这里我是在计算过程中直接递归处理进位的,而不是在一轮乘法后再用一个for循环处理。
carry_add(tmp, mid, idx);
}
// 我不需要在这里用for循环统一处理进位。
// Nothing todo!
}
i = 0;
while(tmp[i++] == 0) pos++;
len = len - pos;
memcpy(result, tmp + pos, len);
free (tmp);
for (i = 0; i < len; i++) {
result[i] += '0';
}
return 0;
}
int main(int argc, char **argv)
{
int len1, len2, i, count;
char *m1, *m2, *result;
m1 = argv[1];
m2 = argv[2];
count = atoi(argv[3]);
len1 = strlen(m1);
len2 = strlen(m2);
result = calloc(len1 + len2, 1);
// 为了比较速度,这里循环执行count次。
for (i = 0; i < count; i++) {
memset(result, 0, len1 + len2);
mul(m1, m2, result);
}
printf("%s\n", result);
free(result);
return 0;
}
大致就是这个意思。我们试一下这个程序:
[root@localhost ]# ./mul 23567971209865125789034451795247 12345678909988776655443314719047 1
290962605116854555936789385617202938185315195749798588574969609
结果对不对开始我也不知道,不过从算法的执行过程上看,以一次简单乘法计数,这个算法的时间复杂度是O(n2)的,这种算法基本是要被毙掉的,所以必须进行优化。
// mul2.c
// gcc mul2.c -o mul2
void inline carry_add(char *tmp, char num, int index)
{
char tmp_num = 0;
char carry;
tmp_num = tmp[index] + num;
if (tmp_num > 9) {
carry = tmp_num / 10;
tmp[index] = tmp_num%10;
carry_add(tmp, carry, index-1);
} else {
tmp[index] = tmp_num;
}
}
// 处理大数加法
int add(char *s1, int len1, char *s2, int len2, char *result, int *ppos)
{
int i = 0, j = 0, len;
char *c;
len = len1;
if (len2 > len)
len = len2;
for (i = len - 1; i >= 0; i--) {
unsigned char tmp;
if (len1 > len2) {
tmp = s1[i] - '0';
if (i > len1 - len2 - 1)
tmp += s2[i - (len1 - len2)] - '0';
} else {
tmp = s2[i] - '0';
if (i > len2 - len1 - 1)
tmp += s1[i - (len2 - len1)] - '0';
}
carry_add(result, tmp, i + 1);
}
*ppos = 1;
if (result[0] != 0) {
len = len + 1;
*ppos = 0;
}
for (i = 0; i < len + 1; i++) {
result[i] += '0';
}
return len;
}
// 处理大数乘法
int zone_mul(char *mul1, char *mul2, int len, char *result, int result_len)
{
int i, j, n = 0, reslen, totlen, pow1size, pow2size, pos = 0, nblocks = len / 8;
unsigned long m1, m2, tmp_res;
char str1[10], str2[10], resstr[20];
char *pow1, *pow2, *tmp_result;
tmp_result = calloc(result_len, 1);
pow1 = calloc(len*2, 1);
pow2 = calloc(len*2, 1);
// 按照每8位十进制数字进行分割计算。
for (i = 0; i < nblocks; i++) {
memcpy(str1, mul1 + len - i*8 - 8, 8);
m1 = atoi(str1);
for (j = 0; j < nblocks; j++) {
memcpy(str2, mul2 + len - j*8 - 8, 8);
m2 = atoi(str2);
tmp_res = m1*m2;
// 计算补多少零,也就是乘以10的几次方
pow1size = i*8;
pow2size = j*8;
totlen = reslen = sprintf(resstr, "%lu", tmp_res);
totlen += pow2size;
memset(pow2, '0', totlen);
memcpy(pow2, resstr, reslen);
reslen = totlen;
totlen += pow1size;
memset(pow1, '0', totlen);
memcpy(pow1, pow2, reslen);
memset(result, 0, n + pos);
// 累加一次计算结果,执行大数加法
n = add(pow1, totlen, tmp_result, n, result, &pos);
memcpy(tmp_result, result + pos, n);
}
}
memset(result, 0, n + pos);
memcpy(result, tmp_result, n);
free(tmp_result);
free(pow1);
free(pow2);
}
int main(int argc, char **argv)
{
int len1, len2, i = 0, count;
char *m1, *m2, *result;
m1 = argv[1];
m2 = argv[2];
count = atoi(argv[3]);
len1 = strlen(m1);
len2 = strlen(m2);
result = calloc(len1 + len2, 1);
for (i = 0; i < count; i++) {
memset(result, 0, len1 + len2);
zone_mul(m1, m2, len1, result, len1 + len2);
}
printf("%s\n", result);
free(result);
return 0;
}
我们来比试一下效果,计算5000次同一个大数乘法:
[root@localhost ]# time ./mul 119334567890334449388883313579158334567098134455 667908995633221198765432134678040000123411113456 50000
79704631383957730438879843848804741889926116047138197998269353980447530720116354515911947726480
real 0m1.891s
user 0m1.889s
sys 0m0.001s
[root@localhost ]# time ./mul2 119334567890334449388883313579158334567098134455 667908995633221198765432134678040000123411113456 50000
79704631383957730438879843848804741889926116047138197998269353980447530720116354515912427726480
real 0m1.475s
user 0m1.472s
sys 0m0.001s
[root@localhost ]#
对于计算机而言, 用计算机力所能及的多位乘法代替人脑的一位乘法 会减少很多的计算步骤,多位乘法对于计算机而言并不苛刻,只要在它的内建支持范围内。就像我们计算99乘法一样,你不会觉得9×9 9\times 99×9比1×1 1\times 11×1更难。
201×33×707+484×6363
并不是说有了2048位字长的计算机就可以暴力破解RSA了,而是说有了2048位字长的计算机之后,大数乘法的开销就被压缩了,按照nlogn倍压缩掉了。遍历2048位解空间的开销丝毫不受影响,受影响的只是拆解,计算2048位大数(2048位字长的计算机中不叫大数了…)的开销。换句话说,RSA暴破难题包括两部分,一部分是数学上的,这是由数学决定的,另一部分是实现上的计算开销,这个开销受计算机结构,字长,时钟频率,算法等一系列因素影响,如果实现了2048位字长的计算机,这些开销将会大大降低,如果是量子计算机,2048位解空间可以并行开解,那就更快了,但是也丝毫没有动摇RSA算法的数学基础。
有人问我在同样O(nlogn) 的时间复杂度情况下,为什么快速排序比归并排序快,我没有办法证明,但是事实上的原因却是非常显然的: 详见:不知为不知–信息论和最大熵原则 :https://blog.csdn.net/dog250/article/details/78944526
随机的就是最好的!
☞100 美元一行代码,开源软件到底咋赚钱?☞我优化多年的 C 语言竟然被 80 行 Haskell 打败了?
☞为何Google、微软、华为将亿级源代码放一个仓库?从全球最大代码管理库说起