二叉树:深度优先和广度优先遍历

章内容为代码随想录二叉树遍历方式总结,具体内容看:代码随想录——二叉树理论基础

二叉树的遍历方式

二叉树的遍历方式大体分为深度优先和广度优先遍历。

深度优先遍历包含以下几种,都包含有递归和迭代的实现方式:

  • 前序遍历
  • 中序遍历
  • 后序遍历

广度优先遍历一般就是层次遍历,掌握迭代法。

image

图像来自代码随想录

实现方式

深度优先遍历一般采用递归的方式实现,同时也可以借助栈来实现非递归的遍历

广度优先遍历一般采用队列来实现。

递归方式遍历

写递归函数需要确定递归三要素:

  1. 递归函数的参数和返回值
    像以下这些简单的遍历,就不需要设置什么返回值,因此返回值为void。参数则是传入树的根节点,以用于遍历。
void dfs(TreeNode node)
  1. 终止条件
    当遍历到的节点是空节点,就返回
if(cur==null)return;
  1. 单层递归的逻辑
    在这里,前序、中序和后序遍历的递归的逻辑几乎都是一样的,只是访问当前节点的顺序不同,具体看下面的代码。

前序遍历

public void dfs(TreeNode node){
    if(node==null){
        return;
    }
    ans.add(node.val);
    dfs(node.left);
    dfs(node.right);
}

中序遍历

public void dfs(TreeNode node){
    if(node==null)return;
    dfs(node.left);
    ans.add(node.val);
    dfs(node.right);
}

后序遍历

public void dfs(TreeNode node){
    if(node==null)return;
    dfs(node.left);
    dfs(node.right);
    ans.add(node.val);
}

可以发现这几个遍历方式就是在访问当前节点的顺序有所不同。具体有以下几道题目:

  • 144.二叉树的前序遍历
  • 94.二叉树的中序遍历
  • 145.二叉树的后序遍历

迭代方式遍历

采用迭代的方式进行遍历,主要是要采用栈这个数据结构。

前序遍历

迭代方式的前序遍历我觉得carl哥的图做的很清楚,看一遍应该就懂了。具体看:迭代遍历

前序遍历的遍历顺序是中、左、右。当访问“中”的时候,需要将这个节点弹出,然后将其右子树和左子树按顺序压入。

至于为什么是先右后左的顺序入栈,这是因为栈是先进后出啊。栈的先进后出决定了子树需要按照先右后左的顺序进入,才可以先访问左节点。(具体还是看动图,非常清楚)

同时还要注意对于空节点的处理。递归的时候对于空节点的处理是,碰到了就返回。那迭代方式怎么处理空节点呢

正确的做法是,只有不空的时候才将节点入栈。

迭代方式何时退出循环?

因为用进行前序遍历,当栈空,说明所有节点都遍历完了。

梳理以下迭代前序遍历的流程如下

  1. 将根节点入栈

  2. 若栈不空则执行:

    1. 弹出栈顶元素(访问中央节点)
    2. 如果右子树非空,则右子树入栈
    3. 如果左子树非空,则左子树入栈

以144.二叉树的前序遍历为例,迭代前序遍历的代码为:

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> ans=new ArrayList<>();
    if(root==null)return ans;
    Deque<TreeNode> stack=new LinkedList<>();
    stack.push(root);
    while(!stack.isEmpty()){
        TreeNode cur=stack.pop();
        ans.add(cur.val);
        if(cur.right!=null){
            stack.push(cur.right);
        }
        if(cur.left!=null){
            stack.push(cur.left);
        }
    }

    return ans;
}

后序遍历

后序遍历和前序遍历有一定的相似性,因此先讨论后序遍历。

后序遍历是按照左右中的方式进行的。在迭代过程中可以按照中右左的方式遍历(对比前序遍历迭代过程,就是在压入左右子节点的顺序不同)。然后对遍历访问的结果进行翻转,就是后序遍历。

以145.二叉树的后序遍历为例子,迭代后序遍历的代码为:

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ans=new ArrayList<>();
    if(root==null)return ans;
    Deque<TreeNode> stack=new LinkedList<>();
    stack.push(root);
    while(!stack.isEmpty()){
        TreeNode cur=stack.pop();
        ans.add(cur.val);
        if(cur.left!=null)stack.push(cur.left);
        if(cur.right!=null)stack.push(cur.right);
    }

    Collections.reverse(ans);
    return ans;
}

image

一张潦草的手写

中序遍历

由于前面两种遍历方式,都是访问中间节点,然后处理中间节点。因此访问和处理的顺序是一致的。而中序遍历不一致。

对此,需要一个指针用于访问节点,而栈则暂时存储节点,用于处理元素(就是指弹出,再访问其左右孩子的操作)。

感觉中序遍历还是比较不好理解

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ans=new ArrayList<>();
    if(root==null)return ans;
    Deque<TreeNode> stack=new LinkedList<>();
    TreeNode cur=root;
    while(cur!=null || !stack.isEmpty()){
        if(cur!=null){
            stack.push(cur);
            cur=cur.left;
        }else{
            cur=stack.pop();
            ans.add(cur.val);
            cur=cur.right;
        }
    }
    return ans;
}

层序遍历

层序遍历采用队列实现。访问到哪一个节点,就把它的左右子树(如果有)加入到队尾。

以102.二叉树的层序遍历为例

public class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ans=new ArrayList<>();
        Deque<TreeNode> queue=new LinkedList<>();
        if(root!=null)queue.offer(root);

        while(!queue.isEmpty()){
            int size=queue.size();
            List<Integer> list=new ArrayList<>();
            for(int i=0;i<size;i++){
                TreeNode cur=queue.pop();
                list.add(cur.val);
                if(cur.left!=null)queue.offer(cur.left);
                if(cur.right!=null)queue.offer(cur.right);
            }
            ans.add(list);
        }
        return ans;
    }
}

层序遍历基本都是这个模板,根据不同的要求会稍做改变。


到此为止。二叉树的基本遍历就结束了。个人感觉层序遍历是最容易理解的。而深度优先遍历,既有递归法(简单),又有迭代法(借助栈),且中序遍历的迭代方式又与前序和后序不大一样,因此要时常回顾一下。

posted @ 2022-07-22 22:58  ShaunY  阅读(298)  评论(0编辑  收藏  举报