以上这样看起来似乎非常非常的简单,但实际上如果再往深一层考虑的话是可以扯出很多东西来的。就这个 int a = 1 而言,显然是限定了这个变量 a 是一个 int 型,也就是熟知的数据类型。
在 数据结构 中对高级语言的数据类型的解释是 一组性质相同的值的集合和定义在该值集上的一组方法的总称 。从中可知,这个 值集 是有范围的 ,而且根据操作系统而定。一般来说,如果是大于等于 32 位系统, int 类型的大小为 4 个字节;对于小于 32 位系统, int 类型的大小为 2 个字节(这也是为啥有些C教材里面的关于 int 类型的答案是 2 字节长度)。
当然,就目前来说吾等人的想法并不想深究这个玩意,只是表示假设想写的所有内容都是以 64 位为例(相当于指针大小为 8 位大小,可以用 unsigned long long int 来强转指针的歪门邪道 随便说说)。另外下述的所有 long 实际上和 long int 以及 int 一致的,都是 4 个字节,而 long long int 是 8 个字节。回归来说,上面的 int a 的最大大小为 4 个字节,而又由于这是一个有符号数,则第 1 位用来表示符号,这也就是其值集为整数 -2^31 ~ 2^31-1 的原因, 这也就是什么 补码反码 的内容了,比方说 负数值(原码) = 补码 - 1 AND 取反 ,当然,无论如何,符号位永远不变!
那么瞎扯完毕,对于 int a 运算的值的有效计算位数为 4 * 8 - 1(符号位) = 31 位,凡是无论多大的整数 N(位数很高的整数)都会自然而然的强行在化为 int a 类型的值集范围(假设看作强转成与 int 位数相同的类型)内进行运算,并且当 N 的位数 bit(N) 有 bit(N) >= 8 时(不包括 N 的符号位)那么 a 的符号位也会被计算到,这实际上可以人为地将 N 的低 8 位看成 unsigned int 类型,所以说当所要计算的数足够大时,会改变原有数值如 a 的符号(溢出),这是不可接受的。
正因为高级程序设计语言所提供的 数据类型 在很多时候无法满足一些 “大宇宙” 问题(如某某曾对有关如何对全宇宙的所有事物进行排序时采用归并排序的荒谬思路),说简单点比如关于素数 P 的判定充要条件
其中 β 为修正值,因为每次做加法运算都有可能出现进位的情况,而 M 和 N 的值只与最大数值本身有关,这是很不好的结果。
那么,若以初中的思维来看一下有关数 R = P * Q 的计算过程,众所周知,乘法是符合 结合律 和 分配律 的,不妨先以 结合律 的思想来做一个简单讨论。
此时若设对任意正整数 K > 0 ,有 R = P * (K * Q/K) ,也可看作
设
还可看作
或
此时也就可以人为地对通过对 K 值的构造来找到特定的、能使得对 P 和 Q 有利(或只对 P 或 Q 有利,或相比直接计算更效率)的情况。
但是,由于 P 和 Q 都同为大整数,而上述式子所述的 K 值显然得满足为 Q 的非平凡因子,否则得到的结果会出现浮点数或无意义;同时,即便是找到了对应的 K 值,在计算 K/Q 时仍需要解决另一大与乘法类似的问题——除法(若将除法看作是乘倒数),这有点就像是进入了一个递归;并且若以上述方式作为分解,并不一定能够降低过程中进行加法运算的数量,甚至可能会比直接计算更多。
其中 |S| 为集合 S 的元素数量,这样就使得在计算总和时进行加法运算的数量有可能得以减少。再次,对于 Ci 序列,不难看出总有 Ca < Cb 当且仅当 a < b ,这就意味着若从 Q 的最高位开始求集合 S ,所得到的结果是 有序 的(降序),而对于以二进制表示的计算形如 (2^Ci)*P 实际上可以看作是 P 的二进制表示向左移动 Ci 位;若结合 有序 集合 S ,那么设
则后一个结果 Ca 就能由前一个结果 Cb 通过向右移动 b - a 位得到。即对集合 S 内的所有
只需在通过 P 由高到低移位得到第一个(设为 Cb)结果后,其直接后继的值(设为 Ca)可通过其直接前驱 Cb 移动 b - a 位得到,在最坏情况下,构造该集合 S 的 时间复杂度 为
其实就是会多计算 Ca 或 Cb的值,因为在集合 S 中无相同的值,即 Ca != Cb ,因此在进行加法运算时,按照 低位对齐,各位作和 的规则,最坏的情况是指计算过程进 1 位的情况,此时所进行计算的 位 长度就为 Max{Ca,Cb} + bit(P) + 1 。
那么由此可以得出,若在最坏的情况下,整个乘法运算所需要的 时间复杂度 约为
或
即主要还是和 Q 有关,且主要影响的是在集合 S 的构成部分。如果使用该方法,实际运用时可以考虑分解较小的乘数。
幂次乘法
相较于简单加法和简单乘法,对幂次乘法来说,更像是在简单乘法的基础上再做乘法。不妨还是先以小学的角度来看有关幂次乘法的计算,如给定一个 R = P^Q 自然而然地想到,相当于 Q 个 P (或 P 个 Q)进行相乘得到的结果。而乘法运算在如此角度来看,原型为多个数值的累加,以此再进行分解,将第 i 趟 P 自乘得到的结果记为 L{i} ,则可以得到递推式
即
若以这种思想进行计算,每一趟乘法都需要做 P 次的加法运算,设 U{i}{j} 为在做第 j 次加法运算时第 j-1 次得到的和,在最坏的情况下,第 i 趟乘法所作的加法运算的 时间复杂度 为
由于对第 i 趟乘法中 L{i} 的每一次加法运算不可能每一次都会进位,所以 ɑ 仅作为一个修正值看待。
实际上,若设 i 为算法的第 i 层递归调用(若将首次调用看作是第 1 层),则将当前层 Q的值即可看作是 Ki ,且满足
此时也就可以通过将 Q 通过向右移位 i 层来获取当前层的 Ki 值,可写出以下递推函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Integer pow2(Integer P, Integer Q){
if(Q == 0) return1; Integer len = 0; Integer R = P; for(Integer i = Q; i > 0; len ++,i = i >> 1); for(len = len - 2; len >= 0; len --){ R = R * ((Q >> len)&1 ? R * P : R); } return R; }
在这个算法中多了一个 len 变量,用以记录 Q 值二进制表示的 位 的长度,然后通过对Q 和 len 的相关计算来模拟上述递归算法中第 len - 1 层递归的情况,以便决定是否乘上 P (补 “1”)。
总的来说相比递归算法其 空间复杂度 降低到了
但相对的,需要知道 Q 值二进制表示的 位 的长度,多了一层计算,则一般 时间按复杂度 为
实际上,还可以通过位运算来规避求层数的问题,如以下函数
1 2 3 4 5 6 7 8 9 10 11 12
Integer pow3(Integer P, Integer Q){ if(Q == 0) return1; Integer R = P; for(Integer i = Q >> 1; i > 0; i = i >> 1){ R = R * ((Q^i)>Q ? R : P * R); } return R; }
这样可进一步降低 时间复杂度
不过,这对乘法运算的次数 E 并没有影响。
若设 C 为序列 K 中奇数的数量,显然有 C ≤ bit(Q),不妨取 C = bit(Q)/2 ,则乘法运算的次数 E 可表示为
即在对幂次为 Q 的求幂算法中,只需要做 E 次乘法,这相较于直接做连乘是有很大改进的。
猜想
在上一小节中,是将 Q 分解成了关于 2*Kn 的递归式,这样总可以找到
即在 K 序列中,任何值不相等且是 有序 的,若设 Ki 是 Kj 的直接前驱,当知道了 Ki 的值,通过很简单的计算就能得到 Kj 的值。
但从另一个角度来看,由于 Q 是属于非负整数值集,如果设 [*] 为向下取整符号,则将其分解成
也就是说若能确定一个 C 的值使得该等式成立,所需要的乘法运算次数就能降低至
那么解决该问题的关键是如何求 C 值,并且 C 值最好能由 Q 或在求 P^(2^[log2(Q)]) 的过程中通过简单的计算得到,但似乎不太可能。
接着来讨论如何对某个字节的指定 位 进行操作,若设需要操作的位为第 n 个字节的第 i 位,且 n{i} 为第 n 字节的第 i 位(后文皆同)。当期望该 i 位的值为 0 时,需要将第 n 个字节和一个值为 2^i 的修正字节 m 的取反做与运算,即构造除第 i 位为 0 其他位全为 1 的字节;而当期望该 i 位的值为 1 时,则需要将第 n 个字节和一个值恰好为 2^i 的修正字节 m 做或运算,保证除了第 i 位为 1 外,其他位都为 0 。
至于修正字节 m 可以由 0x80 向右移动 8-i 或 1 向左移动 i 位得到,以第二种方式为例,可得
tp = 0; tv = 7; tu = 0; tc = 0; while(tu < tl){ while(tp < 8 && tc < more->bits){ if(itp < 0){ itu--; itp = 7,itv = 0; } it.pointer[itu] = ((more->pointer[tu] >> tv) & 1) ? it.pointer[itu] | (st<<itv) : it.pointer[itu] & (~(st<<itv)); tc++; itv++,tv --; itp--,tp ++; } tp = 0; tv = 7; tu++; } it.bits = tc + im; // 分解 while(im >= 0){ if(less->bit(im)){ flag = false; iu = it.len() - 1; ip = (it.bits + 8 - 1) % 8; iv = 7 - ip; tu = 0; tp = 0; tv = 7; // 对分配律各项做和 while(iu >= 0){ while(ip >= 0){ if(tp >> 3){ tu ++; tp = 0; tv = 7; } a = (it.pointer[iu] >> iv) & 1; b = (t.pointer[tu] >> tv) & 1;; t.pointer[tu] = (flag+a+b)&1 ? t.pointer[tu] | (st<<tv) : t.pointer[tu] & (~(st<<tv)); flag = (flag && (a || b)) || (a && b); ip--,iv++; tp++,tv--; } ip = 7; iv = 0; iu --; } // 若存在局部进位 while(flag){ while(tp < 8 && flag){ a = (t.pointer[tu] >> tv) & 1; t.pointer[tu] = (flag+a)&1 ? t.pointer[tu] | (st<<tv) : t.pointer[tu] & (~(st<<tv)); flag = (flag && a) || a; tp ++; tv --; } tp = 0; tv = 7; tu ++; } } im--; it.bits--; } // 若存在整体进位 t.bits = t.bit(t.size - 1) ? t.size : t.size - 1; return t; } };
理论上使用该链串法可以极大节省存储空间,即对于一个非负整数 P 而言,若设 [*] 为向上取整符号,S(*) 为对应类型的长度,则用该方法需要的字节数组空间为
所占的 位 空间大小还是比较接近 log2(P)。
字节法
在上一小节的链串法中通过修正字节 m 和对应字节的 n 运算来对第 i 个位进行运算,虽然仅仅是位运算级别的移位运算,但当数足够大时造成的效率损失仍然是有的。如在做 P + Q 运算时,若设 C 为两数在进行位运算时修正字节 m 的移位总数量,在最坏的情况下,即存在进位的情况下,且每个字节数组都填满且都进行了对应位运算,容易得出
// 若存在局部进位 while(flag){ a = it.pointer[iu]; it.pointer[iu] = a^flag; flag = (flag && a) || a; iu++; } // 若存在整体进位 it.len = it.pointer[it.size - 1] ? it.size : it.size - 1; return it; } // 乘法运算 Integer operator*(Integer i){ if(empty() || i.empty()){ Integer it; it.zero(true); return it; } Integer it(this->len + i.len); Integer t(this->len + i.len); // 优先分解较小的乘数 Integer *less,*more; if(this->len < i.len){ less = this; more = &i; }else{ less = &i; more = this; } bool a,b; bool flag; int tl = more->len; registerint im = less->len - 1; registerint tu,itu; // 高下标存储低位 tu = 0, itu = tl - 1; while(tu < tl){ it.pointer[itu] = more->pointer[tu]; tu ++,itu --; } it.len = im + tl; // 分解 while(im >= 0){ if(less->pointer[im]){ flag = false; itu = it.len - 1; tu = 0; // 对分配律各项做和 while(itu >= 0){ a = it.pointer[itu]; b = t.pointer[tu]; t.pointer[tu] = (a+b+flag)&1; flag = (flag && (a || b)) || (a && b); itu--, tu++; } // 若存在局部进位 while(flag){ a = t.pointer[tu]; t.pointer[tu] = a^flag; flag = (flag && a) || a; tu++; } } im --; it.len --; } // 若存在整体进位 t.len = t.pointer[t.size - 1] ? t.size : t.size - 1; return t; } };
相比于链串法,使用字节法明显代码看起来少了很多,但相对的所占用的存储空间也有所增大。对于一个非负整数 P ,若设[*] 为向上取整符号, S(*) 为对应类型的长度,可以表示为
所占的 位 空间大小约为 8*log2(P)。
测试
大整数加法
为检测这两个算法对大整数加法的计算效率,如可先分别生成长度为 1000 的大整数序列 A 和 B,然后依次进行加法运算,最后比较所使用的总时间。
若设 i 为大整数序列 A 或 B 的第 i 个大整数,则有
测试伪代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
voidtest(){ Integer A,B,C; Time begin,end,Total = 0; for([1..1000]){ A,B = Random(); { begin = Now(); C = C + A + B; end = Now(); } A,B = zero(); Total = Total + (end - begin); } }
voidtest(){ Integer A,B,C; Time begin,end,Total = 0; for([1..1000]){ A,B = Random(); { begin = Now(); C = A * B; end = Now(); } A,B = zero(); Total = Total + (end - begin); } Total = Total / 1000; }