最近在学习一些资料的时候正好看到一些和大整数求余数相关的问题,这些问题粗粗看来似乎有点麻烦。但是当结合一些有关数学的特性来分析时,会觉得很有意思。
问题1: 求一个整数X的N次方除以某个整数P的余数。用数学公式表示则如下:
其中N >= 0, P > 0.
这个问题需要考虑的就是如果N比较大的时候,很可能就超出我们所用一般数据类型所能表示的范围。如果直接去求X的N次方,就算有数据能保存的下来,肯定也会消耗大量的时间和空间。
问题2: 给定一个很大的数,求它除以某个整数P的余数。这个数因为足够大到没办法用普通的数据类型来表示,所以需要用一个整数类型的数组或字符串来保存。结果也是要求X mod P
我们先来看第一个问题。这个问题假定X和P并不是太大,可以用一个计算机的常用数据类型来表示。一种最简单直白的方法就是我们直接将所有N个X相乘,然后再对被除数P相除,求余数。当然,这是基于一个前提,我们有能够保存足够大的数据类型。如果我们对这种思路的时间和空间复杂度做一个粗略的估计的话,会发现,假设X是int类型的整数,占4个字节,而最坏的情况就是每次相乘的结果就占用的结果增加4个字节,这样N次乘积就需要占用4N字节的空间。而如果算上每次相乘的中间结果,占用的空间就达到N*N的量级。再看时间复杂度,假定两个int类型的整数乘积的运算时间单位为1的话,在没有任何优化假定的前提下,一个32位整数和64位整数乘积的时间则为原来的两倍。如果以这个标准来分析的话,后面的时间复杂度也到了N*N量级。
可见,虽然前面这种办法虽然理论上可行,但是实际上时间和空间复杂度太大,不太合适。现在我们再来看看另外一种思路。因为问题的关键就是指数N比较大,如果能将指数能够降下来,将其转换成对等的表达式,则问题就好解决了。我们看前面求乘积的过程,假定是最简单的情况,N =2,则相当于求(X * X) mod D. 如果利用整数求余数的性质,我们发现他们满足下面的性质:
这个等式的证明可以参照相关的数学材料或者文章后面的补充证明部分。通过这个性质,至少我们可以发现,对于两个数的乘积求余数,我们可以先求一个数的余数,然后再将这个余数乘以另外一个数再求余数。这样就可以求出来两个数乘积的余数。那么,如果对于3个,4个甚至更多的数的乘积求余数呢?我们可以将这个等式扩展一下,对于3个数的乘积,我们可以先求出前面两个数乘积的余数,再和第三个数相乘求。依次类推,重复N次就可以求出N次方的结果。于是,基于这种思路,我们可以写出如下的代码:
public static long power(long x, long n, long p)
{
if(n == 0)
return 1;
long tmp = x % p;
for(long i = 0; i < n - 1 ; i++)
{
tmp = (x * tmp) % p;
}
return tmp;
}
这种方法和前面的思路比起来,有一个进步的地方,就是每次运算的时候都对中间结果求模运算,使得结果都在普通数据类型可以保存的范围内,这样不会需要额外的存储空间。而时间的复杂度主要取决于运算的指数,所以时间复杂度为o(N)。这样我们就找到了一个还不错的解决方法了。
前面的办法虽然是已经在一个o(N)的范围了,可是如果N很大的话,我们还是要做一个很大的循环运算。还有没有可能使得我们的方法更加有效率呢?我们求X的N次方,可以根据N的性质做如下的分析:
当N为偶数的情况下:
那么,就有如下的等式成立:
后面这一部分的等式成立是基于模运算的这么一个特性:
当N为奇数的情况下,则有:
那么,结合前面讨论的公式,对奇数情况下求模,则结果为如下等式:
综合前面的两种情况,我们可以发现,当N为偶数时,我们可以求X的平方再取模,如果是奇数的话则要再乘以X,然后取模。这样,一次运算下来,我们就将指数N折半了。按照这个过程,整个过程的时间复杂度可以降低到对数的级别上来。
根据讨论的递归关系,我们可以得出如下递归方式的代码:
public static long power(long x, long n, long p)
{
if(n == 0)
return 1;
long tmp = power((x * x) % p, n / 2, p);
if(n % 2 != 0)
tmp = (tmp * x) % p;
return tmp;
}
将递归版本转换成循环实现的方式的代码如下:
public static long loopPower(long x, long n, long p)
{
x %= p;
long tmp = 0;
while(n > 0)
{
tmp = (x * x) % p;
if(n % 2 != 0)
tmp = (tmp * x) % p;
n /= 2;
}
return tmp;
}
结合前面的情况,因为问题2中本身需要求模运算的数字比较特殊,不是用一个普通的数据类型来保存,而是用的整型数组或者字符串数组。在这种情况下,我们需要考虑的是利用一些模运算的特性,使得整个运算的过程拆分成可运算的各个小的步骤。在这里,我们先假设是10进制的数字,比如说一个int数组[1, 2, 3, 4, 5, 6],那么他们实际对应的这个数字应该是如下:
这就相当于转换成了一个多项式求和的问题。对于一个整型的数组表示的长数据,我们按照多项式方式求和的典型代码如下:
public static long sum(int[] array)
{
long sum = 0;
for(int i = 1; i < array.length; i++)
{
sum = sum * 10 + array[i];
}
return sum;
}
如果再结合一些模运算的性质来考虑,比如,对多个数字的相加再求模和先对中间部分结果求模再相加之后求模的结果是一样的。那么,我们可以得出一个通用的求模运算的方法:
public static long sum(int[] array, int base, int p)
{
long sum = 0;
for(int i = 1; i < array.length; i++)
{
sum = sum * base + array[i];
sum %= p;
}
return sum;
}
这里,base表示数的进制,可以是10以外的其他进制。
前面针对大整数的两种情况进行了讨论,一种是给定一个整数,然后有一个比较大的指数,这种情况下需要考虑将指数变小给降下来。这里要利用到模运算里底数可以结合的特性。而针对一个很长的整数,我们可以将他转换成多项式求和的形式。这里就利用了多个数字求和取模和部分结果先取模再求和取模的结果一致这个特性。问题本身不是很复杂,主要是要把这几种特性想清楚,给用好了。总的来看,确实有点绕。
我们先来证明如下的等式成立:
因为X, Y都要对P求模,而实际上X, Y 都可以表示成 X = A*P + B 的形式,其中B就是X mod P的结果。那么前面的A*P这部分则是对于P可整除的。(X * Y) = (A * P + B)* Y = A * P * Y + B * Y
如果对这个等式的右边求模的话,显然A*P*Y这一部分是P的倍数,求模后的结果为0, 则结果为(B * Y) mod P, 前面我们知道 B = X mod P。这样我们就证明了(X * Y) mod P = [X(Y mod P)] mod P。后面这部分的证明可以类似推导出来。
我们再证明下面的等式成立:
按照前面一部分的讨论,我们可以假设X = A * P + B, 那么原来的等式则转化为:
在右边的等式中,如果我们按照二项式定理将它展开,那么他们将成为很多项乘积的和。但是所有和A*P相乘的项都可以被P整除,那么这些项的结果最终都是0,只有不含有A*P的项对最后的结果有效。展开后唯一有效的部分就是B^N。而B本身就是X mod P。那么我们也就证明了上面等式的成立。
data structures and problem solving using java