二叉树的遍历,递归与迭代版

二叉树的遍历有三种:先序遍历、中序遍历、后序遍历。

对于每一个子树,遍历的顺序是

    先序遍历:根      ->左子树->右子树    ,如图中①

    中序遍历:左子树->根      ->右子树    ,如图中②

    后序遍历:左子树->右子树->根          ,如图中③


先序遍历:

    按照上面的规则,递归版本不难给出

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出栈之前入栈,对右子树的遍历只能在左子树遍历完成后开始,就实现了迭代版的先序遍历,代码如下

扫描二维码关注公众号,回复: 2146516 查看本文章
public void preOrder(Node root) {
	if(root == null) return;
	Node cur = root;
	Stack<Node> stack = new Stack<Node>();
	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,会在左子树完成遍历后被访问,实现了迭代版中序遍历。代码如下

public void inOrder(Node root) {
	if(root == null) return;
	Node cur = root;
	Stack<Node> stack = new Stack<Node>();
	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;
}

    有了这个函数,我们就可以实现额外空间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<Node> stack = new Stack<Node>;
	Stack<Node> output = new Stack<Node>;
	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都可以。

猜你喜欢

转载自blog.csdn.net/Daniel_2046/article/details/81008302