二叉树的遍历

268 阅读9分钟

二叉树遍历概述

遍历是数据结构中的常见操作。
二叉树的遍历就是把所有元素都访问一遍。

根据节点访问顺序的不同,二叉树的常见遍历方式有4种:

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Level Order Traversal)

先序遍历

先序遍历的思路:
1.访问根节点;
2.访问当前节点的左子树;
3.若当前节点无左子树,则访问当前节点的右子树。

对于左子树和右子树,同样采用先序遍历。

二叉树先序遍历示意图: image.png

上图二叉树先序遍历的过程:
1)访问该二叉树的根节点,找到 A;
2)访问节点 A 的左子树,找到节点 B;
3)访问节点 B 的左子树,找到节点 D;
4)由于访问节点 D 左子树失败,且也没有右子树,因此以节点 D 为根节点的子树遍历完成。但节点 B 还没有遍历其右子树,因此现在开始遍历,即访问节点 E;
5)由于节点 E 无左右子树,因此节点 E 遍历完成,并且由此以节点 B 为根节点的子树也遍历完成。现在回到节点 A ,并开始遍历该节点的右子树,即访问节点 C;
6)访问节点 C 左子树,找到节点 F;
7)由于节点 F 无左右子树,因此节点 F 遍历完成,回到节点 C 并遍历其右子树,找到节点 G;
8)节点 G 无左右子树,因此以节点 C 为根节点的子树遍历完成,同时回归节点 A。整个二叉树遍历完成。

先序遍历结果:

A、B、D、E、C、F、G

先序遍历递归实现

注意:对于节点类,后面三种遍历方式使用相同节点类。
节点类:

/**
 * 节点类
 * @param <E> 泛型
 */
private static class Node<E> {
    E element; //节点数据
    Node<E> left; //左节点
    Node<E> right; //右节点
    Node<E> parent; //父节点 - 额外增加

    public Node(E element, Node<E> parent) {
        this.element = element;
        this.parent = parent;
    }
}

先序遍历递归代码:

/**
 * 先序遍历对外 api
 */
public void preOrder() {
    preOrderPrint(root);
}

/**
 * 先序遍历具体实现 - 递归
 */
private void preOrderPrint(Node<E> node) {
    if (node == null) return;
    // 访问子树节点数据
    System.out.println(node.element);
    // 递归遍历左子树
    preOrderPrint(node.left);
    // 递归遍历右子树
    preOrderPrint(node.right);
}

先序遍历非递归实现(栈结构)

先序遍历的非递归借助栈结构完成:
①将根节点入栈;
②判栈空,获取栈顶元素输出;
③判断右子树是否为空,不为空则右子树入栈;再判断左子树是否为空,不为空则左子树入栈在,回至②执行。

③中必须是先判断右子树,再判断左子树,因为栈的规则是先进后出,即:右子树先入栈,左子树后入栈,访问节点数据(出栈)时,则会先左子树再右子树。

image.png

对于上图二叉树,遍历过程大致如下:
第一步:根节点(A)入栈,此时栈中只有节点 A。

image.png

第二步:栈不为空,访问栈顶元素(A),出栈(节点A出栈),然后先后判断 A 的右、左子树,并入栈,此时栈中有节点B(栈顶)和节点C(栈底)。

image.png

第三步:重复执行(判断栈是否为空,不为空出栈),访问栈顶元素(B),出栈(节点B出栈),然后将 B 的右子树和左子树先后入栈,此时栈中有节点D、E、C。

image.png

第四步,重复执行....

最后遍历的结果为:A、B、D、E、C、F、G

先序遍历非递归代码:

/**
 * 先序遍历具体实现 - 非递归
 * 1. 先将根节点入栈,
 * 2. 当栈不为空时循环执行以下操作:
 *     2.1 访问栈顶节点
 *     2.2 栈顶节点的右节点入栈
 *     2.3 栈顶节点的左节点入栈
 */
private void preOrderPrint2() {
    if (root == null) return;
    Stack<Node<E>> stack = new Stack<>();

    // 1.根节点入栈
    stack.push(root);

    // 栈不为空时重复执行以下操作
    while (!stack.isEmpty()) {

        // 2.访问栈顶节点(出栈)
        Node<E> topNode = stack.pop();
        System.out.println(topNode.element);

        // 3.栈顶节点右节点入栈(右节点不为空时)
        if (topNode.right != null) {
            stack.push(topNode.right);
        }

        // 4.栈顶节点左节点入栈(左节点不为空时)
        if (topNode.left != null) {
            stack.push(topNode.left);
        }
    }
}


中序遍历

中序遍历的思路:
1.访问当前节点的左子树;
2.访问根节点;
3.访问当前节点的右子树。

中序遍历左子树、根节点、中序遍历右子树。

二叉树中序遍历示意图: image.png

上图采用中序遍历的过程为:
1)访问该二叉树的根节点,找到 A;
2)遍历节点 A 的左子树,找到节点 B;
3)遍历节点 B 的左子树,找到节点 D;
4)由于节点 D 无左孩子,因此找到节点 D,并遍历节点 D 的右子树;
5)由于节点 D 无右子树,因此节点 B 的左子树遍历完成,访问节点 B;
6)遍历节点 B 的右子树,找到节点 E;
7)由于节点 E 无左子树,因此访问节点 E ,又因为节点 E 没有右子树,因此节点 A 的左子树遍历完成,访问节点 A ,并遍历节点 A 的右子树,找到节点 C;
8)遍历节点 C 的左子树,找到节点 F;
9)由于节点 F 无左子树,因此访问节点 F,又因为该节点无右子树,因此节点 C 的左子树遍历完成,开始访问节点 C ,并遍历节点 C 的右子树,找到节点 G;
10)由于节点 G 无左子树,因此访问节点 G,又因为该节点无右子树,因此节点 A 的右子树遍历完成,即整棵树遍历完成;

