一、爲什麼要搞定遞歸
在計算機科學與技術中,遞歸思想是簡單而且複雜的。它可以將複雜的數學問題用簡單的代碼實現,但是要理解它卻是需要複雜的思考。大多數算法中都巧妙的使用了,或者可以使用遞歸來完成,比如排序算法中的快速排序、堆排序、歸併排序等,數據結構中樹的遍歷、平衡樹的判斷、二叉查找樹的建立與維護以及圖的遍歷與最短路徑求解等,動態規劃、貪心算法等等,實在事太多太多,就不一一列舉了。所以掌握好遞歸對於學習好算法與數據結構的重要性不言而喻。
二、初見遞歸
要想學好遞歸就得先了解函數調用在內存中的實現方式:函數調用是通過棧的方式實現的。遞歸簡單來說就是在函數內部調用其本身,借用棧的特性,在一定的遞歸終止條件下,達到正向循環和反向循環的目的。這裏有幾個重點:1)函數調用與運行的順序,2)遞歸終止條件。前者關係到我們在函數內部對數據進行操作的位置,後者關係到我們遞歸循環達到的範圍。所以在書寫遞歸函數之前,務必要將其弄清楚。
三、例子
例子:實現一個函數檢查二叉樹是否平衡。平衡樹定義:任意一個節點,其兩棵子樹的高度差不超過1。
遞歸解法:如上所說,我們要解決的其實是找到每個節點左右子樹最大高度,並進行比較。所以要利用遞歸解決問題,要先搞清楚遞歸的兩個重點:1)函數調用於運行的順序,2)遞歸終止條件。
對於前者,是由我們的目的決定的:我們想要得到每個節點對應的左右子樹的高度。這種一般是在遞歸調用之後執行操作(包括樹的高度計算,或者返回結果),可以對應到樹的後序遍歷。
對於後者,是由我們的數據結構特性決定的:樹的葉子節點的左右左右孩子節點爲空。其實到這裏,利用遞歸算法解這個題的思路就已經形成了。
首先,在遞歸調用前,作出判斷,如果當前節點是空,則返回0,表示當前節點爲葉子節點,其對應子樹高度爲0。其次,分別將當前節點的左右孩子作爲參數進行遞歸調用。最後,在遞歸調用後,比較前兩個遞歸調用的返回值大小,並得到較大的,其即爲當前節點的最大左右子樹高度。並將最大高度加1並返回。完畢!
就是這麼簡單。其實完全文字敘述可能有點難以理解,不過如果有一定的基礎,還是比較通俗易懂的。如果有時間,我以後會加上圖解。
代碼如下:
int getHeight(TreeNode *root){
if(root == null)
return 0;
int left = getHeight(root->left);
int right = getHeight(root->right);
int height = Math.max(left, right);
return height + 1;
}
最後這是對於一個節點的左右子樹高度的判斷,我們還需要對整棵樹的每個節點進行判斷。所以我們還需要如下代碼:
bool checkBalanced(TreeNode *root){
if(root == null)
return true;
if(Math.abs(getHeight(root->left) - getHeight(root->right)) > 1)
return false;
return checkBalanced(root->left) && checkBalanced(root->right);
}
以上算法的時間複雜度是O(NlogN)。對該算法進行很小的修改即可達到線性時間O(N)的效果。大家可以自行思考。
四、再看遞歸
經過以上分析(其實這部分是要結合我設計的圖來說明的,最近比較忙,圖估計要晚些時候是上了,那就先完成這個部分吧。),我們可以看到,遞歸的一個比較重要的特性,棧的運行順序。所以我們會在書寫遞歸程序時有類似下面的總結:
1)首先弄清楚我們要對數據處理的順序(對應於前面說的遞歸的第一個重點);
2)其次弄清楚數據結構的特性,或者說遞歸的終止條件(對應於前面說的遞歸的第二個重點);
3)在檢查代碼時,將每次遞歸過程想象成一個完全相同的(裝了同樣的代碼,僅僅是傳入的參數可能不同)矩形框,矩形框中的一行對應於函數中的一行執行代碼。從每個遞歸調用出會衍生出新的矩形框;
4)在第一次達到遞歸終止條件時,代表該矩形框可以被拋棄(函數調用運行結束),並返回調用他的矩形框中,在調用處的下一行繼續運行,直到返回。後續運行,依次類推。