(給算法愛好者加星標,修煉編程內功)
來源:代碼隨想錄
相信很多同學對遞歸算法的時間複雜度都很模糊,那麼這篇來給大家通透的講一講。
「同一道題目,同樣使用遞歸算法,有的同學會寫出了O(n)的代碼,有的同學就寫出了O(logn)的代碼」。
這是為什麼呢?
如果對遞歸的時間複雜度理解的不夠深入的話,就會這樣!
那麼我通過一道簡單的面試題,模擬面試的場景,來帶大家逐步分析遞歸算法的時間複雜度,最後找出最優解,來看看同樣是遞歸,怎麼就寫成了O(n)的代碼。
面試題:求x的n次方
想一下這麼簡單的一道題目,代碼應該如何寫呢。最直觀的方式應該就是,一個for循環求出結果,代碼如下:
int function1(int x, int n) {
int result = 1; // 注意 任何數的0次方等於1
for (int i = 0; i < n; i++) {
result = result * x;
}
return result;
}時間複雜度為O(n),此時面試官會說,有沒有效率更好的算法呢。
「如果此時沒有思路,不要說:我不會,我不知道了等等」。
可以和面試官探討一下,詢問:「可不可以給點提示」。面試官提示:「考慮一下遞歸算法」。
那麼就可以寫出了如下這樣的一個遞歸的算法,使用遞歸解決了這個問題。
int function2(int x, int n) {
if (n == 0) {
return 1; // return 1 同樣是因為0次方是等於1的
}
return function2(x, n - 1) * x;
}面試官問:「那麼這個代碼的時間複雜度是多少?」。
一些同學可能一看到遞歸就想到了O(logn),其實並不是這樣,遞歸算法的時間複雜度本質上是要看: 「遞歸的次數 * 每次遞歸中的操作次數」。
那再來看代碼,這裡遞歸了幾次呢?
每次n-1,遞歸了n次時間複雜度是O(n),每次進行了一個乘法操作,乘法操作的時間複雜度一個常數項O(1),所以這份代碼的時間複雜度是 n * 1 = O(n)。
這個時間複雜度就沒有達到面試官的預期。於是又寫出了如下的遞歸算法的代碼:
int function3(int x, int n) {
if (n == 0) {
return 1;
}
if (n % 2 == 1) {
return function3(x, n / 2) * function3(x, n / 2)*x;
}
return function3(x, n / 2) * function3(x, n / 2);
}面試官看到後微微一笑,問:「這份代碼的時間複雜度又是多少呢?」 此刻有些同學可能要陷入了沉思了。
我們來分析一下,首先看遞歸了多少次呢,可以把遞歸抽象出一顆滿二叉樹。剛剛同學寫的這個算法,可以用一顆滿二叉樹來表示(為了方便表示,選擇n為偶數16),如圖:
遞歸算法的時間複雜度當前這顆二叉樹就是求x的n次方,n為16的情況,n為16的時候,進行了多少次乘法運算呢?
這棵樹上每一個節點就代表著一次遞歸併進行了一次相乘操作,所以進行了多少次遞歸的話,就是看這棵樹上有多少個節點。
熟悉二叉樹話應該知道如何求滿二叉樹節點數量,這顆滿二叉樹的節點數量就是2^3 + 2^2 + 2^1 + 2^0 = 15,可以發現:「這其實是等比數列的求和公式,這個結論在二叉樹相關的面試題裡也經常出現」。
這麼如果是求x的n次方,這個遞歸樹有多少個節點呢,如下圖所示:(m為深度,從0開始)
「時間複雜度忽略掉常數項-1之後,這個遞歸算法的時間複雜度依然是O(n)」。對,你沒看錯,依然是O(n)的時間複雜度!此時面試官就會說:「這個遞歸的算法依然還是O(n)啊」, 很明顯沒有達到面試官的預期。
那麼O(logn)的遞歸算法應該怎麼寫呢?
想一想剛剛給出的那份遞歸算法的代碼,是不是有哪裡比較冗餘呢。
於是又寫出如下遞歸算法的代碼:
int function4(int x, int n) {
if (n == 0) {
return 1;
}
int t = function4(x, n / 2);// 這裡相對於function3,是把這個遞歸操作抽取出來
if (n % 2 == 1) {
return t * t * x;
}
return t * t;
}再來看一下現在這份代碼時間複雜度是多少呢?
依然還是看他遞歸了多少次,可以看到這裡僅僅有一個遞歸調用,且每次都是n/2 ,所以這裡我們一共調用了log以2為底n的對數次。
「每次遞歸了做都是一次乘法操作,這也是一個常數項的操作,那麼這個遞歸算法的時間複雜度才是真正的O(logn)」。
此時大家最後寫出了這樣的代碼並且將時間複雜度分析的非常清晰,相信面試官是比較滿意的。
總結對於遞歸的時間複雜度,畢竟初學者有時候會迷糊,刷過很多題的老手依然迷糊。
「本篇我用一道非常簡單的面試題目:求x的n次方,來逐步分析遞歸算法的時間複雜度,注意不要一看到遞歸就想到了O(logn)!」
同樣使用遞歸,有的同學可以寫出O(logn)的代碼,有的同學還可以寫出O(n)的代碼。
對於function3 這樣的遞歸實現,很容易讓人感覺這是O(logn)的時間複雜度,其實這是O(n)的算法!
int function3(int x, int n) {
if (n == 0) {
return 1;
}
if (n % 2 == 1) {
return function3(x, n / 2) * function3(x, n / 2)*x;
}
return function3(x, n / 2) * function3(x, n / 2);
}可以看出這道題目非常簡單,但是又很考究算法的功底,特別是對遞歸的理解,這也是我面試別人的時候用過的一道題,所以整個情景我才寫的如此逼真,哈哈。
大廠面試的時候最喜歡用「簡單題」來考察候選人的算法功底,注意這裡的「簡單題」可並不一定真的簡單哦!
如果認真讀完本篇,相信大家對遞歸算法的有一個新的認識的,同一道題目,同樣是遞歸,效率可是不一樣的!
- EOF -
覺得本文有幫助?請分享給更多人
推薦關注「算法愛好者」,修煉編程內功
點讚和在看就是最大的支持❤️