二叉树的先序遍历、中序遍历、后序遍历和层序遍历
二叉树的先序遍历其实就是树的dfs 深度优先遍历。
中序遍历和后序遍历 只是在深度优先思想的基础上做一点调整,左孩子、右孩子和根节点遍历的前后顺序不同罢了。
DFS实现有 递归实现 和 栈实现 两种,所以这三种遍历方法也分别有这两种实现方式。以下介绍都是在子树先左后右的基础上,原则上这三种遍历主要区别在于根的顺序位置,左右子树的顺序可以随意调换。
层序遍历其实就是树的BFS广度优先遍历方式。BFS实现一般由队列实现。
对这两种方法的详细介绍可见这篇博文: 图/树的DFS和BFS带图详解。
示例:如下二叉树
先序遍历
先序遍历又称前序遍历,先根遍历,前序周游。对于树的每个结点作为根节点来说,都是先访问树的根节点,然后访问其一个(左)子树,最后访问其另一个(右)子树。(根左右/根右左,默认根左右)
比如示例二叉树的先序遍历结果为:1(根) ->2 ->4 ->5 ->7 ->3 ->6
题目(Leetcode 144)
给定一个二叉树,返回它的 前序 遍历。
示例:
输入: [1,null,2,3]
输出: [1,2,3]
递归实现(同dfs)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public static List<Integer> res;
public List<Integer> preorderTraversal(TreeNode root) {
res = new ArrayList<>();
recursive(root);
return res;
}
public void recursive(TreeNode root){
if(root == null) return;
res.add(root.val);
recursive(root.left);
recursive(root.right);
}
}
迭代(栈)实现
实现一(同dfs直接栈实现)
class Solution {
public static List<Integer> res;
public List<Integer> preorderTraversal(TreeNode root) {
res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<TreeNode>();//构造一个栈
stack.push(root);//初始将根节点压入栈
while(!stack.isEmpty()){//进入循环
TreeNode node = stack.pop();//弹出一个结点,将其右、左子节点压入栈中(这样出栈的时候顺序就是左右)
if(node.right!=null) stack.push(node.right);
if(node.left!=null) stack.push(node.left);
res.add(node.val);
}
return res;
}
}
实现二:按列迭代遍历
另一种迭代思想: 深度优先,可理解为一列一列遍历(对应的广度优先,是一层一层也就是一行一行遍历)。对每个节点来说,优先从其深度开始遍历(遍历它对应的一列)。
树的一列:按照深度优先的思想,对每个节点来说,他的深度就是它的左结点的左结点的左结点,一直到没有左结点为止,我们把其理解为一列。如下图所示红色箭头标识的就是把1当作父结点开始的一列。
具体实现:对每个结点来说,都先把其列(所有左结点的左结点…)全部遍历加入栈中,该列结束后,在转换到对应的右节点作为根节点重复上述操作。
这种思想的先序遍历就是在按列遍历的过程中,先把根节点加入结果res,然后再向下依次遍历列(所有左节点),然后再遍历右节点。实现根左右。
class Solution {
public static List<Integer> res;
public List<Integer> preorderTraversal(TreeNode root) {
res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<TreeNode>();//构造一个栈
while(!stack.isEmpty()|| root != null){//进入循环
while(root!=null){//先把左节点的左节点的所有左节点都压入栈中,然后依次加入结果list,实现先根后左
res.add(root.val);
stack.push(root);
root = root.left;
}//将所有左节点(当前这列)遍历完后,栈中含有所有的左节点,依次弹出并将其右节点压入栈,依次根据根左右的情况加入list
if(!stack.isEmpty()){
root = stack.pop();
root = root.right;
}
}
return res;
}
}
中序遍历
中序遍历又称中根遍历,中序周游。对于树的每个结点,都先访问一个(左)子树,再访问根结点,最后访问另一个(右)子树。(左根右/右根左,默认左根右)
比如示例二叉树的中序遍历结果为:4 ->2 ->5 ->7 ->1(根) ->3 ->6
题目(Leetcode 94)
给定一个二叉树,返回它的 中序 遍历。
示例:
输入: [1,null,2,3]
输出: [1,3,2]
递归实现(类似dfs)
class Solution {
//递归法 同dfs
public static List<Integer> res;
public List<Integer> inorderTraversal(TreeNode root) {
res = new ArrayList<>();
recursive(root);
return res;
}
public void recursive(TreeNode root){
if(root == null) return;
recursive(root.left);
res.add(root.val);
recursive(root.right);
}
}
迭代(栈)实现(按列迭代遍历)
按列迭代遍历思想的中序遍历就是在按列遍历的过程中,从根节点开始,对每个节点来说,先遍历其列(所有左节点),全部遍历完压入栈后,依次出栈加入结果res中,实现先左后根,出栈一个左结点遍历其右节点重复上述步骤,实现左根右。
class Solution {
public static List<Integer> res;
public List<Integer> inorderTraversal(TreeNode root) {
res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
while(!stack.isEmpty() || root != null){
while(root != null){
stack.push(root);//步骤1:将该根节点的所有左边结点全部都加入栈中 实现先左的顺序 直至左边没有了
root = root.left;
}
//最左边的全部加入栈中后,开始遍历右边
if(!stack.isEmpty()){
root = stack.pop();//将上述左边的依次弹出,实现遍历
res.add(root.val);//先遍历左边,该左边结点遍历后,将其弹出栈,并将其相应右节点加入栈中,重复上述步骤1
root = root.right;//将有右边的加入栈中,后右
}
}
return res;
}
}
后序遍历
后序遍历又称后根遍历,后序周游。对于树的每个结点,都先访问一个(左)子树,再访问另一个(右)子树,最后访问根结点。(左右根/右左根,默认左右根)
比如示例二叉树的后序遍历结果为:4 ->7 ->5 ->2 ->6 ->3 ->1(根)
题目(Leetcode 145)
给定一个二叉树,返回它的 中序 遍历。
示例:
输入: [1,null,2,3]
输出: [3,2,1]
递归实现(类似dfs)
class Solution {
public static List<Integer> res;
public List<Integer> postorderTraversal(TreeNode root) {
res = new ArrayList<>();
recursive(root);
return res;
}
public void recursive(TreeNode root){
if(root == null) return;
recursive(root.left);
recursive(root.right);
res.add(root.val);
}
}
迭代(栈)实现
实现一(直接栈实现先序遍历倒着输出)
因为dfs实际上是先序遍历(根左右/根右左),后序遍历是左右根/右左根,所以只需要在实现dfs的过程时,将dfs(先序遍历)的结果倒序输出 即为后序遍历的结果。比如:根右左 倒过来就是 左右根。
class Solution {
public static List<Integer> res;
public List<Integer> postorderTraversal(TreeNode root) {
res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
if(root == null) return res;
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
//因为该题后序顺序要求左右根,所以倒过来的先序实现是根右左,所以要先压左子树后压右子树,这样出栈的时候就是先右后左了
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
res.add(0,node.val);//倒着输出
}
return res;
}
}
实现二(按列迭代遍历思路)
后序遍历的按列迭代遍历思路:列的定义同上。
不同的是,需要维护每一列的尽头结点,标记为1,其他结点标记为0;
对于每个结点来说,主要分为两步:
①一直遍历左节点即它的一列,到列的尽头为止标记为1,然后转向它的右节点。
列的尽头: 遍历到的该结点没有左结点或者其左结点已经遍历过即为列的尽头结点。
②当列的尽头结点的右结点存在时,将其当作父结点重复上一步;不存在的话,满足出栈条件,就将该结点出栈。
出栈后,若此刻栈顶元素为0的话,将栈顶元素(列的尽头结点)更新,标记为1,将该结点当作父结点,重复第一步;若此刻栈顶元素本就为1,说明回溯到上一列,右节点已遍历过,直接出栈。
出栈条件:当栈顶元素正好是列的尽头结点(标记为1,表明其没有左节点)且该结点没有右结点或其右结点已遍历过时(没有未遍历过的右节点)。
以示例二叉树为例:
初始栈stack = {},栈顶在左边;标记栈tmpstack = {}。
① 对于根节点1来说,他的深度(那一列)如下图所示(红色箭头就是一列),将其依次全部压入栈中。在这一列的尽头结点4,tmpstack标记为1;非尽头结点,tmpstack标记都为0。stack = {4,2,1},tmpstack = {1,0,0}
判断结点4的右节点,不存在,则结点4此刻满足出栈条件,结点4出栈,加入结果列表res。出栈后,栈顶元素/列的尽头结点更新为2,将结点2标记为1。stack = {2,1},tmpstack = {1,0}
当前遍历结果:4->
② 当第一列到尽头且没有右结点 出栈后,栈顶更新为2,遍历其右结点,右结点存在为5,重复第一步,找出第二列,此时第二列的尽头就是结点5;stack = {5,2,1},tmpstack = {1,1,0}
到列的尽头,再次重复第一步:5有右节点7,从7开始的第三列的尽头也是7。stack = {7,5,2,1},tmpstack = {1,1,1,0}
尽头结点7没有右节点,符合出栈条件出栈。stack = {5,2,1},tmpstack = {1,1,0}
当前遍历结果:4-> 7->
③ 当列到尽头7,即其没有左节点出栈后,重复第二步,栈顶更新为5,5本来标记就为1,表明回溯到上一列,5此刻既是该列的尽头又没有未遍历过的右子树,符合出栈条件出栈。stack = {2,1},tmpstack = {1,0}
当前遍历结果:4-> 7-> 5->
④ 出栈后,栈顶更新为2,此时2本来标记就为1,表明回溯到上一列,2此刻既是列的尽头又没有未遍历过的右子树,符合出栈条件出栈。stack = {1},tmpstack = {0}
当前遍历结果:4-> 7-> 5-> 2->
⑤ 出栈后,栈顶更新为1,重复第一步,1标记为列的尽头,stack = {1},tmpstack = {1};
找1的右结点3存在,从3开始重复第一步,3没有左结点为列的尽头,标记为1,stack = {3,1},tmpstack = {1,1};
找3的右结点6存在,从6开始重复第一步,6没有左结点为列的尽头,标记为1,stack = {6,3,1},tmpstack = {1,1,1}
找6的右结点不存在,此时6既没有左结点(标记为1)又没有右节点,符合出栈条件出栈。stack = {3,1},tmpstack = {1,1}
当前遍历结果:4-> 7-> 5-> 2-> 6->
⑥ 出栈后,栈顶更新为3,3此刻已标记为1,表明回溯到上一列,3此刻既是列的尽头又没有未遍历过的右子树,满足出栈条件,出栈。stack = {1},tmpstack = {1}
当前遍历结果:4-> 7-> 5-> 2-> 6-> 3->
⑦ 出栈后,栈顶更新为1,此时1本来就标记为1,表明回溯到上一列,1此刻既是列的尽头又没有未遍历过的右子树,满足出栈条件,出栈。栈为空,遍历结束。stack = {},tmpstack = {}
当前遍历结果:4-> 7-> 5-> 2-> 6-> 3-> 1
class Solution {
public static List<Integer> res;
public List<Integer> postorderTraversal(TreeNode root) {
res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
Stack<Integer> tmpstack = new Stack<Integer>();//维护每一列的尽头结点标记为1
//1表示该结点已经没有(未遍历的)左结点,因为我们是按列从左往后开始遍历的,所以依次遍历出每列最底层左结点 就能实现左右根的后序遍历。
while(!stack.isEmpty() || root != null){
while(root != null){//先把所有左节点都压入栈中
stack.push(root);
tmpstack.push(0);//标识为左节点
root = root.left;
}
while(!stack.isEmpty() && tmpstack.peek()==1){//遍历到1时说明已经到当前列的尽头结点,可以遍历到结果中了。
tmpstack.pop();
res.add(stack.pop().val);
}
if(!stack.isEmpty()){//在把当前所有左子树的左节点压栈完毕后,压入右节点 循环:将右节点的所有左子树的左节点压栈,可以理解为下一列
tmpstack.pop();
tmpstack.push(1);
root = stack.peek();//peek()是返回栈顶元素的值,pop()是返回栈顶元素的值并删除栈顶元素
root = root.right;
}
}
return res;
}
}
层序遍历
层序遍历,又称层次遍历,顾名思义就是按层次一层一层的遍历。
比如示例二叉树的层序遍历结果为:1 ->2 ->3 ->4 ->5 ->6 ->7
题目(Leetcode 102)
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/\
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal
bfs队列实现
看到层序遍历,很明显第一反应想到BFS,因为这两种都是一层一层的遍历。但因为该题要求的输出结果需要将每一层的结点分别输出,输出为二维数组,这比简单的BFS遍历又要多一个要求,需要计算出每个结点的层数,将其分别加入不同层的数组中。
如果只需输出为一维数组的顺序遍历,则按图/树的DFS和BFS带图详解里的实现即可,从根节点压入队列开始,弹出一个结点,将该结点的所有子节点压入队列,然后依次弹出队列直到队列为空。
因为我们需要把每一层的层数记录下来,所以在上述思想基础上做一点点小小的改动,从根节点开始(初始队列中时第一层的结点),再设置一个循环一层一层的弹出:每次将队列中所含有的这一层的所有结点分别弹出 + 把其所有子节点压入队列,层数+1跳出这层循环继续下一层的遍历。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
int depth = 0;//层数
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if(root == null) return res;
//初始化将根节点压入队列
queue.offer(root);
//然后进行第一步,弹出一个结点,压入其所有子节点,直到队列
while(!queue.isEmpty()){
int deplen = queue.size();//该层有多少个子节点即当前队列中有几个元素,然后将子节点的孩子结点都加入到下一层
//为该层在二维数组结果中添加一个一维数组
res.add(new ArrayList<Integer>());
//第二层循环:表示一层一层的队列弹出和压入
for(int i = 0; i < deplen; i++){
TreeNode node = queue.poll();//弹出一个结点
res.get(depth).add(node.val);
//压入node的所有子节点
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
depth++;
}
return res;
}
}
dfs递归实现
虽然DFS是深度优先的遍历,但是因为该题输出的是二维数组,需要分层记录,所以我们同样可以使用DFS进行遍历,只需在遍历时将层数记录下来,以此来将遍历到的结点放入他们对应的层的数组中。
class Solution {
public static List<List<Integer>> res;
public List<List<Integer>> levelOrder(TreeNode root) {
res = new ArrayList<List<Integer>>();
//递归 dfs 按每一层,先左 后右
if(root == null) return res;
dfs(root,0);
return res;
}
public void dfs(TreeNode root, int depth){//depth当前root的层数,第一层的depth为0
if(root == null) return;
while(depth > res.size()-1){
res.add(new ArrayList<Integer>());
}
res.get(depth).add(root.val);
depth++;
dfs(root.left,depth);
dfs(root.right,depth);
}
}
综上可以看出,递归法比迭代法速度都要快一些。