對於如何算 n 的階乘,只要你知道階乘的定義,我想你都知道怎麼算,但如果在面試中,面試官拋給你一道與階乘相關,看似簡單的演算法題,你還真不一定能夠給出優雅的答案!本文將分享幾道與階乘相關的案例,且難度遞增。
案例一
給定一個整數 N,那麼 N 的階乘 N! 末尾有多少個 0?例如: N = 10,則 N!= 3628800,那麼 N! 的末尾有兩個0。
有些人心想,這還不簡單,直接算出 N!的值,然後用除以 10 來判斷多少個 0 就可以了。
有些人則這樣想,直接算 N!的值再來除以 10 判斷多少個 0 的話肯定會出現溢位問題,於是開始思索:一個數乘以 10 就一定會在末尾產生一個零,於是,我們可以從“哪些數相乘能夠得到 10 ”入手。
沒錯,只有 2 * 5 才會產生 10。
注意,4 * 5 = 20 也能產生 0 啊,不過我們也可以把 20 進行分解啊,20 = 10 * 2。
於是,問題轉化為 N! 種能夠分解成多少對 25,再一步分析會發現,在 N!中能夠被 2 整除的數一定比能夠被 5 整除的數多,於是問題就轉化為求 1...n 這 n 個數中能夠被 5 整除的數有多少個,注意,像 25 能夠被 5整除兩次,所以25是能夠產生 2 對 2 5滴。有了這個思路,程式碼如下:
int f(int n){
int sum = 0;
for(int i = 1; i <= n; i++){
int j = i;
while(j % 5 == 0){
sum++;
j = j / 5;
}
}
return sum;
}
有些人想出了這個規律,很是得意,然而,這還不是面試官要的答案,大家想一個問題,
當 N = 20 時,1~20 可以產生幾個 5 ?答是 4 個,此時有 N / 5 = 4。
當 N = 24 時,1~24 可以產生幾個 5 ?答是 4 個,此時有 N / 5 = 4。
當 N = 25 時,1~25 可以產生幾個 5?答是 6 個,主要是因為 25 貢獻了兩個 5,此時有 N / 5 + N / 5^2 = 6。
...
可以發現 產生 5 的個數為 sum = N/5 + N/5^2 + N/5^3+....
於是,最優雅的寫法應該是這樣:
int f(int n){
int sum = 0;
while(n! = 0){
sum += n / 5;
n = n / 5;
}
}
這時,你就可以自信這把程式碼扔給面試官了。
案例2
求 N! 的二進位制表示中最低位 1 的位置。例如 3!=6,二進位制為 1010,所以 最低位 1 的位置在第二位。
沒有思路?請仔細想一下這道題與上道題的關係!
仔細想一下,這道題不也是求末尾有多少個 0 嗎?你求出了末尾有多少個0自然知道 1 的位置(0的個數加1就是1的位置了),只不過,這道題是求二進位制末尾有多少個 0。
由於是二進位制,所以每次乘以 2 末尾就會產生一個 0 。
於是,模仿上面一道題,求 N!含有多少個 2 的個數。心想,幸好我做個類似了,於是一波操作猛如虎,一分鐘就寫出了程式碼:
int f(int n){
int sum = 0;
while(n! = 0){
sum += n / 2;
n = n / 2;
}
}
面試官:還能在優化嗎?
什麼鬼?還要在優化?我都 O(logn) 時間複雜度了。
還記得我之前講解了幾道有關位運算的嗎?這道題確實可以用位運算來優化,除以 n / 2 == n >> 1。不過位運算的速度快多了,於是,優化後的程式碼如下:
int f(int n){
int sum = 0;
while(n! = 0){
n >>= 1;
sum += n;
}
}
還能在優化嗎?
可以,不過我先不講,因為我覺得,這已經夠快了。後面講其他題可能會把這道題再拿出來講。
如果你能寫出這樣的程式碼,已經算很牛逼了。
案例三
給你一個數 N,輸出 N! 的值。
沒錯,就是這麼簡單,直接讓你求階乘的值。
這個時候,你就要小心了,千萬別一波操作
long long sum = 1;
for(int i = 1; i <= n; i++){
sum *= i;
}
一個 for 迴圈,馬上搞定。
因為你不知道 n 的範圍,有可能你用 long long 也是會溢位的,所以這個時候應該要問一下面試官有沒有限定 n 的範圍。
面試官:沒有限定!
這下好了,這道階乘的題,難度頓時上升了,說實話,我敢保證大部分人還真沒去實現過。所以,今天我就帶大家來實現一下,以防以後真的被問到。結果最熟悉的題頓時不知道怎麼下手好。
這個時候,我們就必須用字串來存放所求的值的,在相乘的時候也是用字串來相乘,說白了,就是要會求兩個大整數相乘。
下面我們先來實現一個求兩個大整數相乘的函式。一種比較簡單的方法就是,像我們小學那會一樣,讓個位數與另一個數的其他數相乘,然後讓十位數與另一個的其他數相乘,最後在把他們進行相加。
實現程式碼如下:
public static String mul(String s1, String s2) {
// 先把字串轉化為 字元陣列。
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
int len = c1.length + c2.length;
// 用來存放兩個數的積,字元的初始值為 '\u0000',也就是 0
char[] c = new char[len];
// 由於大整數的低位是在字串的末尾,所以我們從末尾遍歷來計算
for (int i = c1.length - 1; i >= 0; i--) {
int index = len - 1;
int res = 0;//用來存放進位
for (int j = c2.length - 1; j >= 0; j--) {
int temp = (c1[i]-'0') * (c2[j]-'0') + c[index] + res;
res = temp / 10;
c[index--] = (char)(temp % 10);
}
// 每趟乘下來的進位要進行儲存。
c[index] = (char)res;
len--;
}
// 最後把c中的字元加上 '0'
for (int i = 0; i < c.length; i++) {
c[i] += '0';
}
String s = new String(c);
// n位數乘以m位數得到積位 (m+n)位數或者(n+m-1)位數
// 我們只需要判斷c[0]是否為0,為0則把它捨棄。
if(c[0] == '0')
return s.substring(1);
else
return s;
}
注意,一定要自己實現一遍,一定要自己實現一遍,因為原理簡單,但手動實現就不一定那麼簡單了,容易出現bug。
採用這種方法的話,兩個大整數相乘的時間複雜度為 O(n),其實還有更快的方法,大概時間複雜度是 O(n^1.6),不過程式碼有點小複雜,感興趣的可以閱讀這篇文章:漫畫:如何實現大整數相乘?,我這裡就不展開說了,不然單獨這個就可以另起一篇很長的文章了。
知道了兩個大整數相乘之後,我們再來算我們的階乘,就好做了,我們每次相乘的時候,只需要把它當作兩個大整數重複乘就可以了。程式碼如下:
// N 的階乘
public static String f(int n) {
String s = "1";
Integer t = null;
for (int i = 2; i <= n; i++) {
t = i;
s = 大整數相乘.mul(s,t.toString());
}
return s;
}
// 大整數相乘
public static String mul(String s1, String s2) {
// 先把字串轉化為 字元陣列。
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
int len = c1.length + c2.length;
// 用來存放兩個數的積,字元的初始值為 '\u0000',也就是 0
char[] c = new char[len];
// 由於大整數的低位是在字串的末尾,所以我們從末尾遍歷來計算
for (int i = c1.length - 1; i >= 0; i--) {
int index = len - 1;
int res = 0;//用來存放進位
for (int j = c2.length - 1; j >= 0; j--) {
int temp = (c1[i]-'0') * (c2[j]-'0') + c[index] + res;
res = temp / 10;
c[index--] = (char)(temp % 10);
}
// 每趟乘下來的進位要進行儲存。
c[index] = (char)res;
len--;
}
// 最後把c中的字元加上 '0'
for (int i = 0; i < c.length; i++) {
c[i] += '0';
}
String s = new String(c);
// n位數乘以m位數得到積位 (m+n)位數或者(n+m-1)位數
// 我們只需要判斷c[0]是否為0,為0則把它捨棄。
if(c[0] == '0')
return s.substring(1);
else
return s;
}
時間複雜度是 O(n^3)。如果要優化的話,主要是在大整數相乘這裡進行優化。
總結
是不是覺得,階乘也沒有那麼簡單了?這三道與階乘相關的題,應該是比較常見的題吧,今天跟大家分享一波,希望大家有時間的話,自己也用程式碼實現一下,特別是算 N!。後面會繼續跟大家分享一些我覺得不錯的演算法題,敬請關注。
最後推廣下我的公眾號:苦逼的碼農:戳我即可關注,文章都會首發於我的公眾號,期待各路英雄的關注交流。