递归详解
递归分析的基本步骤
以下是递归编写的几个关键步骤:
- 确定基本情况:
- 递归算法应该有一个或多个基本情况,这些情况不需要递归,并且可以直接解决。
- 例如,对于二叉树的遍历,基本情况是当前节点为空。
- 确定递归情况:
- 除基本情况外的所有其他情况都是递归情况。
- 通常,递归情况会将问题分解为更小的子问题,并递归地解决它们。
- 使用返回值:
- 根据问题的性质,递归函数可能会有返回值。确保在每个基本情况和递归情况中都有返回值。
我们用二叉树的前序遍历的例子来说明递归的思路:
- 基本情况: 如果当前节点为空,则没有什么要做的,直接返回。
- 递归情况:
- 访问当前节点。
- 递归地遍历左子树。
- 递归地遍历右子树。
在代码中,这可以表示为:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return; // 基本情况
vec.push_back(cur->val); // 访问当前节点
traversal(cur->left, vec); // 递归地遍历左子树
traversal(cur->right, vec); // 递归地遍历右子树
}
用栈来理解递归
在实际执行递归函数时,系统内部确实是使用栈来处理函数调用的。
- 每次函数调用都创建一个新的栈帧:
- 当一个递归函数被调用时(无论是第一次调用还是递归调用),都会为该调用创建一个新的栈帧。
- 这个栈帧包含了函数的参数、局部变量、返回地址以及其他必要信息。
- 栈帧被压入调用栈:
- 新的栈帧被压入调用栈的顶部。
- 当前执行的函数总是位于栈的顶部。
- 递归调用意味着更多的栈帧:
- 每当函数递归调用自身,一个新的栈帧就会被压入栈中。
- 这导致栈不断增长,直到达到递归的基本情况,此时不再进行更多的递归调用。
- 返回意味着从栈中弹出栈帧:
- 当一个函数完成执行并返回时,其栈帧从调用栈中弹出。
- 控制权返回到现在位于栈顶的函数。
- 深度过大的递归可能导致栈溢出:
- 由于物理内存是有限的,栈空间也是有限的。
- 如果递归调用的深度过大,调用栈可能会耗尽所有可用空间,导致栈溢出错误。
为了更具体地理解,可以考虑一个简单的递归函数,例如计算数字 ( n ) 的阶乘。每次递归调用都会将一个新的栈帧压入栈中,直到 ( n ) 为 1,此时函数开始返回,栈帧逐个从栈中弹出。
例题及题解
144.二叉树的前序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur==nullptr)return;
vec.push_back(cur->val);
traversal(cur->left,vec);
traversal(cur->right,vec);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> vec;
traversal(root, vec);
return vec;
}
};
145.二叉树的后序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur==nullptr)return;
traversal(cur->left,vec);
traversal(cur->right,vec);
vec.push_back(cur->val);
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> vec;
traversal(root, vec);
return vec;
}
};
94.二叉树的中序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur==nullptr)return;
traversal(cur->left,vec);
vec.push_back(cur->val);
traversal(cur->right,vec);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> vec;
traversal(root, vec);
return vec;
}
};
递归方法很简单,接下来我们尝试使用迭代的方法解决有关二叉树的一些问题
迭代详解
前序遍历
首先是前序 要将递归的前序遍历改为迭代的方式,可以使用栈来实现。栈可以帮助我们保存节点的信息,从而模拟递归过程。
以下是改为迭代写法的代码及详解:
代码
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
if (root == nullptr) {
return result;
}
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* node = stk.top();
stk.pop();
result.push_back(node->val);
if (node->right != nullptr) {
stk.push(node->right);
}
if (node->left != nullptr) {
stk.push(node->left);
}
}
return result;
}
};
思路详解
- 初始化:
- 创建一个 vector
result用于保存前序遍历的结果。 - 创建一个 stack
stk用于保存待遍历的节点。
- 边界条件:
- 如果 root 是 nullptr,直接返回 result,因为没有节点需要遍历。
- 栈的初始化:
- 将 root 节点压入栈中。
- 迭代过程:
- 当栈不为空时,执行以下步骤:
- 弹出栈顶节点,并将该节点的值添加到 result 中。
- 如果该节点有右子节点,将右子节点压入栈中(注意,这里先压入右子节点,是因为栈是后进先出的结构,我们希望左子节点能先被遍历)。
- 如果该节点有左子节点,将左子节点压入栈中。
- 返回结果:
- 返回 result,它现在包含了前序遍历的结果。
这种方法通过使用栈来显式地维护待访问的节点列表。
后序遍历
顺序是:左子树 -> 右子树 -> 根节点。
为了使用栈实现后序遍历的迭代方法,我们可以利用前序遍历的思路,但稍作调整。具体来说,我们可以先按照根节点 -> 右子树 -> 左子树的顺序遍历(这实际上是前序遍历的一个变种),然后将结果反转,即得到后序遍历的结果。
以下是修改为后序遍历的代码及详解:
代码
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
if (root == nullptr) {
return result;
}
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* node = stk.top();
stk.pop();
result.push_back(node->val);
if (node->left != nullptr) {
stk.push(node->left); // 注意这里先压入左子节点
}
if (node->right != nullptr) {
stk.push(node->right); // 然后压入右子节点
}
}
reverse(result.begin(), result.end()); // 反转结果
return result;
}
};
思路详解
- 初始化:
- 创建一个 vector
result用于保存后序遍历的结果。 - 创建一个 stack
stk用于保存待遍历的节点。
- 边界条件:
- 如果 root 是 nullptr,直接返回 result,因为没有节点需要遍历。
- 栈的初始化:
- 将 root 节点压入栈中。
- 迭代过程:
- 当栈不为空时,执行以下步骤:
- 弹出栈顶节点,并将该节点的值添加到 result 中。
- 如果该节点有左子节点,将左子节点压入栈中。
- 如果该节点有右子节点,将右子节点压入栈中。
- 反转结果:
- 使用
std::reverse函数将 result 中的元素反转。
- 返回结果:
- 返回 result,它现在包含了后序遍历的结果。
通过这种方法,我们实际上是在模拟后序遍历的过程,但使用了前序遍历的思路并在最后进行了反转,从而得到后序遍历的结果。
中序遍历
对于中序遍历的迭代实现,我们需要稍作调整。中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
我们可以使用一个栈来帮助我们。具体的迭代过程如下:
- 对于当前节点,将其推入栈,并将当前节点更新为其左子节点,直到当前节点为空。
- 当当前节点为空时,从栈顶弹出一个节点,并访问它。
- 将当前节点更新为弹出节点的右子节点,然后回到步骤1。
重复上述过程直到栈为空且当前节点也为空。
下面是对应的代码实现:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> stk;
TreeNode* curr = root;
while (curr != nullptr || !stk.empty()) {
while (curr != nullptr) {
stk.push(curr);
curr = curr->left;
}
curr = stk.top();
stk.pop();
result.push_back(curr->val);
curr = curr->right;
}
return result;
}
};
这里的关键是,当我们访问一个节点时,我们要确保已经访问了它的左子树。为了实现这一点,我们在进入一个新节点时,不断地将其左子节点推入栈中,直到没有左子节点为止。这样,当我们从栈中弹出并访问一个节点时,我们可以确保其左子树已经被访问过了。然后,我们可以转到这个节点的右子树,并重复上述过程。