本文主要对二叉树的各种遍历方式进行详细的描述,包括深度优先遍历(前序遍历|中序遍历|后序遍历)和广度优先遍历(也叫层序遍历),同时从递归和非递归的两种角度进行实现,采用Java语言编写。
首先给出基础的树节点定义,包括一个获取二叉树总节点个数的方法,后面在后序遍历的非递归实现中会用到。
1 |
|
本文以下图所示的二叉树为例进行描述。
该二叉树的各种遍历方式结果如下:
- 前序遍历:1 2 4 7 3 5 6 8
- 中序遍历:4 7 2 1 5 3 8 6
- 后序遍历:7 4 2 5 8 6 3 1
- 层序遍历:1 2 3 4 5 6 7 8
深度优先遍历
深度优先遍历是指沿着树的深度遍历树的节点,尽可能深的搜索树的分支。对于树的深度优先遍历而言,可进一步按照根节点相对于左右子节点的访问先后来划分。如果把左节点和右节点的位置固定不动,那么根节点放在左节点的左边,称为前序(pre-order)、根节点放在左节点和右节点的中间,称为中序(in-order)、根节点放在右节点的右边,称为后序(post-order)。
因为树结构本身就是递归思想的一种生动的实例,所以递归实现的遍历代码非常简洁清晰。而非递归的实现方式,主要也是用栈结构模拟递归调用实现。
前序遍历
即按照 根节点->左节点->右节点 的顺序进行遍历。
首先给出递归实现:
1 |
|
遍历的递归实现比较简单,不过这个代码可以很好的说明递归实现的思想。递归实现的两个关键条件就是:最小单元和终止条件。
下图就是可以描述树这种数据结构特点的一个最小单元,当我们要遍历整个树的时候,先考虑正确遍历这个最小单元,前序遍历需要按照 “根->左->右” 的顺序进行遍历,所以先输出根节点的值,然后递归处理左节点,右节点,即有了上面递归函数的处理顺序。
而终止条件就是判断递归处理何时应该结束,对于当前遍历实现就是当处理到null节点的时候。
当我们希望利用递归的方式处理更复杂的编程情况时,二叉树的递归遍历是个很好的基础参考。
下面给出前序遍历的非递归实现,共有两种方式。
非递归实现1如下:
1 |
|
对于非递归实现1而言:
- 初始化一个栈,首先将根节点压入栈中。
- 之后进入循环,在循环中,将栈顶的元素弹出,并进行输出,之后因为栈后入先出的属性,因此先压入右节点,再压入左节点。
- 下一次循环就会将左节点先弹出,进行同样的处理。
- 最后按照 根->左->右 的方式遍历完全部节点即完成。
因为前序遍历的特殊性,首先输出根节点,因此处理完后可以不必再回溯根节点,所以可以按照这种方式进行处理。
非递归实现1和后面的层级遍历的实现具有高度的饿对称性,这个放在层级遍历进行分析。
另一种非递归实现2如下:
1 |
|
对于非递归实现2而言:
- 初始化一个栈
- 临时节点node起到游标的作用,从根节点root出发。
- 之后进入双层循环,对于第一次循环而言,内循环从根节点开始,以上文中的二叉树为例,将1->2->4这一支最左侧子链 依次输出 并压入栈,这一支左子链完结之后从内循环跳出;接下来出栈节点,获取该节点的右子树,进入内循环,按照相同的方式处理。一直到所有的节点处理完毕。
- 比较贴切的利用栈模仿了前序遍历的递归实现。
中序遍历
即按照 左节点->根节点->右节点 的顺序进行遍历。
递归实现如下:
1 |
|
非递归实现如下:
1 |
|
对于非递归实现而言:
- 中序遍历的非递归实现和前序遍历的非递归实现2基本相同,只是输出节点的位置不同。
- 进入双层循环后,对于第一次循环而言,内循环从根节点开始,以上文中的二叉树为例,将1->2->4这一支最左侧子链依次压入栈,这一支左子链完结之后从内循环跳出;接下来出栈节点,在这里才输出节点,即先输出左子节点,获取该节点的右子树,之后进入内循环,按照相同的方式处理。一直到所有的节点处理完毕。
后序遍历
递归实现如下:
1 |
|
非递归实现如下:
1 |
|
后序遍历的非递归处理比较复杂一些。主要原因在于后序遍历的顺序是:左->右->根,那就意味着输出左节点之后,需要回溯到根节点,借由根节点先去输出右节点,最后再回溯输出根节点。相当于回溯了两次根节点,那么关键就是要区别出这两次回溯。
这里采用了一个记录回溯状态的数组flag,当某个节点存在右子树并且已经处理过右子树之后,就置为1。等到处理完右节点之后的第二次回溯就可以输出该节点了,不用再进行右节点的处理。
分步骤看后序遍历的非递归实现:
- 首先初始化一个栈,之后利用一个循环将根节点的最左侧子链依次压入栈中。
- 然后进入一个三层循环,在第一层循环中,peek一下栈顶元素,若该元素拥有右子树并且还没被处理(flag标志为0)。有右子树说明该栈顶元素就是自己这一层级的根节点,所以进入第二层循环先处理该栈顶元素的右节点。
- 在第二层循环中,传入的右节点相对于自己的层级就是根节点,还是要先把左子树压入,之后peek一下栈顶元素,若这个栈顶元素还有右子树且还没被处理,其实又再次进入二层循环的处理。
- 直到碰到没有右子树或者右子树已处理的情况,就输出节点。
- 按照这种方式输出 正是 左->右->根 的顺序输出。
广度优先遍历(层级遍历)
广度有限遍历即层次遍历是从根节点开始,沿着树的宽度遍历树的节点。从根节点到叶子节点,一层一层按照从左到右的顺序依次遍历节点。
广度优先遍历的实现方式如下:
1 |
|
对于广度优先遍历的实现而言:
- 首先初始化一个队列,将根节点入队
- 进入循环,第一次循环时,将队首的根节点出队并输出(第一层就输出完毕了),由于队列是先进先出的属性,所以先将左子节点入队,再将右子节点入队。在下一次循环中,第二层就会按照从左到右的顺序依次出队,并且第三层的元素按照从左到右的顺序依次进入队列…最后按照层级输出了全部节点。
回过头来再对比下层级遍历的实现和前序遍历的非递归实现1,会发现两处代码非常对称,只是在暂存节点选择的结构上,层级遍历用了队列,前序遍历的非递归实现1用了栈,以及压入左右节点的顺序正好相反。
综上,本文详细对比介绍了树的几种遍历方式,同时对比了递归和非递归的实现。从中可以发现,只要理清楚了树的最小单元的处理,把握好终止条件,推而广之,不管是采用递归还是循环,都能够优雅的遍历树的所有节点。
参考:
http://biaobiaoqi.github.io/blog/2013/04/27/travsal-binary-tree/