計算n階乘中尾部零的個數

2021-08-21 09:32:35 字數 4790 閱讀 2885

本來覺得問題挺容易的,不打算記錄,誰知道一不小心,還真沒做出來。最終憑藉「樸實」的演算法思想解決了問題,但是其中的曲折還真是汗顏。科學的思維指導確實必不可少,「野路子」的樸素的戰鬥理論不論是效率還是後續的演算法演進都經不起考驗。

這裡只是記錄一下自己最近兩天對此問題的一些想法,目前只能說是解決了問題,並且滿足題目要求。據說問題來自《程式設計之美》,以後刷書本的時候看到原題,如果需要補充的話,再來更新。

and,開始吧~

設計乙個演算法,計算出n階乘中尾部零的個數

樣例11! = 39916800,因此應該返回 2

挑戰o(logn)的時間複雜度

先說結論,此問題大致有三種思路:第一種算出結果,然後檢視末尾的0的個數,效果非常差;第二種,加法操作,從5開始,每次進5,然後判斷,效果達不到o(logn);第三種,每次除5,多次之後結束。 

詳情如下。

重點分析在演算法2和演算法3,需要的可以直接跳到這部分檢視。

面對此問題,第一反應是直接計算結果:11!=39916800,然後設計程式判斷末尾的0的個數,很簡單就可以實現。 

但是相應的會有很多的問題: 

1、計算階乘的開銷 

現在只是11的階乘,都已經很大了,如果是5555550000000的階乘呢?按照程式的計算結果,末尾會有1388887499996個0,計算開銷很值得考慮。 

2、溢位 

按照上面的介紹,5555550000000的階乘有1388887499996個0,那麼可以推知階乘的結果會是很大的乙個整數,肯定會超出long型別的界限,結果會溢位。這樣還要考慮處理溢位問題,又是另乙個問題。 

3、效率 

演算法2會涉及到效率問題,會發現即使是演算法2也會出現計算時間超出要求的問題,那麼更為「樸素」的演算法1效率更是可想而知了。 

因此,演算法1,捨棄。

演算法2分析

仔細的考慮問題,會發現末尾出現的0是10或10的倍數相乘的結果,而10其實是5與偶數相乘。也就是,最終結果中末尾出現的0是5、10、15、20、25…自身或與偶數相乘之後的產生的。下面可以分為偶數和5的倍數分析。

首先考慮偶數。 

考慮2的冪次項2、4、8…中的2的個數,發現2的冪指數的增長速度遠比5的冪指數增長的快,更不用說其他的普通偶數6、12、14…。因此可以認為有足夠的偶數與奇數形式的5的倍數相乘產生足夠的0。所以我們後面只考慮5的倍數。

接著考慮5的倍數。

1、2、3、4、5、6、7、8、9、10、11...
其實1、2、3、4、6、7…都是可以不用考慮的,因此選擇以5為迭代步數即可。 

首先,這些數字都可以不用進行%5(對5取餘數)運算,因此每次迴圈時可以直接將函式的count變數直接加1。其次,考慮25、125、625…等5的冪次項,因為他們每乙個都可以在與偶數相乘之後產生多個0。因此,設定乙個迴圈體,判斷是多少冪次項,並將結果加進count。 

綜上所述,可以編寫**如下:

演算法2**

public

class

solution

}return count;}}

**很簡單,不再解釋。 

但是效率很差,分析發現**的時間複雜度實際是o(n/5)~=o(n),達不到要求的o(logn)。 

演算法2雖然可以解決問題,但考慮執行效率,演算法2應該捨棄。

反思&對比

這個演算法真的是感觸很深,對平時很多習以為常的公式、道理有了非常直觀的認識,因此對自己的衝擊很大,也促進了思考的進步。

提交演算法2的**,發現前面的簡單測試都能通過,但是數值5555550000000測試失敗。特別是實現了時間複雜度o(logn)的演算法3之後,才發現兩者時間開銷差別真的是很大。

重新分析

1、2、3、4、5、6、7、8、9、10、11、...
1、分析上面的數列可知,每5個數中會出現乙個可以產生結果中0的數字。把這些數字抽取出來是:

...、5、...、10、...、15、...、20、...、25、...
這些數字其實是都能滿足5*k的數字,是5的倍數。統計一下他們的數量:n1=n/5。比如如果是101,則101之前應該是5,10,15,20,...,95,100101/5=20個數字滿足要求。

整除操作滿足上面的數量統計要求。

2、將1中的這些數碼化成5*(1、2、3、4、5、...)的形式,內部的1、2、3、4、5、...又滿足上面的分析:每5個數字有乙個是5的倍數。抽取為:

