递归遍历 (必须掌握)
本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。
这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
好了,我们确认了递归的三要素,接下来就来练练手:
以下以前序遍历为例:
- 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入List来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
public void preorder(TreeNode root, List<Integer> result)
- 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (root == null){return;
}
3.确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中间节点的数值,代码如下:
result.add(root.val); //中preorder(root.left, result); //左preorder(root.right, result); //右
单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,再看一下完整代码:
class Solution {//前序遍历·递归·LC144_二叉树的前序遍历public List<Integer> preorderTraversal(TreeNode root) {
//定义了一个名为preorderTraversal 的公共方法,该方法接受一个TreeNode类型的参数root,表示二叉树的根节点。
//该方法的返回类型是List<Integer>,表示二叉树的前序遍历结果。List<Integer> result = new ArrayList<Integer>();//创建一个名为result的ArrayList对象,用于存储遍历结果preorder(root,result);//调用preorder方法进行前序遍历,传入根节点root和结果列表result。return result;//返回遍历结果。}public void preorder(TreeNode root,List<Integer> result){
//定义了一个名为preorder的方法,用于执行前序遍历。
//该方法接受一个TreeNode类型的参数root,表示当前遍历的节点,以及一个List<Integer>类型的参数result,用于存储遍历结果。if(root == null){ //如果当前节点root为空,则直接返回,结束当前递归。return;}result.add(root.val);//中;将当前节点的值添加到结果列表中,表示当前节点的访问顺序是在中间。preorder(root.left,result); //递归地对左子树和右子树进行前序遍历。preorder(root.right,result); }
}
同day04中总结的递归法一样:
递归法的宗旨就是紧紧抓住原来的函数究竟返回的是什么?作用是什么? 即可
其余的细枝末节不要细究,编译器会帮我们自动完成。
递归函数preorder()需要传入TreeNode类型的节点和一个List<Integer>类型的参数result用来存储遍历结果,调用递归 直接把相关的参数填进去即可。
preorder(root.left,result); //递归地对左子树和右子树进行前序遍历。
preorder(root.right,result);
class Solution {//中序遍历·递归·LC145_二叉树的后序遍历public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<Integer>();posorder(root,result);return result;}public void posorder(TreeNode root, List<Integer> result){if(root == null){return;}posorder(root.left,result); //左posorder(root.right,result); //右result.add(root.val); //中}
}
class Solution {public List<Integer> inorderTraversal(TreeNode root) {//中序遍历·递归·LC94_二叉树的中序遍历List<Integer> result = new ArrayList<Integer>();inorder(root,result);return result;}public void inorder(TreeNode root, List<Integer> result){if(root == null){return;}inorder(root.left,result); //左result.add(root.val); //中inorder(root.right,result); //右}
}
迭代遍历 (基础不好的录友,迭代法可以放过)
思路
为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?
我们在栈与队列:匹配问题都是栈的强项 (opens new window)中提到了,递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
此时大家应该知道我们用栈也可以是实现二叉树的前后中序遍历了。
前序遍历(迭代法)
我们先看一下前序遍历。
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。
此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?其实还真不行!
但接下来,再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。
class Solution {//前序遍历顺序:中-左-右,入栈顺序:中-右-左public List<Integer> preorderTraversal(TreeNode root) {
//定义了一个名为preorderTraversal 的公共方法,该方法接受一个TreeNode 类型的参数root,表示二叉树的根节点。
//该方法的返回类型是List<Integer>,表示二叉树的前序遍历结果。List<Integer> result = new ArrayList<Integer>();//创建一个名为 result 的 ArrayList 对象,用于存储遍历结果。//然后检查根节点是否为空,如果为空,则直接返回空结果列表。if(root == null){return result;}Stack<TreeNode> stack = new Stack<>();//创建一个名为stack的栈,用于存储待访问的节点。并将根节点root入栈。stack.push(root);while(!stack.isEmpty()){//使用循环来遍历栈中的节点,直到栈为空为止。TreeNode node = stack.pop();//从栈中弹出一个节点,表示当前要访问的节点。result.add(node.val);//将当前节点的值添加到结果列表中,表示当前节点的访问顺序是在中间。//将当前节点的右子节点和左子节点依次入栈。//由于栈的特性,右子节点先入栈,然后左子节点入栈,这样在下一次循环时先访问左子节点,符合前序遍历的顺序。if(node.right != null){stack.push(node.right);}if(node.left != null){stack.push(node.left);//然后继续执行while{}中的内容,使用循环来遍历栈中的节点,直到栈为空为止} }return result;//最后返回结果列表result,其中存储了二叉树的前序遍历结果。}
}
有了前序遍历,先讨论后序遍历(迭代法),比较容易:
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:
// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()){TreeNode node = stack.pop();result.add(node.val);if (node.left != null){stack.push(node.left);}if (node.right != null){stack.push(node.right);}}Collections.reverse(result);return result;}
}
中序遍历(迭代法):(这个过程需要多理解以下,第一遍有点绕没看懂)
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}//创建一个名为stack的栈,用于存储待访问的节点。//同时创建一个名为cur的节点,初始值为根节点root,用于迭代遍历二叉树。Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;//使用循环来遍历二叉树的所有节点,直到当前节点为空 且 栈为空为止。while (cur != null || !stack.isEmpty()){//如果当前节点cur不为空,则将当前节点入栈,并将当前节点移动到其左子节点。if (cur != null){stack.push(cur);cur = cur.left;}else{//如果当前节点为空,则从栈中弹出一个节点,表示当前要访问的节点,//将该节点的值添加到结果列表中,然后将当前节点移动到其右子节点。cur = stack.pop();result.add(cur.val);cur = cur.right;}}return result;//最后返回结果列表result,其中存储了二叉树的中序遍历结果。}
}
统一迭代(基础不好的录友,迭代法可以放过)
这是统一迭代法的写法, 如果学有余力,可以掌握一下