图解二叉树的遍历

引言

本文介绍了二叉树的先/中/后序遍历的递归实现和非递归实现、深度优先搜索以及层次遍历的实现。

先序遍历

所谓先序遍历,就是先访问根节点,再访问左右子节点。在这些遍历策略中,左子节点的访问都在右节点访问之前。

在这里插入图片描述
我们以这样一颗二叉树为例,其实它是更为严格的二叉搜索树。

对该树进行先序遍历,输出:3 1 0 8 5 16 11 10 13 19

其访问顺序如下虚线箭头所示:

在这里插入图片描述

递归版本

/**
 * 先序遍历(先根遍历):先访问根节点,再访问左右子节点
 */
public void preOrder() {
    preOrder(root);
    System.out.println();
}

private void preOrder(Node<E> node) {
    if (node != null) {
        System.out.print(node.data + " ");
        preOrder(node.left);
        preOrder(node.right);
    }
}

递归版本的实现非常简单,注意访问当前节点代码System.out.print(node.data + " ");的位置,是在访问左右孩子节点之前。这就是先序遍历的递归版本实现。

但是由于递归版本用到了栈,如果树中节点过多,会导致栈溢出异常。因此是非常有必要研究非递归版本的实现的。

非递归版本

在这里插入图片描述
观察上图,可以发现每次访问一个新节点后,都尝试访问左孩子,然后左孩子的左孩子。直到左孩子为空,或者左孩子已经被访问过。

可以总结一下,对于任何子树,访问根节点后,都会去访问它的左子树,从局部来看,就像沿着左侧节点不停的下行。直到左子树为空后,转而去访问它的右子树,对于该右子树,同样沿着左侧节点下行。我们称这些左侧节点为左侧链。

在这里插入图片描述

我们的算法根据左侧链展开。

我们可以将每一条左侧链突出的绘制出来。

在这里插入图片描述

L d L_d 是左侧链上最深的节点。最大深度为d。 T 0 T_0 表示以 L 0 L_0 右孩子节点 R 0 R_0 为根的右子树。

整个二叉树的先序遍历访问顺序如上图所示。当达到最深的节点后,继而访问它的右孩子(也可能不存在),进而依次访问它们的右子树(对于右子树也是沿着左侧链访问)。 L 0 L_0 右孩子节点 R 0 R_0 是最后访问的,这符合栈结构的特性。

我们根据这个过程来实现代码:

/**
 * 非递归实现先序遍历
 */
public void preOrderNonRecursion() {
    preOrderNonRecursion(root);
    System.out.println();
}

private void preOrderNonRecursion(Node<E> node) {
    //栈中缓存的是右子树根节点(可能为空)
    Stack<Node<E>> stack = new Stack<>();
    while (true) {
        visitLeft(node,stack);
        //访问了之后,判断栈是否为空
        if (stack.isEmpty()) {
            //访问完毕,跳出
            break;
        }
        //弹出下一(右)子树的根节点(可能为空),对于右子树继续沿着左侧链下行
        node = stack.pop();
    }
}

private void visitLeft(Node<E> node,Stack<Node<E>> stack) {
    while (node != null) {
        //先访问根节点
        System.out.print(node.data + " ");
        //将右孩子入栈(可能为空)
        stack.push(node.right);
        //继续沿左侧下行
        node = node.left;
    }
}

中序遍历

就是先访左子节点,其次是根节点,最后是右子节点

在这里插入图片描述

对该树进行中序遍历,输出:0 1 3 5 8 10 11 13 16 19。(对BST进行中序遍历可有序输出)

其访问顺序如下虚线箭头所示:

在这里插入图片描述

递归版本

/**
 * 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
*/
public void inOrder() {
   inOrder(root);
   System.out.println();
}

private void inOrder(Node<E> node) {
   if (node != null) {
       inOrder(node.left);
       System.out.print(node.data + " ");
       inOrder(node.right);
   }
}

非递归版本

