树的遍历

820 阅读5分钟

一.树的简介

1.1 树的定义

树作为一个比较重要的数据结构,由节点和边组成,父节点通过边连接到子节点。二叉树是每个节点最多有两个子节点的树,是最常见的一种树。

1.2 树的应用场景

二叉树本身的应用场景不多,但其衍生的树应用很多,比如jdk1.8的HashMap使用了红黑树,数据库索引使用了B树和B+树

1.3 树的结构

java中的二叉树每个节点都维护一个左节点和一个右节点,图和节点代码如下

image.png

class TreeNode{
    int val;
    TreeNode left;
    TreeNode right;
}

多叉树每个节点都维护一个子节点列表,图和节点代码如下

image.png

class TreeNode{
    int val;
    List<TreeNode> childList;
}

1.4 树的遍历

这里以递归实现的先序遍历为例,二叉树需要递归遍历其左右子节点

public void recursionTraverse(TreeNode treeNode){
    if(treeNode != null){
        System.out.println(treeNode.val);
        recursionTraverse(treeNode.left);
        recursionTraverse(treeNode.right);
    }
}

多叉树需要遍历其所有子节点

public void recursionTraverse(TreeNode treeNode){
    if(treeNode != null){
        System.out.println(treeNode.val);
        for(TreeNode child : treeNode.childList){
            recursionTraverse(child);
        }
    }
}

多叉树的遍历与二叉树的遍历思想相同,后续我们都以二叉树为讲解案例

二.广度优先遍历(BFS)

2.1 定义

树的广度优先遍历就是树的层序遍历,从根节点开始,根节点为第一层,根节点的子节点为第二层,以此类推。遍历完上一层的节点,才可以遍历下一层的节点,直到没有节点可以访问。需要一个队列来保存每一层的节点列表。

2.2 图解及代码

image.png

  1. 循环前的准备,把第一层节点加入到队列中,也就是根节点node-0;
  2. 第一层循环,把第二层节点加入到队列中,具体操作是从队列中逐个取出节点,打印输出或保存到集合中,然后把其非空子节点加入到队列中,也就是node-1和node-2;
  3. 第二层循环,把第三层节点加入到队列中,操作和上一步相同,也就是node-4、node-5、node-7;
  4. 第三层循环,把第四层节点加入到队列中,操作和上一步相同,由于没有子节点,本次无新节点入队
  5. 第四层循环,队列中已无节点,循环结束,遍历完成
public static void floorTraverse(TreeNode treeNode){
        if (treeNode == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(treeNode);

        while (queue.size() != 0){
            int size = queue.size();
            while (size > 0){
                TreeNode node = queue.poll();
                System.out.println(node.val);
                if (node.left != null) queue.add(node.left);
                if (node.right != null) queue.add(node.right);
                size--;
            }
        }
    }

2.3 时间复杂度与空间复杂度

  1. 由于每个节点都会被访问到一次,时间复杂度为O(n),n为节点数
  2. 每个节点都需要保存到集合中,统一打印,空间复杂度为O(n),n为节点数

三.深度优先遍历(DFS)

3.1 定义

从根节点开始对每一条可能的分支路径深入到不能再深入为止,回溯节点继续深入其他分支;按照根节点、左子节点、右子节点的访问顺序可以具体分为先序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根);按照实现的方式又可以具体分为递归法、非递归法

3.2 图解及代码

image.png

3.2.1 先序遍历

递归法:

  1. 首先打印根节点信息
  2. 递归遍历左子树
  3. 递归遍历右子树
public static void preOrderTraverseRecur(TreeNode treeNode){
        if (treeNode != null){
            System.out.println(treeNode.val);
            preOrderTraverseRecur(treeNode.left);
            preOrderTraverseRecur(treeNode.right);
        }
    }

非递归法:

  1. 首先打印根节点,然后把根节点加入到栈中
  2. 一直遍历左子树,并把节点加入到栈中,直到左子树为空时停止
  3. 弹栈,拿到最近访问的节点,转向其右子树,存在时继续遍历左子树,否则继续弹栈转向其右子树
  4. 当栈为空时,遍历结束
public static void preOrderTraverse(TreeNode root){
        if (root == null) return;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode treeNode = root;
        while (treeNode != null || !stack.isEmpty()){
            //一直遍历左节点
            while (treeNode != null){
                System.out.println(treeNode.val);
                stack.push(treeNode);
                treeNode = treeNode.left;
            }
            //左节点为空时,弹栈,转向上一节点的右子树
            if (!stack.isEmpty()){
                TreeNode pop = stack.pop();
                treeNode = pop.right;
            }
        }
    }

