二叉树遍历(先序、中序、后序)

1,233 阅读5分钟

二叉树遍历(先序、中序、后序)

二叉树的先序、中序、后序的递归和非递归遍历。 三种遍历方法的考查顺序一致,得到的结果却不一样,原因在于:

先序:考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。(根左右)

中序:考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树。(左根右)

后序:考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值。(左右根)

二叉树节点类

function TreeNode(val) {
    this.val = val;
    //左子树
    this.left=null;
    //右子树
    this.right=null;
}

先序遍历

递归先序遍历

递归先序遍历很容易理解,先输出节点的值,再递归遍历左右子树。中序和后序的递归类似,改变根节点输出位置即可。

// 递归先序遍历
function recursionPreorderTraversal(root) {
    if (root != null) {
        System.out.print(root.val + " ");
        recursionPreorderTraversal(root.left);
        recursionPreorderTraversal(root.right);
    }
}

非递归先序遍历

因为要在遍历完节点的左子树后接着遍历节点的右子树,为了能找到该节点,需要使用栈来进行暂存。中序和后序也都涉及到回溯,所以都需要用到栈。

// 非递归先序遍历
function preorderTraversal(root) {
    // 用来暂存节点的栈
    let treeNodeStack = [];
    // 新建一个游标节点为根节点
    let node = root;
    // 当遍历到最后一个节点的时候,无论它的左右子树都为空,并且栈也为空
    // 所以,只要不同时满足这两点,都需要进入循环
    while (node != null || treeNodeStack.length > 0) {
        // 若当前考查节点非空,则输出该节点的值
        // 由考查顺序得知,需要一直往左走
        while (node != null) {
            System.out.print(node.val + " ");
            // 为了之后能找到该节点的右子树,暂存该节点
            treeNodeStack.push(node);
            node = node.left;
        }
        // 一直到左子树为空,则开始考虑右子树
        // 如果栈已空,就不需要再考虑
        // 弹出栈顶元素,将游标等于该节点的右子树
        node = treeNodeStack.pop();
        node = node.right;
    }
}

先序遍历结果:

递归先序遍历: 1 2 4 6 7 8 3 5
非递归先序遍历:1 2 4 6 7 8 3 5

中序遍历

递归中序遍历

// 递归中序遍历
function recursionMiddleorderTraversal(root) {
    if (root != null) {
        recursionPreorderTraversal(root.left);
         System.out.print(root.val + " ");
        recursionPreorderTraversal(root.right);
    }
}

非递归中序遍历

和非递归先序遍历类似,唯一区别是考查到当前节点时,并不直接输出该节点。

而是当考查节点为空时,从栈中弹出的时候再进行输出(永远先考虑左子树,直到左子树为空才访问根节点)。

// 非递归中序遍历
function middleorderTraversal(root) {
	let treeNodeStack = []; //定义存储节点的栈
    let node = root; // 定义游标节点
    //当游标节点不为null
    while(node != null || treeNodeStack.length > 0) {
    	while(node) {
        	treeNodeStack.push(node);
        	node = node.left;
        }
        node = treeNodeStack.pop();
        System.out.print(node.val + " ");
        node = node.right;
    }
    
}

中序遍历结果

递归中序遍历: 4 7 6 8 2 1 3 5
非递归中序遍历:4 7 6 8 2 1 3 5

后序遍历

递归后序遍历

// 递归后序遍历
function recursionMiddleorderTraversal(root) {
    if (root != null) {
        recursionPreorderTraversal(root.left);
        recursionPreorderTraversal(root.right);
        System.out.print(root.val + " ");
    }
}

非递归后序遍历

后续遍历和先序、中序遍历不太一样。

后序遍历在决定是否可以输出当前节点的值的时候,需要考虑其左右子树是否都已经遍历完成。

所以需要设置一个lastVisit游标。

若lastVisit等于当前考查节点的右子树,表示该节点的左右子树都已经遍历完成,则可以输出当前节点。

并把lastVisit节点设置成当前节点,将当前游标节点node设置为空,下一轮就可以访问栈顶元素。

否则,需要接着考虑右子树,node = node.right。

以下考虑后序遍历中的三种情况:

  1. 右子树不为空接着检查右子树 node.right !=null, 如下图所示,从节点1开始考查直到节点4的左子树为空。

注:此时的游标节点node = 4.left == null。

此时需要从栈中查看 Peek()栈顶元素。

发现节点4的右子树非空,需要接着考查右子树,4不能输出,node = node.right。

  1. 当前节点左右子树都为空 node.right==null 如下图所示,考查到节点7(7.left == null,7是从栈中弹出),其左右子树都为空,可以直接输出7。

此时需要把lastVisit设置成节点7,并把游标节点node设置成null,下一轮循环的时候会考查栈中的父节点6。

  1. 栈顶元素左右节点遍历完成,输出栈顶元素。 lastVisitNode==node.right 如下图所示,考查完节点8之后(lastVisit == 节点8),将游标节点node赋值为栈顶元素6,节点6的右子树正好等于节点8。表示节点6的左右子树都已经遍历完成,直接输出6。
// 非递归中序遍历
function middleorderTraversal(root) {
	let treeNodeStack = []; //定义存储节点的栈
    let node = root; // 定义游标节点
    let lastVisitNode = root; // 最后一个被访问的节点
    //当游标节点不为null
    while(node != null || treeNodeStack.length > 0) {
    	while(node) {
        	treeNodeStack.push(node);
            lastVisitNode = node;
        	node = node.left;
        }
        node = treeNodeStack.peek(); // 将node置为栈顶元素
         //如果其右子树也为空,或者右子树已经访问
        //则可以直接输出当前节点的值
        if(node.right == null || lastVisitNode == node.right) {
        	System.out.print(node.val + " ");
      		treeNodeStack.pop();
          	lastVisit = node;
           	node = null;//目的是让其跳过上一个while循环,让下一轮的node直接指向当前这轮节点的父节点。
        } else {
            //否则,继续遍历右子树
            node = node.right;
        }
    }
    
}

后序遍历结果

递归后序遍历: 7 8 6 4 2 5 3 1
非递归后序遍历:7 8 6 4 2 5 3 1