在这里插入图片描述
开始从整颗树的根节点3介入,依照先访问左子树的原则,我们此时不访问3,而是继续向下到1,同样它有左孩子,继续向下到0,此时0没有左孩子(其他位置可以看成左孩子已经访问过),接下来就可以访问0了。然后是0的右孩子,这里为空,因此以0为根的子树已经访问完毕。可以看成0被删除掉了。接下来访问以1为根的子树的根节点,然后是右孩子(很巧,还是没有)。按照这个策略,继续下去。

整体来看,访问顺序为:从根节点开始,一直沿着左侧分支,逐层向下,直到不能再向下,再访问(最左侧节点的)根节点,然后是其右子树**。对右子树也按照同样的顺序访问。

同样地,我们可以抽象成这样一个访问顺序。

在这里插入图片描述

我们先看左边红框中的结构。在访问 L k 1 L_{k-1} 时,会先访问它的左孩子 L k L_k ,因为 L k L_k 没有左孩子,所以可以访问它,然后是它的右孩子 R k R_k ,在访问完 L k L_k 的右孩子后,即 L k 1 L_{k-1} 的左子树访问完毕,此时才可以访问 L k 1 L_{k-1}

再看上图右边,也是和先序遍历类似的左侧链结构,不同的是按照上面所说的访问顺序进行访问的。第一个访问的是最左侧节点 L d L_d ,其次是它的右子树,然后是它的根节点,根节点的右子树…以此类推。

/**
 * 非递归实现中序遍历
 */
public void inOrderNonRecursion() {
    inOrderNonRecursion(root);
    System.out.println();
}

private void inOrderNonRecursion(Node<E> node) {
    Stack<Node<E>> stack = new Stack<>();
    while (true) {
    	//沿着左侧链下行
        goLeft(node,stack);
        if (stack.isEmpty()) {
            break;
        }
        //弹出最左侧元素,其左子树为空(上图的Ld)或已经访问过
        node = stack.pop();
        //访问它
        System.out.print(node.data + " ");
        //接下来访问右子树(可能为空),相当于 goLeft(node.right,stack);
        node = node.right;
    }
}

private void goLeft(Node<E> node,Stack<Node<E>> stack) {
    while (node != null) {
        //沿左侧分支,反复地入栈
        stack.push(node);
        node = node.left;
    }

}

后序遍历

就是先访左子节点,其次是右子节点,最后才是根节点。

递归版本

/**
* 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
*/
public void postOrder() {
   postOrder(root);
   System.out.println();
}

private void postOrder(Node<E> node) {
   if (node != null) {
       postOrder(node.left);
       postOrder(node.right);
       System.out.print(node.data + " ");
   }
}

层次遍历

层次遍历挺简单的,主要利用了队列的先进先出。一层一层的遍历即可,类似图的广度优先遍历。

/**
 * 层次遍历
 */
public void levelOrder() {
    levelOrder(root);
    System.out.println();
}

/**
 * 和图的广度优先遍历类似,也需要用到队列
 * 一层一层的遍历
 *
 * @param node
 */
private void levelOrder(Node<E> node) {
    if (node != null) {
        Queue<Node<E>> queue = new Queue<>();
        queue.enqueue(node);
        while (!queue.isEmpty()) {
            Node<E> p = queue.dequeue();
            System.out.print(p.data + " ");
            if (p.left != null) {
                queue.enqueue(p.left);
            }
            if (p.right != null) {
                queue.enqueue(p.right);
            }
        }
    }
}

深度优先搜索

/**
 * 深度优先遍历,相当于先序遍历
 */
public void depthOrder() {
    depthOrder(root);
    System.out.println();
}

/**
 * 和图的深度优先搜索类似
 *
 * @param node
 */
private void depthOrder(Node<E> node) {
    if (node != null) {
        Stack<Node<E>> stack = new Stack<>();
        stack.push(node);
        while (!stack.isEmpty()) {
            Node<E> p = stack.pop();
            System.out.print(p.data + " ");
            //先push right,再push left 达到pop时先访问left的效果
            if (p.right != null) {
                stack.push(p.right);
            }
            if (p.left != null) {
                stack.push(p.left);
            }
        }
    }
}
发布了131 篇原创文章 · 获赞 38 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/91573817