因此,采用中序遍历的结果为:

D、B、E、A、F、C、G

中序遍历递归实现

中序遍历代码:

public void inOrder() {
    inOrderPrint(root);
}

/**
 * 中序遍历具体实现
 */
public void inOrderPrint(Node<E> node) {
    if (node == null) return;
    // 递归遍历左子树
    inOrderPrint(node.left);
    // 访问子树节点数据
    System.out.println(node.element);
    // 递归遍历右子树
    inOrderPrint(node.right);
}


中序遍历非递归实现(栈结构)

/**
 * 中序遍历具体实现 - 非递归,利用栈实现
 * 1.先将根节点入栈,设置 node = root;
 * 2.将当前节点的所有左孩子入栈,直到左孩子为空
 * 3.访问栈顶元素,如果栈顶元素存在右孩子,则继续第2步(栈顶元素右子树入栈)
 * 4.重复第2、3步,直到栈为空并且所有的节点都被访问(循环结束)
 * 
 * 上面四步分解为代码理解如下:
 *     1.设置 node = root;
 *     2.节点不为null 或者 栈不为空,则循环执行以下操作:
 *         如果 node != null,将 node 入栈,设置 node = node.left;
 *         如果 node == null,弹栈,访问栈顶元素,并设置 node = node.right;
 */
public void inorderPrint2() {
    if (root == null) return;
    Stack<Node<E>> stack = new Stack<>();

    // 设置 node = root,在 while 循环一开始就会将根节点入栈
    Node<E> node = root;
    while (node != null || !stack.isEmpty()) {

        // 将当前节点所有左子树入栈(最开始会把根节点入栈(node = root))
        while (node != null) {
            stack.push(node);
            node = node.left;
        }

        // 弹栈访问栈顶元素
        Node<E> headNode = stack.pop();
        System.out.println(headNode.element);

        // 将栈顶元素的右节点赋值给 node,执行栈顶元素右孩子入栈
        node = headNode.right;
    }
}


后序遍历

中序遍历的思路:
从根节点出发,依次遍历各节点的左右子树,直到当前节点左右子树遍历完成后,才访问该节点元素。

后序遍历左子树、后序遍历右子树、根节点

二叉树后序遍历示意图: image.png

对此二叉树进行后序遍历的操作过程为:
1)从根节点 A 开始,遍历该节点的左子树(以节点 B 为根节点);
2)遍历节点 B 的左子树(以节点 D 为根节点);
3)由于节点 D 既没有左子树,也没有右子树,此时访问该节点中的元素 D,并回退到节点 B ,遍历节点 B 的右子树(以 E 为根节点);
4)由于节点 E 无左右子树,因此可以访问节点 E ,并且此时节点 B 的左右子树也遍历完成,因此也可以访问节点 B;
5)此时回退到节点 A ,开始遍历节点 A 的右子树(以节点 C 为根节点);
6)遍历节点 C 的左子树(以节点 F 为根节点);
7)由于节点 F 无左右子树,因此访问节点 F,并回退到节点 C,开始遍历节点 C 的右子树(以节点 G 为根节点);
8)由于节点 G 无左右子树,因此访问节点 G,并且节点 C 的左右子树也遍历完成,可以访问节点 C;节点 A 的左右子树也遍历完成,可以访问节点 A;
9)到此,整棵树的遍历结束。

因此,采用后序遍历的结果:

D、E、B、F、G、C、A

后序遍历递归实现

后序遍历代码:

public void postorder(){
    postorderPrint(root);
}

/**
 * 后序遍历具体实现 - 递归
 */
public void postorderPrint(Node<E> node) {
    if (node == null) return;
    postorderPrint(root.left);
    postorderPrint(root.right);
    System.out.println(node.element);
}


层序遍历

按照二叉树中的层次从左到右依次遍历每层中的结点。

从上到下、从左到右依次访问每一个节点

二叉树层序遍历示意图:

image.png

实现思路:使用队列
通过使用队列的数据结构,从树的根结点开始,依次将其左孩子和右孩子入队。而后每次队列中一个结点出队,都将其左孩子和右孩子入队,直到树中所有结点都出队,出队结点的先后顺序就是层次遍历的最终结果。

  1. 将根节点 A 入队

  2. 循环执行以下操作,直到队列为空:
    将队头节点 X 出队,进行访问;
    将 X 的左子节点入队;
    将 X 的右子节点入队;

以上图为示:
首先,根结点 A 入队;
根结点 A 出队,出队的同时,将左孩子 B 和右孩子 C 分别入队;
队头结点 B 出队,出队的同时,将结点 B 的左孩子 D 和右孩子 E 依次入队;
队头结点 C 出队,出队的同时,将结点 C 的左孩子 F 和右孩子 G 依次入队;
不断地循环,直至队列内为空。

层序遍历代码:

/**
 * 层序遍历
 */
public void levelOrder() {
    if (root == null) return;;
    Queue<Node<E>> queue = new LinkedList<>();

    // 根节点入队
    queue.offer(root);
    while (!queue.isEmpty()) {
        // 队列头结点出队
        Node<E> headNode =  queue.poll();
        // 访问出队的头结点数据
        System.out.println(headNode.element);

        // 将队头结点的左右孩子依次入队
        if (headNode.left != null) {
            queue.offer(headNode.left);
        }
        if (headNode.right != null) {
            queue.offer(headNode.right);
        }
    }
}