上一篇文章简单讲解了移位操作是如何进行的,最后简要分析了算法时间复杂度。这次介绍绝对值加法,这个算法是构建有符号数加法减法的重要模块之一。绝对值计算的算法是高度优化的,本身的时间复杂度为 O(n),但是如果被高级的算法调用,其复杂度很容易被提高到 O(n^2) 甚至 O(n^3)。绝对值加法只计算两个大整数的绝对值之和:z = |x| + |y|,之所以要先解决绝对值的计算,是因为有符号的加法或减法都可以先通过计算绝对值的和或差,然后再修改符号位来实现。
★ 计算原理
大整数的绝对值加法和小学数学的笔算加法没有什么区别,只是进制不同而已。核心操作是:从低位到高位将相同的数位进行相加,进位传递到下一位。所有操作累加和进位操作完成后,将结果的高位清零,并且将符号位 sign 设置为 1 (绝对值相加,结果必为非负整数)。
★ 实现
下面给出绝对值加法的实现代码。需要注意的是,这个算法里面使用了好几个宏定义,而且都是位于关键的操作之上,之所以这样,是因为在某些环境下,基本的数据类型有单精度类型和双精度类型,而在另一些环境下,只有单精度类型。例如 32 位系统下,有 32 位的单精度整形和 64 位的双精度整形;而在 64 位系统下,只有 64 位的单精度整形,没有 128 位的双精度整形。在不同的环境下,其核心操作是略有不同的。使用宏定义,可以方便地在不同环境之间进行切换。后面会详细讲这些宏定义的工作原理。
int bn_add_abs(bignum *z, const bignum *x, const bignum *y) { int ret; const bignum *tmp; bn_digit *px, *py, *pz; size_t i, max, min, olduse; ADDC_INIT if(x->used > y->used) { max = x->used; min = y->used; tmp = x; } else { max = y->used; min = x->used; tmp = y; } olduse = z->used; z->used = max + 1; BN_CHECK(bn_grow(z, z->used)); px = x->dp; py = y->dp; pz = z->dp; for(i = 0; i < min; i++) { ADDC_CORE px++; py++; pz++; } if(min != max) { for(; i < max; i++) { ADDC_FINAL pz++; } } ADDC_STOP for(i = z->used; i < olduse; i++) *pz++ = 0; z->sign = 1; bn_clamp(z); clean: return ret; }
算法第一步是找出数位较大的正数,并且让指针 tmp 指向它。第二步是增加目标整数的精度,设定三个别名指针分别指向输入整数和输出整数的 dp 来提高内存的访问效率。第三步是通过一个循环将位数较小的整数累加到较大的整数上,并将结果存储到目标整数的相应数位中,然后使用一个额外的循环和 ADDC_STOP 宏定义传递累加产生的进位,最后将高位清零,符号位设置成 1 和压缩多余的数位完成计算。
相关的宏定义:
#ifdef _MSC_VER #define LL(v) (v##ui64) #else #define LL(v) (v##ULL) #endif #define BN_HAVE_LONG_LONG #define BN_MASK0 LL(0xFFFFFFFFFFFFFFFF) #else #define BN_MASK0 0xFFFFFFFFUL #endif
这部分定义了相关的掩码来提取结果的某些比特位。
#ifdef BN_HAVE_LONG_LONG #define ADDC_INIT bn_udbl rr = 0; #define ADDC_CORE rr += (bn_udbl)(*px) + (*py); *pz = (bn_digit)(rr & BN_MASK0); rr >>= biL; #define ADDC_FINAL rr += (bn_udbl)(tmp->dp[i]); *pz = (bn_digit)(rr & BN_MASK0); rr >>= biL; #define ADDC_STOP z->dp[max] = (bn_digit)(rr & BN_MASK0); #else #define ADDC_INIT bn_digit rr, t, c = 0; #define ADDC_CORE t = *px; t = (t + c) & BN_MASK0; c = (t < c); rr = (t + *py) & BN_MASK0; c += (rr < t); *pz = rr; #define ADDC_FINAL t = tmp->dp[i]; t = (t + c) & BN_MASK0; c = (t < c); *pz = t; #define ADDC_STOP z->dp[max] = c; #endif
这部分宏定义是算法的关键之处。首先 ADDC_INIT 指明在不同环境下有那些变量被定义,紧接着 ADDC_CORE 定义了在不同环境下的累加操作应该如何执行,然后 ADDC_FINAL 确定了该如何执行累加完成后的进位传递操作,最后 ADDC_STOP 用于传递最后一个进位。下面讲讲在两个不同的环境下这几个宏是如何执行的。
A. 单精度和双精度变量都存在的环境:
1. ADDC_INIT 定义双精度类型的变量 rr 用于存放累加的结果。用双精度的原因是单精度累加后结果可能要用双精度类型才能完整存储,简单的数学证明如下:
设单精度类型是一个 n 位的整形,则能表示的最大无符号整数是:2^n - 1,两个最大整数相加,结果是:2 * (2^n - 1) = 2^(n + 1) - 2,结果超出单精度类型所能表示的范围,因此至少需要双精度类型才能完整表示。
2. 在 ADDC_CORE 中,先计算 x 和 y 每一位的和,并和来自低位的进位相加,结果放在 rr 中;然后将 rr 和掩码 BN_MASK0 相与提取出本位,存放在目标整数的对应数位上;最后将变量 rr 右移 biL 位计算出进位结果。记住 rr 是双精度的,累加结果的低半部分是本位结果,高半部分是进位结果。
3. 在 ADDC_FINAL 中,数位多的整数的剩余数位与来自低位的进位相加,然后从结果中提取本位存储到目标整数的对应数位上,最后 rr 右移 biL 位计算新的进位值。
4. ADDC_STOP 宏将剩余的最后一个进位传递到结果的最高数位上。注意,这个进位可能是 0,也有可能不是 0。
B. 只有单精度的环境:
1. ADDC_INIT 宏定义了三个变量:累加的本位结果 rr, 临时变量 t, 进位值 c。
2. 这里的 ADDC_CORE 计算稍微复杂点。第一步,将 x 的某一位存放到临时变量 t 中。第二步,变量 t 和来自低位的进位 c 相加,得到结果的本位值,存放于变量 t 中。这里要注意的是所有变量都是单精度类型,如果结果大于单精度数所能表示的范围,则会产生溢出,相当于做了一次 mod 2^n 运算,所以得到的是结果的本位值。第三步,判断前一步是否产生了进位。在加法中,如果结果产生进位,则本位的值小于两个加数。所以,如果有进位,则 t < c,比较的结果为 1。第四步,计算 t 和第二个整数对应数位的和,结果的本位存放于变量 rr 中。第五步,判断是否产生进位,原理和第三步一样。最后一步,将最终的本位结果送到目标整数的对应数位上。
3. ADDC_FINAL 用于传递剩余的进位,和上面的 ADDC_FINAL 原理一样,只不过是本位和进位是用两个变量表示而已。
4. ADDC_STOP 宏将剩余的最后一个进位传递到结果的最高数位上。仍然要注意,这个进位可能是 0,也有可能不是 0。
上面的内容看起来有点凌乱,如果对 C 的数据类型不是很了解,不明白溢出是怎么回事的话,可以先去看看相关的章节恶补一下。
★ 总结
上面的代码为了效率和移植方便,所以把关键的地方拆分出来单独写,因此看起来不是很直观,可读性不是很好。不过加法的原理十分简单,只要明白其中的计算原理,仍然可以很快弄懂。下一篇文章将介绍绝对值减法。
【回到本系列目录】
版权声明
原创博文,转载必须包含本声明,保持本文完整,并以超链接形式注明作者Starrybird和本文原始地址:http://www.cnblogs.com/starrybird/p/4359221.html