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:

判断两个二叉树是否一样?

原文地址,<LeetCode OJ> 100. Same Tree

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

镜像反转二叉树,

原文地址,<LeetCode OJ> 226. Invert Binary Tree

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

获取前序遍历结果

原文地址,<LeetCode OJ> 144/145/94 Binary Tree (Pre & In & Post) order Traversal

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/

你可能感兴趣的:(LeetCode,C++,算法,面试,设计)