相信整数幂运算作为一个算法演变的例子是再合适不过的了为了节省访客们宝贵的学习时间省去介绍递归等可能涉及到的初级概念的定义。同时如果发现文中有错误的地方请敞开衣服指正。
因为在测试性能时合适的测试数据是必要的,所以本文用C++的大数类进行演示。
这里我们先列一下会提到的算法分析技术:
测试平台:
- Linux
- g++ 4.7
原始递归方法 这就不花时间赘述什么了。
BigInteger pow(BigInteger x, int N) { if (N == 1) { return x; } if (N == 0) { return 1; } return pow(x, N - 1) * x; }
190000
结果值围绕其上下波动,波动范围取决于操作系统的进程调度。显然其时间复杂度为O(N)。显然这个算法有改进空间,对于递归来说,一个递归算法在执行时,会根据其算法而消耗一定的递归调用栈空间(注意,这里并不考虑编译器的尾递归优化),并且会函数调用会进行参数压栈,存储必要信息等操作,带来一定的调用开销,因此其附带有空间复杂度O(N)咱们可以改为迭代版本的计算以节省掉这些开销,但是经过测试发现,时间上并没有明显的提高,所以咱们可以试着将其优化为更好版本的迭代计算,节省开销。于是,我们接下来的改进还是用递归,怎么样,是不是觉得我琢磨不透?不要在意这些细节。
更好的递归计算方法
减治法的定义在文章开始处已经给出,与分治法类似,就不赘述了。(?ˉωˉ?) 维基的描述:
A divide and conquer algorithm works by recursively breaking down a problem into two or more sub-problems of the same (or related) type, until these become simple enough to be solved directly
对此问题,思路可以从此公式开始:
十分抱歉,由于还没搞清楚在live writer上怎么用插件生成Latex公式,所以暂时别扭的着看,您千万别介意。
对于X^64,我们可以将其分解为X^32?X^32,这样为了得到X^64的值,我们只需要计算X32的值,然后将其相乘,同样地,为了得到X^32的值,我们需要得到X^16的值,以此类推,这样我们从就可以得到一个过程:
X^2=X?X
X^4=X^2?X^2
X^8=X^4?X^4
X^16=X^8?X^8
X^32=X^16?X^16
X^64=X^32?X^32
于是,可以看到原本需要64次的乘法,这里只需要6次,对于幂为奇数时的推导就不浪费笔墨了,其推导过程比为偶数时要复杂点,大伙儿可以自己按照公式推导一下。
可以知道,算法的规模实际上取决于幂的大小,于是我们可以将算法整体的分析简化为对幂的分析,设幂为N,则有2i=N,其中i为乘法次数,则有
i=logN
当然这只是理论上来说的,具体的分析稍显复杂。
最后我们给出实现代码:
BigInteger pow(BigInteger x, int N) { if (N == 1) { return x; } if (N == 0) { return 1; } if (N % 2 == 0) { return pow(x * x, N >>= 2); } else { return x*pow(x*x, N>>=/2); } }
测试x=13,N=6000得到的运行时间 30000 可以看到时间减少了很多,并且空间复杂度O(logN)相对来说可以忽略不计了,大家可以在自己的机器上测试一下。总而言之,我们的算法得到了改善。但是这还不够,毕竟函数调用仍然有开销,能够将其优化为迭代计算吗?会节省多少时间呢?
迭代版本的求幂
在动手实现迭代版本之前,我们需要考虑几个问题。 1. 既然是迭代,只能从x?x开始算起,可是这和原始递归版本有什么区别呢? 2. 要用多线程吗?可是咱们在优化算法本身啊。 3. 打幂的主意试试?好像改进的递归版本已经做过了,x本身有没有什么可以下手的地方呢?
回头想想,既然要迭代,又要快速,必须得做到像改进的递归算法那样保存中间结果了(参考动态规划),可是保存了中间结果又如何?我们怎么知道一个数需要用哪些中间结果呢?思绪至此,想不出来没关系,咱们可以请教大师前辈们,这也是一种情怀啊~~~经过大师指点,原来,与改进递归版本的一样,x^n的表示方式不需要任何变化,依然是
X2=X?X
X4=X2?X2
X8=X4?X4
X16=X8?X8
X32=X16?X16
X64=X32?X32
并且n可以表示为一定数量的2相加,若n为奇数,则减1后依然还是原问题。至此,这就可以当成是n的二进制表示转换成对应十进制的运算么?既然知道方向了,可得以下解法。
BigInteger quick_pow(BigInteger x, int N) { //boundary condition if (x == 0) { return 0; } if (x == 1 || N == 0) { return 1; } //store x,x^2,x^4...until x^2^logN int n = log2(N); BigInteger arr[n + 1]; arr[0] = x; for (int i = 1; i <= n; i++) { arr[i] = arr[i - 1] * arr[i - 1]; } BigInteger result = 1; int count = 0; while (N != 1) //Calculating result according to the binary representation of N. { switch (N % 2) { case 1: result *= arr[count]; break; case 0: break; } count += 1; N >>= 2; } result *= arr[count]; return result; }
至此改进就完毕了,省去了一些隐性的开销,运行时间经过测试与改进版递归相差不大。代码写的不好,有待重写,若有更好的可以共享。
最后推荐一本书,书名《算法分析与设计基础》,从算法思想分类的角度主要介绍了算法的一些主要分析技术,挺新引,值得一看。
更快的求整数幂算法