二叉树常见的遍历方式有先序遍历、中序遍历、后序遍历、层次遍历。
首先介绍前三种遍历方式,对于每一个子树,遍历的顺序是
先序遍历:根 ->左子树->右子树 ,如图中①
中序遍历:左子树->根 ->右子树 ,如图中②
后序遍历:左子树->右子树->根 ,如图中③
先序遍历:
按照上面的规则,递归版本不难给出
public void preOrder(Node root) {
if(root == null) return;
System.out.printf("%d", root.value);
preOrder(root.lChild);
preOrder(root.rChild);
}
计算机执行递归算法时,是通过栈来实现的,以下来自数据结构课本
当你调用一个函数时,系统会将这个函数进行入栈操作,在入栈之前,通常需要完成三件事。
1、将所有的实参、返回地址等信息传递给被调函数保存。
2、为被调函数的局部变量分配存储区。
3、将控制转移到被调函数入口。
当一个函数完成之后会进行出栈操作,出栈之前同样要完成三件事。
1、保存被调函数的计算结果。
2、释放被调函数的数据区。
3、依照被调函数保存的返回地址将控制转移到调用函数。
上述操作必须通过栈来实现,即将整个程序的运行空间安排在一个栈中。每当运行一个函数时,就在栈顶分配空间,函数退出后,释放这块空间。所以当前运行的函数一定在栈顶。
但是利用系统的栈会带来很大的额外开销,保存了太多不必要的数据,我们可以对栈中保存的内容进行剪裁,只保留我们需要的东西,用一个自己创建的栈实现迭代版本的遍历。
首先要明确的是把null节点视为叶节点,即最深层的节点也是自身和两个null孩子形成的二叉树的根节点。
递归的代码里,可以视为把lChild和rChild摆在这一层,在对lChild进行判定来进入下一层,遇到递归基即孩子不存在就返回上一层,对右子树的遍历只能在左子树遍历完成后开始。所以对每个子树,检查一下root节点并打印,有孩子就按照先右后左入栈(出栈的顺序就成了先左后右),没有就过,如此循环,遍历左子树途中更深层的node会在rChild出栈之前入栈,对右子树的遍历只能在左子树遍历完成后开始,就实现了迭代版的先序遍历,代码如下
public void preOrder(Node root) {
if(root == null) return;
Node cur = root;
Stack stack = new Stack();
stack.push(cur);
while(!stack.isEmpty()) {
cur = stack.pop();
System.out.print(cur.value);
if(cur.rChild != null) stack.push(cur.rChild);
if(cur.lChild != null) stack.push(cur.lChild);
}
}
中序遍历:
递归版本如下
public void inOrder(Node root) {
if(root == null) return;
inOrder(root.lChild);
System.out.printf("%d"+" ", root.value);
inOrder(root.rChild);
}
现在改为迭代版本。中序遍历的特点是,每个节点都是作为一个子树的root节点,在遍历完左子树之后被访问。所以核心的点在于每个node都是作为root从下一层回来的时候被访问的,而第一次遇到它是不访问的。这个算法的动作就是向左探至null、访问父节点、从父节点的rChild起重复循环。整个遍历过程实际上被划分为一个一个的“向左探至null”的过程。利用栈结构先进后出的特点,把向左探的通路上的node压栈,对于每个子树的root,会在左子树完成遍历后被访问,实现了迭代版中序遍历。
值得注意的是,而每次push入值的顺序是二叉树的前序遍历(根左右)。
代码如下
public void inOrder(Node root) {
if(root == null) return;
Node cur = root;
Stack stack = new Stack();
while(!stack.isEmpty() || cur != null) {
if(cur != null) {
stack.push(cur);
cur = cur.lChild;
}else {
cur = stack.pop();
System.out.print(cur.value + " ");
cur = cur.rChild;
}
}
}
中序遍历是非常常见的遍历方式,如果我们可以不遍历整个二叉树就可以直接给出某个node在中序遍历中的后继就好了。想一下,对于每一个node,如果有右子树,那么后继就是从rChild开始左探的最后一个非null节点;如果没有右子树,就是把它包含在左子树中的最低的根节点。寻找节点x的后继的函数如下
public Node succ(Node x) {
if(x.rChild != null) {
x = x.rChild;
while(x != null) x = x.lChild;
}else {
if(x.parent == null) return null;
while(x.parent.lChild != x) x = x.parent;
x = x.parent;
}
return x;
}
if(x.rChild != null) {
x = x.rChild;
while(x != null) x = x.lChild;
}else {
if(x.parent == null) return null;
while(x.parent.lChild != x) x = x.parent;
x = x.parent;
}
return x;
}
有了这个函数,我们就可以实现额外空间O(1)的中序遍历。用一个标志变量back表示现在是不是刚从左子树回溯上来,从root开始,判断x有无左子树(rChild为空 or 刚从左子树回溯的情况统一视为无右子树)和有无右子树。循环体分为“访问”(x无左子树则访问)和“换挡”(x有右子树换到rChild,否则调用succ更新x为其后继)两部分。代码如下
public void inOrder(Node x) {
boolean back = false;
while(true) {
if(x.lChild != null && back == false) {
x = x.lChild;
}else {
System.out.print(x.value + " ");
}
if(x.rChild != null) {
x = x.rChild;
back = false;
}else {
x = succ(x);
if(x == null) break;
back = true;
}
}
}
值得注意的是这样的中序遍历会反复调用succ(),性能有所下降。
后序遍历:
递归版如下
public void postOrder(Node root) {
if(root == null) return;
postOrder(root);
postOrder(root);
System.out.print(root.value + " ");
}
先序遍历的顺序是 根->左->右,后序遍历是左->右->根。只要保证在每一个局部的子树上满足这个顺序,整个遍历序列就满足这个顺序。在先序遍历的算法里我们已经实现了在每个局部子树满足 根->左->右,把左右的对调,在每个局部子树上满足 根->右->左,反过来输出就是 左->右->根。把先序遍历的访问改为压栈,最后一个个弹出来访问,就可以实现迭代版后序遍历。代码如下
public void postOrder(Node root) {
if(root == null) return;
Node cur = root;
Stack stack = new Stack;
Stack output = new Stack;
stack.push(cur);
while(!stack.isEmpty()) {
cur = stack.pop();
output.push(cur);
if(cur.lChild != null) stack.push(cur.lChild);
if(cur.rChild != null) stack.push(cur.rChild);
}
while(!output.isEmpty()) {
System.out.print(output.pop().value + " ");
}
}
总结一下,关于这三种种遍历的迭代写法的理解,重点在于
1.把null当作叶节点
2.数学归纳法,n和n+1可以,那么任意的n都可以。
层次遍历
层次遍历很好理解,一层一层遍历,在每一层上从左往右走。这里还是一层一层进行,但是每一层都要遍历完在进入下一层。
还记得我们是怎么实现迭代版先序遍历的吗?利用栈结构先进后出的特点,把左孩子的两个孩子在右孩子后压栈,实现在遍历右子树前完成左子树的遍历。这里我们不用栈了,用先进先出的队列结构。每次取出队首的节点,将他的子节点按先左后右加入队列,这样下一层的节点永远在上一层节点之后加入队列,当然也在上一层所有节点之后被遍历。从第二层开始,每一层的遍历都是从左到右的,这一层的孩子节点自然也是从左到右的,实现了二叉树的层次遍历。代码如下
public void levelOrder(Node root){
if(root == null) return;
Quene quene = new LinkedList();//LinkedList类实现了Queue接口,因此可以把LinkedList当成Queue来用
quene.offer(root);
Node cur = root;
while(!quene.isEmpty()){
cur = quene.poll();
System.out.print(cur.value + " ");
if(cur.lChild != null) quene.offer(cur.lChild);
if(cur.rChild != null) quene.offer(cur.rChild);
}
}