二叉树的四种遍历方法
遍历一颗二叉树,主要有前序遍历,中序遍历,后续遍历以及层序遍历这四种方法
掌握好这四种遍历方法是后续完成其他树相关题目的关键,而掌握好二叉树其他类型的树都可以由其转换得出
本文对于遍历的实现使用Java语言实现,仅解释遍历的逻辑,而不着重于Java的语法问题
阅读本文前需要具备深搜和广搜的概念基础,以及对二叉树的概念了解
了解好这些事项后,就让我们开始吧
理论基础
遍历一棵二叉树可以考虑深度优先(每次遍历到没有孩子节点的节点处)还是广度优先(一层一层的遍历),深度优先遍历又可以依据遍历左右节点的次序划分为前中后续遍历,而广度优先遍历就是所说的层序遍历
下面是一棵简单的二叉树
对于深度优先,在图中可以模拟一次1->2->4,走完这一条路的尽头后才会返回去遍历其他节点
前中后续的意思主要是在对中间节点的遍历,给定一个完整的父亲和其孩子节点
所谓前序,即先遍历中间节点,而中序是先遍历左节点再遍历中间节点,至于后续类比一下就是遍历完左右节点再遍历中间节点
当然,不是说遍历二叉树时先对每一个节点都这么遍历,像前序遍历在遍历时实际上是先将最左边的节点全部遍历了一遍,获取了一部分的中间节点,然后再依据规则去遍历左右节点
前序遍历:中->左->右 1->2->4->5->3->6->7
中序遍历:左->中->右 4->2->5->1->6->3->7
后序遍历:左->右->右 4->5->2->6->7->3->1
//可以关注一下根节点1的位置,与前中后正好相对应
层序遍历很好理解,从上到下,从左到右,就是按图中标号的顺序进行遍历
层序遍历:1->2->3->4->5->6->7
代码实现
下面的所有代码基于该二叉树类
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
前中后序遍历
练习通道:144. 二叉树的前序遍历 - 力扣(LeetCode)
145. 二叉树的后序遍历 - 力扣(LeetCode)
前中后续的原理是深度优先遍历dfs
,dfs
的实现依托于栈,用栈(先进后出)可以很好的模拟其遍历过程
dfs
相关题目通常使用递归实现,但递归运行的本质其实就是栈,直接创建一个栈进行实现其实就是迭代,这是需要了解的一点
统一的递归实现
递归一直都是一个很玄乎的东西,好在我们可以通过递归三部曲去尝试分析(以前序遍历为例)
- 首先要确定传入的参数和返回值:进行了遍历之后是需要把节点的值存储起来获得结果的,所以首先需要一个结果数组
res
,如果要开始一次dfs
,自然需要把根节点传进去,既然我们已经将结果数组当参数传进去了,返回值也就不需要了 - 其次确定退出条件:当我们沿着一条路走到结尾时再去遍历其他节点,那退出条件显而易见,如果一个节点的孩子为空就退出当前路线的遍历,也不用专门去判断左右孩子哪一个为空,我们在遍历中会自动全部遍历的
- 最后确定核心逻辑:我们在处理一个中间节点时下一步免不了处理其左右节点,递归调用后的左右节点可以看作“新的根节点”,递归的调用可以得到
preorder(root.left, res) preorder(root.right, res)
现在还有两个问题,核心逻辑的两个式子的顺序以及什么时候存储节点的值
在前面提到过前序遍历是中->左->右
的逻辑,先遍历所有左边的节点,那么两个式子的顺序可以很容易得出
很多时候分析递归时其实可以抽其中的一层出来看,以此来获得整个递归遍历的顺序,在开始遍历左节点前是要遍历中间节点的,而一开始传了根节点进来作为第一个左节点,那自然开始递归前需要先将根节点的值加入
而这样子的顺序也恰好是整个前序递归的逻辑
class Solution {
public void preorder(TreeNode root, List<Integer> res) {
if (root == null) return;
res.add(root.val);//中
preorder(root.left, res);//左
preorder(root.right, res);//右
}
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
preorder(root, res);
return res;
}
}
我们在分析时依据了前序中->左->右
,中后续和前序的区别就在于什么时候添加中间节点的值,那么我们只需将核心的三行代码调换位置即可得到其递归实现
//中序
preorder(root.left, res);//左
res.add(root.val);//中
preorder(root.right, res);//右
//后序
preorder(root.left, res);//左
preorder(root.right, res);//右
res.add(root.val);//中
前后序遍历的迭代实现
这里依然以前序遍历为例,有了递归代码的基础,其实可以直接得到三行核心代码
res.add(cur.val);
stack.push(cur.left);
stack.push(cur.right);
递归本身遍历的过程中会自动把顺序排好,而我们自己用栈实现时这样的加入后出栈的顺序是右->左->中,这里需要调转一下入栈的顺序
res.add(cur.val);
stack.push(cur.left);
stack.push(cur.right);
另外递归处理的时候遇到空节点会自动返回,我们在用栈模拟是无法处理空节点的,所以还需要增加对是否为空节点,判断是否入栈
别忘了添加完节点的值后需要将该节点出栈,代码如下:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new LinkedList<TreeNode>();
if (root == null) return res;
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
res.add(cur.val);
if (cur.right != null) stack.push(cur.right);
if (cur.left != null) stack.push(cur.left);
}
return res;
}
}
一般的前后序遍历这种相反结构的都可以通过反转结果进行实现,这里也不例外
- 是这样吗?前序是中左右,反转结果后是右左中,而后序是左右中,前两位是相反的
解决的方法也很简单,在左右节点入栈时更换一下执行顺序即可,这样出来的是中右左,反转后就是后序的左右中了,代码如下:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new LinkedList<TreeNode>();
if (root == null) return res;
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
res.add(cur.val);
if (cur.left != null) stack.push(cur.left);
if (cur.right != null) stack.push(cur.right);
}
Collections.reverse(res);
return res;
}
}
中序遍历的迭代实现
中序遍历的思路较前序遍历差距较大,前序遍历和递归的代码差不多是因为它所处理的节点恰好就是遍历的节点
那么重新进行分析,中序遍历每次先遍历左边的节点,那自然需要先找到左边的节点,添加完左边的节点后再去找到对应组内的中间节点进行添加,最后找到右边节点,这里主要是入栈上的顺序差异,添加节点和前序的逻辑是一样的
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode cur = null;
if (root != null) cur = root;
while (cur != null || !stack.isEmpty()) {//这里必须是两个条件,防止第一次进不来,如果在上面先把根节点入栈了,那么if里面的语句就得更改,整个添加的逻辑就变了
if (cur != null) {
stack.push(cur);//不需要像前序那样判断节点是否为空,这里需要依据节点是否为空进入不同的逻辑
cur = cur.left;//任何情况下都先找到最左下的节点,找到后再遍历时直接在else里面添加
}
else {
cur = stack.pop();//当第一次处理完最左边节点后,发现其右边没有节点时,下一个处理的自然就是中间节点
res.add(cur.val);
cur = cur.right;//转而处理右边节点
}
}
return res;
}
}
统一的迭代实现
根据上面三种遍历的实现,可以发现前后序的遍历是有一定巧合和技巧在的,所以如果要统一,那形式上应该和中序是差不多的
-
肯定不能和中序一样直接找到某一边的节点,这样子就前序就没办法进行了,所以形式上是中序的逻辑,而
if
语句内则应考虑递归时的逻辑,之前中序的迭代最大问题是当前节点和处理节点不一致,依据此点,我们可以加入一个标记位null
,如果遇到null
了就说明当前是要处理的节点 -
那么在入栈加入节点时碰到应当处理的节点,紧接着就应该再入一个
null
节点,else
语句内需要需要先排除额外加的null
节点的影响,再加入结果值
这里给出前序的完整代码,需要注意入栈的顺序,栈是先入后出的,入栈需要反着来出栈才能是想要的结果
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new LinkedList<TreeNode>();
if (root != null) stack.push(root);//不需要穷尽遍历到左节点再操作,所以这里就可以和之前一样直接入栈了
while (!stack.isEmpty()) {//这里根节点先入栈,循环条件和一开始的那个前序一样即可
TreeNode cur = stack.pop();//因为根节点是先入栈的,而后面在加入中间节点时还会再加一次,这里需要处理重复添加,同时也是为了获取当前节点
if (cur != null) {
if (cur.right != null) stack.push(cur.right);//右
if (cur.left != null) stack.push(cur.left);//左
stack.push(cur);//中
stack.push(null);//标记位
}
else {//本身cur遍历到了末尾
cur = stack.pop();//除掉null节点
res.add(cur.val);
}
}
return res;
}
}
同递归时的处理一样,统一后中序和后序只需变换核心代码的顺序即可
//中序遍历
if (cur.right != null) stack.push(cur.right);//右
stack.push(cur);//中
stack.push(null);
if (cur.left != null) stack.push(cur.left);//左
//后序遍历
stack.push(cur);//中
stack.push(null);
if (cur.right != null) stack.push(cur.right);//右
if (cur.left != null) stack.push(cur.left);//左
实战演练226. 翻转二叉树 - 力扣(LeetCode)
-
反转二叉树不像反转链表那样困难,只需将每一个节点的左右节点调换即可得到整体反转的二叉树,获取每一个节点就成了唯一问题
-
怎么获取呢,遍历即可,任选上面三种方法都可以,这里以前序实现最为贴合题目需求,因为它直接找到了每一个中间节点,再写一个
swap
方法即可完成此题(其他两种遍历还需要改变一些细节较为麻烦)
class Solution {
public void swap(TreeNode root) {
TreeNode tep = root.left;
root.left = root.right;
root.right = tep;
}
public TreeNode invertTree(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<TreeNode>();
if (root != null) stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
swap(cur);
if (cur.left != null) stack.push(cur.left);
if (cur.right != null) stack.push(cur.right);
}
return root;
}
}
层序遍历
练习通道:102. 二叉树的层序遍历 - 力扣(LeetCode)
在前面我们提到过,层序遍历的原理是广度优先搜索bfs
,bfs
的实现通常依托于队列(先进先出)
-
假设我们现在有了前一层从左到右的节点,将这些节点全部加入结果数组后,如何进行下一层的遍历
-
答案很简单,将这一层节点的孩子节点全部提前储存就好了,所以理论上可以不断创建新的数组去储存也是可行的
-
用队列去储存免去了额外空间的消耗且易于操作
还有一点需要注意,在Java中new
的一个新队列,其队列的长度是会随入队出队而不断变化的,所以我们在处理该层节点时需要用一个变量固定好需要遍历的长度
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();//结果数组
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if (root == null) return res;
queue.offer(root);//从根节点开始延申
while (!queue.isEmpty()) {
List<Integer> tep = new ArrayList<Integer>();
int size = queue.size();//固定长度
for (int i = 1; i <= size; i++) {
//将该层节点的值加入结果数组中
TreeNode cur = queue.poll();
tep.add(cur.val);
//遍历该层每一个节点时,将其非空孩子节点加入队列
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
//因为是从左到右,所以要先存左边的再存右边的
}
res.add(tep);
}
return res;
}
}
实战演练104. 二叉树的最大深度 - 力扣(LeetCode)
-
层序遍历是一层一层的走,用它来解决深度问题是在适合不过了,只需将上述代码修改一下即可获得答案
-
最小深度也是类似,遇到
null
节点直接返回记录的深度即可
class Solution {
public int maxDepth(TreeNode root) {
int depth = 0;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if (root == null) return depth;
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 1; i <= size; i++) {
TreeNode cur = queue.poll();
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
depth++;
}
return depth;
}
}
总结
遍历是整个二叉树题目的关键,需要我们进行熟练掌握,本文到此就结束啦!
参考:代码随想录二叉树的遍历部分