目录
一.内容铺垫
前言:大多数同学遇见递归题目,往往有一种模棱两可的感觉。写递归算法的人,往往分成两种,一种是将整个过程从头想到尾,将每一步递归都具体化为一个个栈,然后思考如何解决问题。第二种是脑子里有三段论的印象,却不能清晰的判断三段论的内容。有时候稀里糊涂就把题做出来。总而言之,用这两种方式刷题,刷再多也无济于事,因为你无法保证在面试的时候100%通过。
所以本章将讨论如何规范化,乃至公式化的解决递归这类题目。将算法知识,抽象化归为数学公式,可谓是宝剑在手天下我有!
1.递归本质
举例:求 5!
解析: 递归本质是系统创建一系列栈,每次重复调用该函数就会将该待处理的程序压入栈。 比如,我们最开始创建的程序是5*f(4) ,所以最先进栈,接着依次是4*f(3),3*f(2),2*f(1),f(1)=1 。
注意:虽然系统创建了程序,但是还没有计算,因为没有到达终止条件,系统只是在不停的压入栈栈。
最后到f(1)=1时候,无法继续压入栈。所以开始回溯,程序依次计算直到返回最开始进入的栈的程序的值,即为最终结果。
总结:我们用最简单的例子还原了整个递归过程。所以,如果我们从细节处微观的来解决问题可谓是层层嵌套,十分复杂。 但是我们从中发现了规律,每一小步的过程都一样!
2.递归元素
定义说明一
终止条件:终止条件 统称为某一件事情的终止发生情况,即不可分割的最后一步。
eg:1. 阶乘的第一个数一定是1。
2.斐波纳契数列 前两个数 是1。
3.叶子节点或者空节点。
4.书的一页。
5.教学楼的一个人。
抽象统一: 终止条件的特性是不可分割,不可构造。
比如我们要算阶乘1x2x3x4,我们可以发现2!是1!x2 但是我们无法找到1!由谁构造出来。
我们同样无法得知斐波那契数列前两个数是由谁相加的来。
叶子节点如何再进行拆分?空节点如何再进行拆分? 我们无从得知。
我们要统计一本书有多少页,那么我们还能把书的一页撕开算成两页么?同样是不可分割的。
我们要统计一个教学楼有多少人,我们可以先统计一个年级-->再统计一个班--->再统计一个小组--->最后统计人。不可能把人劈成两半来计算。
这些就是基元
定义说明二
递归关系:表示第n项和第n+1项之间的关系。
抽象统一:递归关系是表明前一项与后一项之间的联系,而且每项的递归关系都相同
eg:
1.二叉树深度:父树的深度加一就是子树的的深度 这就是前一项与后一项的关系。
2.斐波纳契数列:1,1,2,3,5 后一个是前两个数相加
3. 杨辉三角 : 后一个是上面两个相加之合。
二.递归模板
展示模板:
第一步:根据题目判断返回类型。
第二步:表达终止条件。
第三步: 写最后一层递归条件
例题一:144. 二叉树的前序遍历
确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下
void traversal(TreeNode* cur, vector<int>& vec)
确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
确定最后一层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
大脑要映射出最后一步递归条件,切记不要展开思考具体细节!就把它看为最后一层,只有三个节点。
同时,强调所有递归功能既定实现!也就是先行假定本函数的功能已经实现了。
本题函数功能就是前序遍历把元素放进vector
逻辑:
先把头结点元素放进vector
左枝left进行函数实现,前序遍历把左边元素放入vector
右枝right进行函数实现,前序遍历把右边元素放入vector
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,再看一下完整代码:
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
那么前序遍历写出来之后,中序和后序遍历就不难理解了
只需要修改最后一层顺序!!
你要彻彻底底体会这句话:“所有递归问题函数功能已经实现”!!!
功能是什么? 功能是中序遍历后把元素放进vector。
那么我对root->left 使用traversal , 它就已经把左枝遍历完了
让后我在放中间的
最后处理右边的
这才是中序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
例题二235. 二叉搜索树的最近公共祖先
第一步: 判断函数返回类型为TreeNode*
第二步:判断终止条件,根据函数返回类型可知基元条件肯定是TreeNode*。那么TreeNode*不可分的单元是什么? 即为NULL。
第三布:最后一层的递归条件,(因为所有的步骤都是重复相同的,所以我们只看最后一层的返回是什么样的)。如图:
这就是单层递归结构,并且是最后一层。如大家所见,我将叶子结点都点了很多点点,这是代表,其实left 里面包含了无穷无尽的层数,right也包含着无穷无尽的层数,但是我都不管,我只看这最后一层。因为我一但调用了 寻找最近公共祖先的函数,那么就算left,right里面包含了无数个递归,但是他已经给出了结果。 你再次细品,我不管这个函数怎么实现的,我只要调用了,你就得给我答案吧?你给了我答案,就是已经找到了最近公共祖先,你怎么实现我也不管,所以我管你千千万万层,你left和right都得给出我答案。 这就是为什么我要强调写最后一层递归条件。这时,root 分别接收了left和right的答案,再来判断最后答案。
看代码:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return nullptr;//基元
if (root->val > p->val && root->val > q->val)
return lowestCommonAncestor(root->left, p, q);
//假如 p 和 q 都在左子树上,那么我root只接受left的答案!
if (root->val < p->val && root->val < q->val)
return lowestCommonAncestor(root->right, p, q);
//假如 p 和 q 都在右子树上,那么我root只接受right的答案!
//假如 p,q 一个在左,一个在右。 那么就相当于root接受left,right 返回的都是null 所以我只能返回root了
return root;
}
例题三701. 二叉搜索树中的插入操作
第一步:判断返回类型,题目要求返回的是TreeNode * 。
第二步:找到终止条件,首先了解题目要求是让在二叉搜索树中插入新的节点,所以我们的终止条件是一定要创建一个TreeNode的。 让后根据返回类型可知,终止条件是要返回这个创造的TreeNode。 所以我们可以得知,终止条件是遇到NULL节点就返回一个新创建的节点。
第三步:最后一层的递归条件,如图所示
left 是已经创建完毕的左子树,right是创建完毕的右子数。我们将左子树和右子树分别对接给root 则拼接成完整的root树。
代码如下:
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if(root==nullptr)
{
TreeNode* node=new TreeNode(val);
return node;
}
//基元 遇到NULL返回一个Val节点
if(root->val>val) root->left=insertIntoBST(root->left,val);
//拼接左子树
if(root->val<val) root->right=insertIntoBST(root->right,val);
//拼接右子树
return root;
}
};
例题四 450. 删除二叉搜索树中的节点
第一步:判断返回类型,返回类型是TreeNode*
第二步: 寻找终止条件。 这里着重强调 终止条件不能固定化的认为就是返回NULL,因为终止条件它的实际含义是不可分割的最后一步。
它从定义上讲不是一个特定的实体,而是一个最基本的过程元素。 根据本题目要求,删除节点。所以基元不是见到二叉树就返回NULL。而是要告诉自己,我最基本的一步是删除节点。
再根据返回类型可知,最后一步一定要返回一个节点 那么返回什么节点就要根据情况来说。如图所示:
这次基元分为五种情况,还外加一个没找到删除节点,返回root。
if (root == nullptr) return root;
// 第一种情况:没找到删除的节点,遍历到空节点直接返回了
if (root->val == key) {
// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
if (root->left == nullptr && root->right == nullptr) {
///! 内存释放
delete root;
return nullptr;
}
// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
else if (root->left == nullptr) {
auto retNode = root->right;
///! 内存释放
delete root;
return retNode;
}
// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
else if (root->right == nullptr) {
auto retNode = root->left;
///! 内存释放
delete root;
return retNode;
}
// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
else {
auto retNode=root->right;
while(retNode->left!=nullptr)
{
retNode=retNode->left;
}
retNode->left=deleteNode(root->left,key);
root=root->right;
return root;
}
}
现在重点讲解最后一种情况,即左右都有子树的情况,实际上要删除节点的左子树和右子树可以是单个节点,或者是一颗完整的树。 那小伙伴会被吓到,这可怎么处理呀?
这里可以再次运用递归思想,我管你左边一大坨是什么东西,当我重新调用这个函数的功能后,你就是已经处理过的单个节点了。所以我就可以把所有情况划归为一种,即最后一层递归。然后我需要做的,就是把左边调用过后的值拼接到右子树的最左面节点的左孩子上,让后把右子树拼接上去即可。结合代码再次理解:
auto retNode=root->right;
while(retNode->left!=nullptr)
{
retNode=retNode->left;
}//找到右子树的最左孩子
retNode->left=deleteNode(root->left,key);
//处理过的左子树拼接在右子树上(再次递归思想)
root=root->right;
// 右子树代替root
return root;
第三步:最后一层的递归条件
判断一下,大于val 去右边处理,小于val去左边处理。easy!
整体代码:
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
if (root->val == key) {
// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
if (root->left == nullptr && root->right == nullptr) {
///! 内存释放
delete root;
return nullptr;
}
// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
else if (root->left == nullptr) {
auto retNode = root->right;
///! 内存释放
delete root;
return retNode;
}
// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
else if (root->right == nullptr) {
auto retNode = root->left;
///! 内存释放
delete root;
return retNode;
}
// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
else {
auto retNode=root->right;
while(retNode->left!=nullptr)
{
retNode=retNode->left;
}
retNode->left=deleteNode(root->left,key);
root=root->right;
return root;
}
}
if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};