快速冪(Exponentiation by squaring,平方求冪)是一種簡單而有效的小算法,它能夠以O(lgn)的時間複雜度計算乘方。快速冪不只自己很是常見,並且後續不少算法也都會用到快速冪。java
讓咱們先來思考一個問題:7的10次方,怎樣算比較快?算法
方法1:最樸素的想法,77=49,497=343,... 一步一步算,共進行了9次乘法。spa
這樣算無疑太慢了,尤爲對計算機的CPU而言,每次運算只乘上一個個位數,無疑太屈才了。這時咱們想到,也許能夠拆分問題。翻譯
方法2:先算7的5次方,即77777,再算它的平方,共進行了5次乘法。code
但這並非最優解,由於對於「7的5次方」,咱們仍然能夠拆分問題。遞歸
方法3:先算77得49,則7的5次方爲4949*7,再算它的平方,共進行了4次乘法。rem
模仿這樣的過程,咱們獲得一個在O(lgn)時間內計算出冪的算法,也就是快速冪。it
剛剛咱們用到的,無非是一個二分的思路。咱們很天然地能夠獲得一個遞歸方程:io
$$ a^n = \begin{cases} a^{n-1}·a,\quad if\ n\ is\ odd \\ a^{\frac{n}{2}}·a^{\frac{n}{2}},\quad if\ n\ is\ even\ but\ not\ 0 \\ 1,\quad if\ n\ =\ 0 \\ \end{cases} $$class
計算a的n次方,若是n是偶數(不爲0),那麼就先計算a的n/2次方,而後平方;若是n是奇數,那麼就先計算a的n-1次方,再乘上a;遞歸出口是a的0次方爲1。
遞歸快速冪的思路很是天然,代碼也很簡單(直接把遞歸方程翻譯成代碼便可):
//遞歸快速冪 int qpow(int a, int n) { if (n == 0) return 1; else if ((n & 1) == 1) return qpow(a, n - 1) * a; else { int temp = qpow(a, n / 2); return temp * temp; } }
注意,這個temp變量是必要的,由於若是不把a^(n/2)記錄下來,直接寫成qpow(a, n /2)*qpow(a, n /2),那會計算兩次a^(n/2),整個算法就退化爲了 O(n)。
在實際問題中,題目經常會要求對一個大素數取模,這是由於計算結果可能會很是巨大,可是在這裏考察高精度又沒有必要。這時咱們的快速冪也應當進行取模,此時應當注意,原則是步步取模。
//遞歸快速冪(對大素數取模) int qpow(int a, int n, int mod) { if (n == 0) return 1; else if ((n & 1) == 1) return qpow(a, n - 1, mod) * a % mod; else { int temp = qpow(a, n / 2, mod) % mod; return temp * temp % mod; } }
你們知道,遞歸雖然簡潔,但會產生額外的空間開銷。咱們能夠把遞歸改寫爲循環,來避免對棧空間的大量佔用,也就是非遞歸快速冪。
咱們先看代碼,再來仔細推敲這個過程:
//非遞歸快速冪 int qpow(int a, int n) { int ans = 1; while (n > 0) { //若是n的當前末位爲1 if ((n & 1) == 1) { //ans乘上當前的a ans *= a; } //a自乘 a *= a; //n往右移一位 n >>= 1; } return ans; }
最初ans爲1,而後咱們一位一位算:
1010的最後一位是0,因此 a^1 這一位不要。而後1010變爲101,a變爲 a^2。
101的最後一位是1,因此 a^2 這一位是須要的,乘入ans。101變爲10,a再自乘。
10的最後一位是0,跳過,右移,自乘。
而後1的最後一位是1,ans再乘上 a^8。循環結束,返回結果。
這裏的位運算符,>>是右移,表示把二進制數往右移一位,至關於/2;&是按位與,&1能夠理解爲取出二進制數的最後一位,至關於%2==1。這麼一等價,是否是看出了遞歸和非遞歸的快速冪的關係了?雖然非遞歸快速冪由於牽扯到二進制理解起來稍微複雜一點,但基本思路其實和遞歸快速冪沒有太大的出入。