1. 中序遍历
对于中序遍历的顺序,我们先从最简单的情况观察:
对于这样一个仅含三个节点的二叉树而言,它的中序遍历的访问顺序是左—>根—>右,也就是2—>1—>3
简单概括中序遍历的规则就是:对于非空树x,先递归访问其左子树—>访问当前节点—>最后递归访问右子树
2. 递归
由于树本身就是由递归定义的,很自然地我们使用递归算法描述这样一个访问流程
class A{
//二叉树结构
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
//中序遍历
private void inorderTraversal(TreeNode root, List<Integer> res){
if(root == null) return;
else{
inorderTraversal(root.left, res);
visit(root.val); //访问当前节点
inorderTraversal(root.right, res);
}
}
}
其实中序遍历的递归算法非常简单直观,递归访问左子树、访问当前节点、递归访问右子树(同样地,我们也可以写出递归算法的前序遍历和后序遍历,只需要调整访问当前节点的顺序即可)
可以看到,递归的算法这么简单,那么思考一下:我们能不能用迭代的方式来实现呢?
3. 迭代
考虑中序遍历的迭代实现,首先我们需要考虑要维护的中间变量是什么?
由于迭代的过程是没有收缩扩张的,我们仅仅靠维护中间变量来完成整个计算(遍历)过程,那么我们就需要进一步确定使用哪些中间变量确保过程的顺利执行
我们利用辅助栈来保存树节点【Tree Node】
根据中序遍历的访问顺序,对于一棵树而言
- 从根节点出发,我们需要走到树的最左节点(沿途保存所有经过的节点)
- 访问过最左节点L2后,我们还需要访问右子树T2(将L2看成一个子树根节点,T2子树就是它的右子树,因此按照遍历规则需要T2子树进行中序遍历)
- 对于T2子树,我们进行同样的操作
- 然后访问L1节点…
- …
- 反复如此执行,直到所有节点都被遍历一遍
因为栈后进先出的特性,首先我们从根结点出发,不断向左走的过程中,从根开始将途径节点push入栈,这也就意味这后续访问节点的顺序一定是左子节点在根节点之前的
将每次pop出栈的节点T当成是一棵子树的根节点,接下来要去访问的就是T的右子树(如果存在的话),访问完毕后再进入下一轮迭代
具体的代码如下:
class B{
private Stack<TreeNode> stack;
//中序遍历-迭代
public void inorderTraversal(TreeNode root) {
stack = new Stack<TreeNode>();
goToLeft(root);//从根节点出发
while(!stack.empty()){
TreeNode t = stack.pop();
visit(t.val);
if(t.right != null) goToLeft(t.right);//如果右子树存在
}
}
//一直走到最左端,沿途节点入栈
private void goToLeft(TreeNode root){
while(root != null){
stack.push(root);
root = root.left;
}
}
}
观察上述算法,我们需要思考的是为什么要引入栈?
不用栈可不可以?引入队列行不行?
因为中序遍历的顺序规则,在有左子树存在的情况下,我们最早访问的一定不是根节点,而且我们又没有一个对父节点的引用,所以当我们不断下左的过程中需要用栈这种特殊的数据结构来保存沿途的节点,以便延后处理
另外一个需要注意的点就是,对于每一个从栈顶pop出的节点T,当我们把它看成一棵独立的子树,T作为子树根节点时,我们还需要去中序遍历T的右子树