day14:二叉树-递归遍历、迭代遍历
递归遍历
理论基础:
递归定义:
递归是定义一个对象或问题的方式,通过引用该对象或问题的较小版本来完成,比如斐波那契数列
递归调用:
递归调用是在函数或过程内部调用自身的行为。
底层实现:
递归的底层实现通常依赖于计算机的栈数据结构。当一个函数被调用时,计算机会将函数的局部变量、参数和返回地址等信息存储在栈中,这被称为栈帧。递归调用时,每次新的函数调用都会创建一个新的栈帧,将其推入栈中。
递归的基本执行过程:
- 当函数第一次被调用的时候,创建一个初始的栈帧,将参数和局部变量压入栈中,并存储函数的返回地址。
- 在函数内部,进行递归调用。这会导致新的栈帧被创建并压入栈中,其中包含了相同的函数的参数和局部变量。
- 递归调用会持续,每次都会创建一个新的栈帧,直到达到递归终止条件。这个条件通常在递归定义中明确定义,它告诉程序何时停止递归。
- 当递归达到终止条件后,函数开始回溯。回溯过程中涉及弹出栈帧,将局部变量和参数恢复到之前的状态,并跳回到存储的返回地址。
- 回溯过程一直持续到递归的最初调用处,然后整个递归调用序列才会完成。
注意事项:
递归调用的过程中会消耗栈空间,如果递归深度过大,可能会导致栈溢出错误。
递归三要素
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
前/中/后序递归遍历:
按照三要素分析来确定递归:
1、确定递归函数的参数和返回值
参数:一个根节点和一个集合用于存放遍历过的数据
返回值:void
2、确定终止条件
递归遍历是深度优先搜索的一个过程,当沿着一个方向一直搜索到空节点的时候就往回
3、确定单层递归的逻辑
前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,然后是左和右
中序遍历是左中右的循序,所以在单层递归的逻辑,是要先取左节点的数值,然后是中和右
后序遍历是左右中的循序,所以在单层递归的逻辑,是要先取左节点的数值,然后是右和中
前序递归遍历:中左右
public class Solution {
public IList<int> PreorderTraversal(TreeNode root) {
List<int> result = new List<int>();
PreorderTraversalRecursive(root, result);
return result;
}
//前序遍历:中左右
public void PreorderTraversalRecursive(TreeNode node, List<int> result)
{
if(node == null)
return;
result.Add(node.val);//中
PreorderTraversalRecursive(node.left, result); //左
PreorderTraversalRecursive(node.right, result); //右
}
}
中序递归遍历:左中右
//中序遍历
public class Solution {
public IList<int> InorderTraversal(TreeNode root) {
List<int> result = new List<int>();
InorderTraversalRecursive(root, result);
return result;
}
//中序遍历:左中右
public void InorderTraversalRecursive(TreeNode node, List<int> result)
{
if(node == null)
return;
InorderTraversalRecursive(node.left, result); //左
result.Add(node.val);//中
InorderTraversalRecursive(node.right, result); //右
}
}
后序递归遍历:左右中
//后续遍历
public class Solution {
public IList<int> PostorderTraversal(TreeNode root) {
List<int> result = new List<int>();
PostorderTraversalRecursive(root, result);
return result;
}
//后序遍历:左右中
public void PostorderTraversalRecursive(TreeNode node, List<int> result)
{
if(node == null)
return;
PostorderTraversalRecursive(node.left, result); //左
PostorderTraversalRecursive(node.right, result); //右
result.Add(node.val);//中
}
}
迭代遍历
在递归中有提到,递归用到了栈的数据结构,每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以,也可以用栈来实现二叉树的前中后序遍历了。
前序迭代遍历:
前序遍历是中左右,处理步骤如下:
1、每次先处理的是中间节点,那么先将根节点放入栈中。
2、然后将右孩子加入栈。
3、再加入左孩子。
之所以先将右孩子加入栈,再加入左孩子,是因为我们采用的是栈的数据结构,栈是先入后出的,所以只有先加入右孩子,再加入左孩子,出栈的时候才能是前序遍历的中左右顺序。
public class Solution {
public IList<int> PreorderTraversal(TreeNode root) {
List<int> result = new List<int>();
//根节点为空的情况
if(root == null)
return result;
Stack<TreeNode> stk = new Stack<TreeNode>();
stk.Push(root);
while(stk.Count > 0)
{
TreeNode node = stk.Pop(); //中
result.Add(node.val);
if(node.right != null) //右(空节点不需要入栈)
stk.Push(node.right);
if(node.left != null) //左(空节点不需要入栈)
stk.Push(node.left);
}
return result;
}
}
中序迭代遍历:左中右
中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点,导致访问的顺序和处理的顺序不一致,所以,在迭代法中对二叉树进行中序遍历的话,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
public class Solution {
public IList<int> InorderTraversal(TreeNode root) {
List<int> result = new List<int>();
if(root == null)
return result;
TreeNode cur = root;
Stack<TreeNode> stk = new Stack<TreeNode>();
while(cur != null || stk.Count > 0)
{
//将当前节点及其左子节点入栈
if(cur != null)
{
stk.Push(cur);
cur = cur.left;//左
}
else
{
//弹出栈顶元素,即最左边的节点
cur = stk.Pop();
result.Add(cur.val);//中
//处理右子树
cur = cur.right;//右
}
}
return result;
}
}
后序递归遍历:左右中
后序遍历中左右,因为先序遍历是中左右,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。
public class Solution {
public IList<int> PostorderTraversal(TreeNode root) {
List<int> result = new List<int>();
if(root == null)
return result;
Stack<TreeNode> stk = new Stack<TreeNode>();
stk.Push(root);
while(stk.Count > 0)
{
TreeNode node = stk.Pop(); //中
result.Add(node.val);
if(node.left != null) //左(相对于前序遍历,这里更改顺序,左节点先入栈)
stk.Push(node.left);
if(node.right != null) //右(空节点不需要入栈)
stk.Push(node.right);
}
result.Reverse();//反转链表
return result;
}
}