Loading

快速幂

快速幂

一般的求幂方法

初学编程时,学校一般都会出求x的n次幂的题目。而在做基础题的时候,也经常会用到math.h中的pow函数来求x的n次幂。但是,这些方法不一定能满足我们开发中所需效率。

//迭代法
int pow(int x, int n){
    int ans = 1;
    while(n--)
        ans *= x;
    return ans;
}
//递归法
int pow(int x, int n){
    //base case
    if(!n)	return x;
    return x*pow(x, n-1);
}

这种方法是初学时常用的解法,日常使用也是足够的,时间复杂度为O(N),而递归法则要用到O(N)的空间复杂度。

但有时,我们的底数和指数中有小数或指数有负数时,这种方法往往还需要再写多几行判断和扩展。那么我们就会用pow来求,往往就会用到精度更高的double类型变量,内部的实现远比上述复杂,所以仅仅在整数间运算,我们没必要用pow函数。

快速幂算法

整数间的求幂可以用快速幂算法来把时间复杂度降到O(logN),快速幂算法不仅常见,后续很多算法也会用到。

接下来我们讨论这个算法的原理。

\[x^n=\underbrace {x\cdot x\cdot x\cdot ...\cdot x}_{n} \]

\[若n为奇数:\quad x^n = x\cdot x^{n-1} \]

\[若n为偶数:\quad x^n=x^\frac{n}{2}\cdot x^\frac{n}{2} \]

\[若n为0:\quad x^n=1 \]

这是一个二分的分治思路,我们可以不断地把偶数情况一直拆解成相似的小问题,那么递归方程也显而易见了

\[x^n = \begin{cases}x\cdot x^{n-1}\quad,when\quad n\quad is\quad odd \\x^{\frac{n}{2}}\cdot x^{\frac{n}{2}}\quad ,when\quad n\quad is\quad even\\ 1\quad\quad\quad\quad,n=0\end{cases} \]

//递归方法实现
//time complexity:O(logN)
//space comlexity:O(logN)
int quickPow(int x, int n){
    if(!n)	return 1;
    else if(n & 1)	return quickPow(a, n-1)*a; //此处位运算为判断奇数
    else{
        int temp = quickPow(a, n/2);
        //这里必须用一个变量储存递归结果,如果在return里面调用两次递归,就不是分治了,时间复杂度还是线性阶
        return temp*temp;
    }
}

实际问题中,如果计算结果特别大,我们会对一个大素数进行取模,这时快速幂算法也需要取模,而且是步步取模。我们的素数较大,可能需要用到long long型变量来运算。

#define MOD 1000000007
long long quickPow(long long x, long long n){
    if(!n)	return 1;
    else if(n & 1)	return quickPow(x, n-1) % MOD;
    else{
        long long temp = quickPow(a, n/2) % MOD;
        return temp*temp;
    }
}

递归的代码简洁,又能非常贴切的描述上面推导的递归方程,效率也能比一般求幂方法高,但是众所周知递归是会占用额外的程序栈空间的,所以如果我们能把这个过程换成迭代来实现,效果会更好。

快速幂的迭代方法

迭代方法中,我们就需要换一个角度去思考这种分治的思想。其实在上面的代码中,我们用了一个temp变量来储存了当前层递归的结果来达到分治,那么在迭代中,如果也能不停地调用前面的计算结果,减少运算次数,也就提高了效率。

我们知道,数值数据在计算机中以二进制储存,我们可以在推导中把指数看作二进制形式,我们以x的7次方为例

\[x^7=x^{(0111)_2}=x^{(0100+0010+0001)_2} \]

\[\therefore x^7=x^{(100)_2}\cdot x^{(10)_2}\cdot x^{(1)_2}=x^4\cdot x^2\cdot x \]

可以看出,我们只需要不断地把当前底数乘方,再让幂除二就可以实现快速幂算法了。当然,奇数情况下要先乘一个x。

int quickPow(int x, int n){
    int ans = 1;
    while(n){
        if(n & 1)
            ans *= x;
        x *= x;
        n >= 1; //右移一位,相当于除2
    }
    return ans;
}

虽然上述实现为整数,但其实只要x的数据类型支持乘法且满足结合律,快速幂的算法都是有效的。矩阵,高精度整数都可以照搬这个思路。

拓展:为什么要对大素数“1000000007”取模

有时候,我们的算法得出结果会超过32位整型的范围,题目为了精度或是防止整数溢出,就需要对一个大数字进行取模。

那么为什么是1000000007呢?

因为这个数字是10位的最小质数,跟质数取模能最大程度避免冲突

而对于int32位的最值,1000000007这个数字足够大,它的平方在int64的范围中也不会溢出。

参考文章

"算法笔记(4): 快速幂"
"为什么要对1000000007取模"

posted @ 2021-11-25 23:59  骆驼弟弟  阅读(109)  评论(0)    收藏  举报