【原創】遞歸算法的要素總結


8個月沒維護過blog了,想想看也是經歷了挺多的,學了不少東西。慢慢的把自己的一些心得和總結發出來,和大家分享和改正。

好了,上面是題外話,今天的主題是遞歸算法的實現要素和分析。

 

一、分析和總結

 

寫好一個遞歸算法,我認為主要是把握好如下三個方面:

1. 提取重復的邏輯。

2. 控制邏輯邊界。

3. 恰當的退出。

 

1. 重復邏輯

出現重復邏輯一定是必不可少的,因為遞歸的精髓就是loop。但是,重復的邏輯需要抽象。抽象出來一個干凈利落的可循環邏輯對程序編寫的幫助很大。下面將會提到一個例子,有更多分析。

2. 控制邏輯邊界

控制邊界保證了程序在正確的框架下運行。因為抽象出來的邏輯需要一個框架保證其可以遞歸執行,“剛剛好”是它的要點。寫程序也經常因為邊界把控的不准確容易留下bug。

那么如何正確控制邊界?一個比較好的辦法,就是對邊界也進行邏輯上的遞歸。因為遞歸是層層相同,那么第一層和第k層是一致的。第一層容易得出執行邊界,那么相應的可以抽象第k層的執行邊界。不過第k層的邊界是邏輯上的,而第一層通常是數值上的。通常在第一層的輔助下抽象出嚴格的邏輯邊界。

3. 合適退出遞歸

遞歸的退出往往和邏輯邊界是相輔相成的,這一點下面的例子也會提到。

一般遞歸的退出有兩種表現形式:

1.下層遞歸檢測邊界溢出退出。

特點是在遞歸代碼的開始,會有邊界控制。

2.本層遞歸檢查邊界。

特點是在進入下層遞歸前檢查邊界。

這兩種方式最大的不同在於效率,因為每層遞歸會有對臨時數據的保存,所以減少遞歸層數可以降低程序損耗。

 

三者關系:

2和3的根本在於1的抽象邏輯,一個好的遞歸不僅思想上干凈利落,並且在代碼表現上也是簡單直接。

可以從下面的同一道題的兩份代碼中體會。

 

二、代碼分析

這是leetcode上面的一道題,是根據中序和后序遍歷的結果來恢復二叉樹

下面有兩份代碼,第一份算是正例,1、2、3點的結合比較到位。第二份算是側例,幫助大家體會和理解1、2、3點的不同導致代碼層面的差異。

代碼如下:

 1 /**
 2  * Definition for binary tree
 3  * struct TreeNode {
 4  *     int val;
 5  *     TreeNode *left;
 6  *     TreeNode *right;
 7  *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 8  * };
 9  */
10 class Solution {
11 public:
12     TreeNode *buildTree(vector<int> &inorder, vector<int> &postorder) {
13         if(inorder.size() == 0)
14             return NULL;
15         int PostPos = postorder.size()-1;
16         return buildBinaryTree(inorder, 0, inorder.size(), postorder, PostPos);
17     }
18     
19     TreeNode *buildBinaryTree(vector<int> &inorder, int InPosHead, int InPosTail, 
                    vector<int> &postorder, int &PostPos){ 20 TreeNode *node = new TreeNode(postorder[PostPos--]); 21 int i = InPosHead; 22 //find separate pos 23 for(; i < InPosTail; i++) 24 if(inorder[i] == node->val) 25 break; 26 //recursion bulid 27 if(i < InPosTail-1) 28 node->right = buildBinaryTree(inorder, i+1, InPosTail, postorder, PostPos); 29 if(i > InPosHead) 30 node->left = buildBinaryTree(inorder, InPosHead, i, postorder, PostPos); 31 return node; 32 } 33 };

算法的思路是:根據后序排列來確定對應中序的根節點(1),然后構建右子樹和左子樹(2)。 (括號的數字代表上面的三個方面)

分析:

這句話中 ”構建右左子樹“表明了可以通過遞歸邏輯實現,並確定了邊界的抽象(2)。而”根據后序排列來確定對應中序的根節點“是對遞歸邏輯的抽象(1)。

可以看到,我在上面兩句話中都刻意強調了”右左子樹“,而不是”左右子樹“,這是因為后序表的逆向排列正好代表了右樹優先的根節點,所以先建右子樹再創建左子樹是簡單直接的方法。這對2的邊界控制很有幫助。(恰當的抽象)

並且邏輯退出(3)放到了同層遞歸檢測,這不僅減少程序消耗而且顯示表明了結束條件,對可讀性也有幫助。

 

再貼一份代碼,這個遞歸的抽象邏輯和我寫的稍有不同(我的是優先構建右子樹,這份是不分左右順序),但是在邊界控制稍顯復雜(后序表的邊界控制)。具體的分析留給大家了。

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    private int postPos;
    
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if(inorder == null)
            return null;
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        for(int i=0; i<inorder.length; i++)
            map.put(inorder[i], i);
        return helper(inorder, 0, inorder.length-1, postorder, 0, postorder.length-1, map);
    }
    
    private TreeNode helper(int[] inorder, int inL, int inR,
                            int[] postorder, int postL, int postR, HashMap<Integer, Integer> map){
        if(inL > inR)
            return null;
        TreeNode root = new TreeNode(postorder[postR]);
        int index = map.get(root.val);
        root.left  = helper(inorder, inL, index-1, postorder, postL, postL+index-inL-1, map);
    root.right = helper(inorder, index+1, inR, postorder, postL+index-inL, postR-1, map);
        return root;
    }
}

 

最后,雖然遞歸有邏輯簡單,代碼清晰的優點,但是並不建議首先考慮用遞歸解決問題。好的程序還是需要通過深入解析寫出更快捷、更巧妙的算法,而不是把問題交給機器暴力解決。當然遞歸加剪枝可以避開一些不必要的搜索,不過大部分還是有替代的辦法。

希望能幫初學者對遞歸有個認識和理解,加深對計算機解題方式的理解。

 

轉載請注明出處,謝謝~  http://www.cnblogs.com/xiaoboCSer/p/4172741.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM