前序遍历:对于当前节点,先输出该节点,然后输出他的左孩子,最后输出他的右孩子。
题目链接:144. 二叉树的前序遍历
二叉树遍历的递归实现都很简单,只需要按上面所述的前序遍历的概念出发编写代码即可:
先树根,然后左子树,然后右子树。每棵子树递归。
在递归函数最前面设置好递归出口:当root == null,退出当前递归。
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null) return res;
//每个节点都做下面这三步
res.add(root.val);//输出树根
preorderTraversal(root.left);//再输出左孩子
preorderTraversal(root.right);//最后输出右孩子
return res;
}
}
前、中、后序遍历的迭代实现有很多种写法,我这里总结了一种统一的代码框架适用于三种遍历,只需要做简单的修改即可。
参考:题解 by jason
关键问题:
1、遍历思路和栈的配合
前序遍历是从根节点开始,不断向左孩子遍历,遇到一个节点就将立即将其输出,而右子树是在一层层返回时才输出的,所以迭代每一轮实际上就是递归的一层。递归有内部栈可以暂存还未访问的节点,迭代则需要我们显式地将每一层暂时访问不到的节点存入栈中。
对于前序遍历来说,每一层都是先输出根、左子树,最后再输出右子树,所以每一层都把暂时访问不到的右孩子存入栈中,在返回上一层时再取出。
选择栈作为辅助空间的原因是:栈的存储特点是先入后出,和二叉树遍历的一层层深入和一层层返回相契合。
2、什么时候是遍历的终点?
只用栈来存右孩子,所以栈为空时,有两种可能:1、初始状态;2、所有节点都已访问过。所以还要再找一个条件来确定遍历终点。
当栈为空,且在内部while循环最后执行了 root = root.left之后,如果到了遍历终点,就是root == null,所以增加一个条件:root == null。
所以当
stack.isEmpty() && root == null
说明到达遍历的终点。
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
while(!stack.isEmpty() || root != null){
//不断向左向下遍历到最左节点
while(root != null){
res.add(root.val);//先输出根节点
if(root.right != null) stack.push(root.right);//将这一层暂时访问不到的右孩子存入栈中
root = root.left;//将root更新为左孩子
}
//退出循环时,说明到了二叉树的最底层最左边节点
if(!stack.isEmpty()) root = stack.pop();//取出当前层对应的右孩子,重复上面的步骤
}
return res;
}
}
中序遍历:对于当前节点,先输出它的左孩子,然后输出节点本身,最后输出它的右孩子。
题目链接:94. 二叉树的中序遍历
按上面所述的中序遍历的概念出发编写代码即可:
先左子树,然后根,然后右子树。每棵子树递归。
在递归函数最前面设置好递归出口:当root == null,退出当前递归。
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return res;
inorderTraversal(root.left);
res.add(root.val);
inorderTraversal(root.right);
return res;
}
}
算法设计:
因为中序遍历先访问的是左孩子,然后才是根和右孩子,又因为右孩子可以通过root.right获取到,所以只需要将每一层的根存入栈中即可。
对每个节点,都是先不断向左向下遍历,每一层节点都把它的根节点存入栈中(右孩子可以通过根节点找到,所以可以不必入栈),直到节点的左孩子为空时,说明到达了最底层的最左节点;然后才开始弹出栈顶,访问最左节点,然后取当前节点的右孩子(root = root.right,关键一步),回到 最开始的步骤:
遍历终点:
同前序遍历的迭代实现,当栈为空且root==null时,说明到达遍历终点。
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();
res.add(root.val);
//关键一步
root = root.right;//无论root.right是否为Null,都执行这一步,如果为null,则不会进入while循环寻找左孩子,直接跳到pop处,继续弹出上一层的根节点;如果不为null,则右孩子入栈,下一轮以右孩子为根继续寻找他的左孩子。
}
return res;
}
}
参考:官方题解的评论区 java代码,官方题解修改了树的结构,而评论处的代码恢复了原来的二叉树结构。
morris遍历,简单来说就是构造线索二叉树。morris遍历是迭代实现中序遍历,同时可以省去栈的使用,用当前节点的左子树最右节点的右指针指向当前节点,对每个节点都做这样的处理,就能得到一棵线索二叉树,在遍历到叶子节点时,通过叶子节点的right就能到达它的中序遍历下的后继节点。本质上就是把空闲的指针域利用起来,指向当前节点的中序遍历直接后继节点,在需要栈弹出该节点的时候,通过当前节点的右指针就能获取该节点。
算法流程:(99题的官方题解)
假设当前遍历到的节点为 x:
照着代码分析上述步骤思路会更清晰。
第一次和其他中序遍历的流程一样,是正常的访问,如果此时root有左子树,则还不能输出root.val;
第二次是因为在前一次遇到root时将它的前驱节pre点(root的左子树的最右节点)的右指针域指向了root,所以在该前驱节点作为当前节点root时输出并取它的右孩子(root = root.right),就第二次遇到了前一个root,也就是 pre 的后继节点。此时再次遇到root时,说明root的左子树全部输出完毕,所以 root.val 可以输出了,然后前往root的右子树重复左子树上所做的操作。
之后这个前驱节点 pre 和后继节点 root 之间的联系不会再用到了,所以需要把它的前驱节点 pre 的右指针域恢复为null,避免改变树的结构。
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return res;
//当前节点
TreeNode cur = root;
//前驱节点
TreeNode pre = null;
while(root != null){
//如果当前节点root没有左子树,则输出该节点,然后进入右子树(如果有右子树则进入右子树,没有右子树则取之前构造的直接后继节点)
if(root.left == null){
res.add(root.val);
root = root.right;
}
//如果当前节点root有左子树,则:
else{
pre = root.left;//左子树的根节点
//寻找左子树的最右节点(前驱节点)
while(pre.right != null && pre.right != root){
pre = pre.right;
}
//如果前驱节点的right没有被赋值过,则对它的右指针域赋值(第一次访问root)
if(pre.right == null){
pre.right = root;//将最右节点的右指针域指向root,pre就是root的前驱,root是pre的后置。
root = root.left;//当前节点root进入左子树
}
//如果前驱节点的right被赋值过,则在第二次遍历到当前节点root时,该节点的前驱的右指针域要恢复回null,避免修改树的结构(第二次访问root,可以输出root)
if(pre.right == root){//整棵线索二叉树只有这个前驱节点的right指向当前的root,所以可以用这个条件判断pre是否是root的前驱节点
pre.right = null;//恢复前驱节点的右指针域
res.add(root.val);//第二次遇到root,说明root的左子树已经输出完毕,根据中序遍历的特点此时可以输出root.val
root = root.right;//进入右子树
}
}
}
return res;
}
}
后序遍历:对于当前节点,先输出它的左孩子,然后输出它的右孩子,最后输出节点本身。
题目链接:145. 二叉树的后序遍历
按上面所述的中序遍历的概念出发编写代码即可:
先左子树,然后右子树,然后根。每棵子树递归。
在递归函数最前面设置好递归出口:当root == null,退出当前递归。
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
postorderTraversal(root.left);
postorderTraversal(root.right);
res.add(root.val);
return res;
}
}
节点 cur 先到达最右端的叶子节点并将路径上的节点入栈;
然后每次从栈中弹出一个元素后,cur 到达它的左孩子,并将左孩子看作 cur 继续执行上面的步骤。
从算法流程可以发现,是前序遍历将先左后右改成先右后左即可,
前序:根->左->右,
修改成:根->右->左,
最后逆序输出:左->右->根,
得到逻辑上的后序遍历,但实际上并不是真正的后序遍历。
最后将结果反向输出即可:
Collections.reverse(res);
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
TreeNode last = null;
while(!stack.isEmpty() || root != null){
while(root != null){
res.add(root.val);
if(root.left != null) stack.push(root.left);
root = root.right;
}
if(!stack.isEmpty()) root = stack.pop();
}
//将结果逆序输出
Collections.reverse(res);
return res;
}
}
后序遍历最开始要从根节点不断深度遍历到最左节点,这个过程中root不断更新为自身的左孩子root.left,所以二叉树每一层的当前root都要入栈,直到到达最左节点。
到达最左节点后开始向上返回,弹出栈顶,也就是当前的root。如果节点还有右孩子,则还要继续遍历右子树,令root=root.right(和中序遍历相同);右子树已遍历过,或者没有右子树,才输出当前节点。所以可以发现有两种返回当前root的情况:
关键问题:如何判断返回当前root的是哪一种情况?
方案1:使用队列记录状态变量(比较麻烦)
方案2:对res进行判断(基于对输出结果的理解)
以下面的二叉树为例:
1
2 3
4 5 6 7
假设此时已输出4,返回上一层的2,即root=2,
因为此时还没有访问2的右子树,所以要继续将2压入栈中,然后令root=root.right以便回到之前的while循环遍历右子树。
输出右孩子5之后,此时的res[4,5],弹出栈顶2,相当于又返回root=2的情况,此时2的右子树已访问完毕,所以可以输出2,。
可以发现如果没有访问过右子树,res的最后一个元素不是root.right.val,而如果访问过,则根据后序遍历的特点,在返回root时,res的最后一个元素一定是root.right.val。所以可以通过res.get(res.size() - 1) == root.right.val来判断是否访问过右子树。
前面说到过还有一种情况:当前root没有右子树。也要归入访问过右子树的条件中。
所以后序遍历迭代实现的关键部分整理得:
root = stack.pop();//取出栈顶
if(root.right == null || res.get(res.size()) == root.right.val){
res.add(root.val);
root=null;//(易遗漏!!)
}
else{
stack.push(root);//头结点要重新压回栈中
root = root.right;
}
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
TreeNode last = null;
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
//取出栈顶
root = stack.pop();
//如果root没有右子树或右子树被访问过
if(root.right == null || (res.size() > 0 && res.get(res.size() - 1) == root.right.val)){
res.add(root.val);
root = null;//(易遗漏!!)
}
//如果root的右子树还未被访问过
else{
stack.push(root);//root重新入栈(易遗漏)
root = root.right;//继续遍历右子树
}
}
return res;
}
}
方法3:设置辅助节点指向遍历的前一个节点
设置一个节点last,指向当前root的右孩子,初始值设为null。
从左子树返回root时last == null 且root.right!=null,所以会继续遍历右子树,在遍历右子树时更新last为root.right;
从右子树返回root后,因为last==root.right,说明是从右子树返回的,可以输出root.val。
以下面的二叉树为例:
1
2 3
4 5 6 7
此时已输出4,返回上一层的2,即root=2,此时last=null,
可以认为此时还没有访问2的右子树,所以要继续将2压入栈中,然后令last=root.right,root=root.right以便回到之前的while循环遍历右子树。
输出右孩子5之后,弹出栈顶2,相当于又返回root=2的情况,此时2的右子树已访问完毕,即last==root.right,所以可以输出2。
实际上方法3和方法3本质相同,都是基于后序遍历的特点,后序遍历输出root的前一个节点必定是root.right(前提:不为null)。
前面说到过还有一种情况:当前root没有右子树。也要归入访问过右子树的条件中。
所以整理得:
root = stack.pop();//取出栈顶
if(root.right == null || last == root.right){
res.add(root.val);
last = root;//更新last为当前节点
root=null;//这一步是为了能够继续弹出栈顶,而不是重新回到之前的while循环重复对root的左子树做遍历。
}
else{
stack.push(root);//头结点要重新压回栈中
root = root.right;
}
实现代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
TreeNode last = null;
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);
root = root.left;
}
//取出栈顶
root = stack.pop();
//如果root没有右子树或右子树被访问过
if(root.right == null || last == root.right){
res.add(root.val);
last = root;更新last为当前节点,易遗漏!!
root = null;
}
//如果root的右子树还未被访问过
else{
stack.push(root);//root重新入栈(易遗漏)
root = root.right;//继续遍历右子树
}
}
return res;
}
}