LeetCode總結,遞迴的理解與設計
一,簡介遞迴的原理
遞迴演算法:是一種直接或者間接地呼叫自身的演算法。在計算機編寫程式中,遞迴演算法對解決一大類問題是十分有效的,它往往使演算法的描述簡潔而且易於理解。
1,參考於書籍中的講解:
遞迴演算法的實質:是把問題轉化為規模縮小了的同類問題的子問題。然後遞迴呼叫函式(或過程)來表示問題的解。 遞迴的原理基於子問題,子問題只是一個小的規模的父問題,因為本文是假設子問題能夠求解的,而父問題的解由子問題的解組成,所以父問題和子問題應該解決的是同一個問題,結果應該是一致的。 遞迴演算法解決問題的特點:(1) 遞迴就是在過程或函式裡呼叫自身。
(2) 在使用遞迴策略時,必須有一個明確的遞迴結束條件,稱為遞迴出口。
(3) 遞迴演算法解題通常顯得很簡潔,但遞迴演算法解題的執行效率較低。所以一般不提倡用遞迴演算法設計程式。
(4) 在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存。遞迴次數過多容易造成棧溢位等。所以一般不提倡用遞迴演算法設計程式。
遞迴的原理,其實就是一個棧(stack), 比如求5的階乘,要知道5的階乘,就要知道4的階乘,4又要是到3的,以此類推,所以遞迴式就先把5的階乘表示入棧, 在把4的入棧,直到最後一個,之後呢在從1開始出棧, 看起來很麻煩,確實很麻煩,他的好處就是寫起程式碼來,十分的快,而且程式碼簡潔,其他就沒什麼好處了,執行效率出奇的慢.
例: 求n的階乘
int fac(n){ if(n == 0 || n == 1){ return 1; } else{ return n*fac(n-1); //自己呼叫自己,求n-1的階乘 } }
2,個人的經驗性總結
設計一個遞迴演算法,我認為主要是把握好如下四個方面:
1.函式返回值如何構成原問題的解
其實最先應該明瞭自己要實現的功能,再來設計函式的意義,特別是這個函式的返回值,直接關係到函式是否存在正確結果,函式返回什麼遞迴子程式呼叫就會返回什麼,而遞迴子程式呼叫的返回值會影響到最終結果,因此必須關注函式的返回值,子程式返回的結果被呼叫者所使用(也可以不使用),呼叫者又會返回,也就是說函式返回值是一致性的。
關鍵問題是如何由遞迴子程式構成原問題的解呢?很重要的問題,但是這裡不能一概而論,
比如我們需要的是遍歷的這個過程而不是遞迴子函式的返回的解,那麼我們就可以不接收返回值或者直接寫成void函式,典型的就是二叉樹的三大遍歷方式。
我們的解也有可能是由子問題的解組合而成(新增各種運算),無論如何這裡應該試著從子問題和原文題的關係入手。
2.遞迴的截止條件。
截止條件就是可以判斷出結果的條件,是遞迴的出口啊,最好總是先設計遞迴出口。
3.總是重複的遞迴過程。
一般簡單的遞迴可以顯式的用一個數學公式表達出來,比如前面的求階乘問題。但是很多問題都不是簡單的數學公式問題,我們需要把原問題分解成各種子問題,而子問題使用的是同樣的方法,獲取的是同樣的返回值。
4.控制遞迴邏輯。
有的時候為了能實現目的,我們需要控制邊界啊什麼的,下面有具體介紹。
二,LeeCode實戰理解
例子1:
判斷兩個二叉樹是否一樣?
1),函式返回值如何構成原問題的解
明確函式意義,
判斷以節點p和q為根的二叉樹是否一樣,獲取當前以p和q為根的子樹的真假情況
bool isSameTree(TreeNode* p, TreeNode* q){
函式體.....
}
解的構成,
每一個節點的左子樹和右子樹同時一樣才能組合成原問題的解。原問題接收來自所有子問題的解,只要有一個假即可所有為假(與運算)
2),遞迴的截止條件
截止條件就是可以得出結論的條件。
如果p和q兩個節點是葉子,即都為NULL,可以認為是一樣的,return true
如果存在一個為葉子而另一個不是葉子,顯然當前兩個子樹已經不同,return false
如果都不是葉子,但節點的值不相等,最顯然的不一樣,return false
3)總是重複的遞迴過程
當2)中所有的條件都“躲過了”,即q和p的兩個節點是相同的值,那就繼續判斷他們的左子樹和右子樹是否一樣。
即,isSameTree(p->left,q->left)和isSameTree(p->right,q->right)
4)控制重複的邏輯
顯然只有兩個子樹都相同時,才能獲取最終結果,否則即為假。
如下所示
return (isSameTree(p->left,q->left))&&(isSameTree(p->right,q->right));
最終程式碼
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if(p==NULL&&q==NULL)
return true;
else if(p==NULL&&q!=NULL)
return false;
else if(p!=NULL&&q==NULL)
return false;
else if(p!=NULL&&q!=NULL && p->val!=q->val)
return false;
else
return (isSameTree(p->left,q->left))&&(isSameTree(p->right,q->right));
}
};
例子2
映象反轉二叉樹,
1),函式返回值如何構成原問題的解
明確函式意義,
將根節點root的左右子樹映象反轉,並獲取翻轉後該根節點的指標
TreeNode* invertTree(TreeNode* root) {
函式體.....
}
解的構成,
原問題的解總是由已經解決的左子問題和已經解決的右子問題調換一下即可。
2),遞迴的截止條件
截止條件就是可以得出結論的條件。
如果root不存在,即NULL,顯然此時不用再反轉,返回NULL即可
3)總是重複的遞迴過程
當2)中所有的條件都“躲過了”,即root存在(當然左右子可能不存在)
我們就總是
先獲取將root的左子樹映象翻轉後的根節點,
再獲取將root的右子樹映象翻轉後的根節點,
交換兩者,並返回root即可。
TreeNode* newleft = invertTree(root->right);//先獲取翻轉後的左右子樹的根節點TreeNode* newright = invertTree(root->left);
root->left = newleft;//實現翻轉
root->right = newright;
return root;//返回結果
4)控制重複的邏輯
以上已完成
最終程式碼:
class Solution {
public:
//將根節點反轉,並獲取翻轉後該根節點的指標
TreeNode* invertTree(TreeNode* root) {
if(root == NULL){
return NULL;
}else{
//這樣做將:樹的底層先被真正交換,然後其上一層才做反轉
TreeNode* newleft = invertTree(root->right);
TreeNode* newright = invertTree(root->left);
root->left = newleft;
root->right = newright;
return root;
}
}
};
例子3
獲取前序遍歷結果
1),函式返回值如何構成原問題的解
明確函式意義,
獲取以當前節點root為根的前序遍歷結果
vector<int> preorderTraversal(TreeNode* root) {
函式體....
}
解的構成,
在這裡遞迴子程式的返回值並不是函式的解,我們只關心遍歷順序即可,而遞迴子程式的解並不關心,所以遞迴子程式的返回值我們並不需要(遞迴子函式不接受即可,但是還是要返回結果哈)。
2),遞迴的截止條件
截止條件就是可以得出結論的條件。
如果root為NULL,說明已經沒有子樹了,顯然就截止了
立刻返回結果(這個結果返回給遞迴進來的上一層函式,上一層函式並不接受即可)
3)總是重複的遞迴過程
當2)中的條件都“躲過了”,
則即刻獲取當前根節點的元素值,接著先訪問以左子為根的子樹,接著右....
4)控制重複的邏輯
前序遍歷的基本規則,總是先訪問根節點,再左節點,最後右節點
完整程式碼:
class Solution {
public:
vector<int> result; //將儲存遍歷的所有結果
vector<int> preorderTraversal(TreeNode* root) {
if(root){
result.push_back(root->val);
preorderTraversal(root->left); //遞迴子函式不接受解
preorderTraversal(root->right);
}
return result;
}
};
未完待續......
注:本博文為EbowTang原創,後續可能繼續更新本文。如果轉載,請務必複製本條資訊!
原文地址:http://blog.csdn.net/ebowtang/article/details/50763086
原作者部落格:http://blog.csdn.net/ebowtang
參考資源:
【1】IBM社群,http://www.ibm.com/developerworks/cn/linux/l-recurs.html
【2】LeetCode OJ,https://leetcode.com/problemset/algorithms/