3.2.2 中序遍历

递归法:

  1. 递归遍历左子树
  2. 打印根节点
  3. 递归遍历右子树
public static void midOrderTraverseRecur(TreeNode treeNode){
        if (treeNode != null){
            midOrderTraverseRecur(treeNode.left);
            System.out.println(treeNode.val);
            midOrderTraverseRecur(treeNode.right);
        }
    }

非递归法:

  1. 首先把根节点加入到栈中
  2. 一直遍历左子树,并把节点加入到栈中,直到左子树为空时停止
  3. 弹栈,拿到最近访问的节点,打印输出,转向其右子树,存在时继续遍历左子树,否则继续弹栈转向其右子树
  4. 当栈为空时,遍历结束
public static void midOrderTraverse(TreeNode root){
        if (root == null) return;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode treeNode = root;
        while (treeNode != null || !stack.isEmpty()){
            //一直遍历左节点
            while (treeNode != null){
                stack.push(treeNode);
                treeNode = treeNode.left;
            }
            //左节点为空时,弹栈,转向上一节点的右子树
            if (!stack.isEmpty()){
                TreeNode pop = stack.pop();
                System.out.println(pop.val);
                treeNode = pop.right;
            }
        }
    }

3.2.3 后序遍历

递归法:

  1. 递归遍历左子树
  2. 递归遍历右子树
  3. 打印根节点
    public static void afterOrderTraverseRecur(TreeNode treeNode){
        if (treeNode != null){
            afterOrderTraverseRecur(treeNode.left);
            afterOrderTraverseRecur(treeNode.right);
            System.out.println(treeNode.val);
        }
    }

非递归法:

  1. 首先把根节点加入到栈中,由于根节点需要在左右子树均遍历完之后才打印,所以两次的入栈和出栈,记录其为第一次访问状态
  2. 一直遍历左子树,并把节点加入到栈中(状态为第一次访问),直到左子树为空时停止
  3. 弹栈,拿到最近访问的节点,判断其是否为第一次访问,如果是则改变其状态为非第一次访问状态,重新入栈,转向其右子树,存在时继续遍历左子树,然后继续弹栈转向其右子树;如果不是第一次访问状态则左右子树均已遍历完,直接打印输出即可
  4. 当栈为空时,遍历结束
public static void afterOrderTraverse(TreeNode root){
        if (root == null) return;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode treeNode = root;
        while (treeNode != null || !stack.isEmpty()){
            //一直遍历左节点
            while (treeNode != null){
                treeNode.isFirst = true;
                stack.push(treeNode);
                treeNode = treeNode.left;
            }
            //左节点为空时,弹栈,转向上一节点的右子树
            if (!stack.isEmpty()){
                TreeNode pop = stack.pop();
                if (pop.isFirst) {
                    pop.isFirst = false;
                    stack.push(pop);
                    treeNode = pop.right;
                }else {
                    System.out.println(pop.val);
                }
            }
        }
    }

3.3 时间复杂度与空间复杂度

  1. 时间复杂度,递归法和非递归法都之需要对树节点访问一次,所以时间复杂度都为O(n),n为节点数
  2. 空间复杂度,递归法本质是系统去维护了调用栈,栈的最大空间是树的高度,平均情况是O(logn),最坏情况下退化为O(n)

3.4 递归法与非递归法的优缺点

  • 递归法实现简单,代码逻辑清晰;但是当树比较大时,递归层级较深,方法不断压栈可能会导致JVM的虚拟机栈发生溢出 StackOverflowError
  • 非递归法实现稍显复杂,需要额外借助栈来辅助调用;但也因此避免了多层的方法调用

四.如何唯一确定一棵树(树的序列化)

4.1 定义

  • 序列化:把树转化为文件存储的过程叫做序列化
  • 反序列化:把文件内容重建为树的过程叫做反序列化 我们都知道,先序和中序、后序和中序可以唯一确定一棵树,先序和后序不可以(无法确定根节点);但是如果我们在遍历时把空节点也记录下来,先序、中序和后序都可以唯一确定一棵树。

4.2 图解及代码

image.png

public static void leftFirstTraverse(TreeNode root, List list){
        if (root != null){
            list.add(String.valueOf(root.val));
            leftFirstTraverse(root.left, list);
            leftFirstTraverse(root.right, list);
        }else {
            list.add("#");
        }
    }