...、25、...、50、...、75、...、100、...、125、...
而這些數字都是25的倍數(5的2次冪的倍數),自然也都滿足5*k的要求。 

這些數字是25、50、75、100、125、...=5*(5、10、15、20、25、...)=5*5*(1、2、3、4、5、...),內部的1、2、3、4、5、...又滿足上面的分析,因此後續的操作重複上述步驟即可。 

統計一下第二次中滿足條件的數字數量:n2=n/5/5101/25=(101/5)/5=4。 

因為25、50、75、100、125、...它們都滿足相乘後產生至少兩個0,在第一次5*k分析中已經統計過一次。對於n=101,是20。因此此處的5*5*k只要統計一次4即可,不需要根據25是5的二次冪統計兩次。 

後面的125,250,...等乘積為1000的可以為結果貢獻3個0的數字,只要在5*5*k的基礎上再統計一次n3=((n/5)/5)/5即可。 

3、第三次 

其實到這裡已經不用再寫,規律已經很清楚了。對於例子n=101,只要根據規律進行101/125=((101/5)/5)/5=4/5=0,退出統計。因此最終結果是20+4=24。計算結束。

演算法3**

下面編寫打碼實現上面的思想。

public

class

solution

return count;}}

**分析: 

演算法中每次迴圈均有除以5的操作,也就是每次都會將所要處理的資料量縮小至上一次的1/5,容易推知時間複雜度為o(logn)。

至此,問題解決。

public

class

test

}

因為11不超過int型別的最大長度,所以並不會報錯。但是如果是5555550000000,則會報錯:

the literal

5555550000000

oftype int is

outof

range

將數值進行強制型別轉換也不行:long innum=(long)5555550000000;。 

一種解決方法是使用scanner直接讀取數值。 

改進後的**如下:

public

class

test

}

這時輸入5555550000000則不會報錯。 

另外,如果需要的話,可使用system.currenttimemillis();觀察**執行時間。

從最終的**來看,問題是挺簡單的。之所以折騰這麼久都沒有切入要害,直接做到真正的時間複雜度為o(logn)的效果,個人覺得是因為從分析題目的時候就沒有真正理解o(logn)的真正含義。 

類似於二叉搜尋樹,從根節點開始比較,比根節點小則與左子樹比較,比根節點大則與右子樹比較,相等或到達葉子節點則退出。如此迴圈迭代。 

每次判斷後,下一次可搜尋的資料量均為上一次的1/2,如此迴圈複雜度為o(logn)。

遇到錯誤和不足就要反思,吸取教訓。正視自己的缺點。

下面是個人吐槽時間,吃瓜子的觀眾可以有序退場了。

應該來講,本題的最終目的是要做到o(logn)。分析題目的時候從o(logn)著手分析可能會是更好的方法。從科學的、有章可循的理論出發,作為指導思想,結合之前的例子(二叉搜尋樹),舉一反三,解決本問題不是難事。 

但是反過來,採用「樸素」方法,依靠個人經驗,觀察演算法規律,然後解決問題。乙個不行再去觀察思考嘗試下一種方法,雖然也是一種解決問題的思路,但如果想要在此基礎上做到有章可循的逐步演進,怕是困難得多。 

更何況如果觀察不出規律呢?

先分析理論然後落實到實踐,還是先動手做,再結合/總結昇華出理論,值得推敲。 

理性思考有助於身體健康,切記切記。與君共勉。

lintcode第二題 計算n階乘中尾部零的個數

設計乙個演算法,計算出n階乘中尾部零的個數 樣例11 39916800,因此應該返回 2 挑戰o logn 的時間複雜度 先說結論,此問題大致有三種思路 第一種算出結果,然後檢視末尾的0的個數,效果非常差 第二種,加法操作,從5開始,每次進5,然後判斷,效果達不到o logn 第三種,每次除5,多次...

尾部的零 設計乙個演算法,計算出n階乘中尾部零的個數

思路 乙個n的階乘未尾有多少個 0 取決於 1 到 n 的各個因子中 2 和 5 的個數,而2的個數是遠遠多於5的個數的,因此求出5的個數即可。題解中給出的求解因子 5 的個數的方法是用 n 不斷除以 5,直到結果為 0,然後把中間得到的結果累加.例如,100 5 20,20 5 4,4 5 0 則...

設計乙個演算法,計算出n階乘中尾部零的個數

樣例 1 輸入 11 輸出 2 樣例解釋 11 39916800,結尾的0有2個。樣例 2 輸入 5 輸出 1 樣例解釋 5 120,結尾的0有1個。思想一 碰到這個問題可能首先想到的就是用乙個for迴圈得到n的階乘,然後在算出末尾有幾個零,這種思想只能在n的階乘在資料型別範圍內,才能正確。一旦n的...