不使用递归方式遍历一棵树在工业界有经常的应用,比如在解析一个html文档的时候就是遍历一棵html树。我们以如下图的html二叉树,以前序遍历为例子进行遍历。
在我的以前文章中有介绍到 以二叉树的销毁的例子 去深刻理解递归算法,使用递归遍历一颗二叉树,借用到栈讲述递归的原理。这里不用递归是因为,递归压栈是整个函数的压栈,也就是把当前整个现场环境压进栈中,这样会很消耗内存空间。如果在一棵巨大的树面前,递归遍历的方式会慢到难以置信,甚至压垮计算机。
树的节点
我们以Java语言进行描述,当然怎么方便怎么来。对上图的节点用一个Node
类描述,value存放结点值。
public class Node {
//节点中保存的值
public String value;
//左子树
public Node leftChild;
//右子树
public Node rightChild;
}
创建树
然后把上图中的树表达在代码中,我们重点是关注如何遍历树的,创建树的过程我们就硬编码,先创建叶子节点,依次从叶子节点往上创建好节点,然后再用leftChild和rightChild关系串联起来就可以了。如下的代码,最终根节点就是html节点。
//创建叶子节点,倒着建立的
Node meta = new Node("meta", null, null);
Node ttile = new Node("ttile", null, null);
Node h1 = new Node("h1", null, null);
Node p = new Node("p", null, null);
Node head = new Node("head", meta, ttile);
Node body = new Node("body", h1, p);
Node html = new Node("html", head, body);
以案例来描述前序遍历
前序遍历一股脑的说来就是先遍历头节点然后再遍历左边节点; 左边遍历完成后,再遍历右边节点。
我们尝试着手动遍历上面的那棵树,
如下图,我们拿到一棵树,一直访问左节点下去,遇到左节点然后遍历打印出来。比如访问到html节点
先打印它的值;向下访问html左节点
发现到有节点,节点为head节点
。依次进行下去,访问到meta节点
。
访问到meta节点
,此时到叶子节点;不能再往左子树访问了,也不能往右子树访问了,这个时候需要访问title节点
了。需要访问title节点
是我们人类的想法,计算机可没那么聪明,我们需要告诉它。
我们这时就需要做一个小动作了,之前的递归遍历是操作系统使用栈保存现场,等回头来再使用这个现场。我们此时也用栈,只是不用栈保存现场,用栈来保存未访问兄弟节点。
比如第一次访问head节点
时,需要将它的兄弟节点body节点
压入栈中;访问到meta节点
时,其兄弟节点title节点
压入栈中。这样在访问到meta这个叶子节点
后,只需要访问栈顶的节点即可。根据如上的栈图,栈顶是title节点
,弹出该节点,title节点
就可以访问到了。
依次进行下去,我们就能访问到p节点了,最后一步弹出这个栈顶就大功告成。
不使用递归前序遍历的Java代码如下,关于栈的查看方式,可以通过调试时候打印出当前栈的内容,就可以直观显示出来了。
public void preOrderTraverse(Node node) {
//新建一个栈,用于暂存未遍历的节点
Stack<Node> stack = new Stack<>();
Node nextNode = node;
while (nextNode != null) {
System.out.println(nextNode.value);
//访问左子树,并将未访问的兄弟节点压栈
if (nextNode.leftChild != null) {
if (nextNode.rightChild != null) {
stack.push(nextNode.rightChild);
}
nextNode = nextNode.leftChild;
continue;
}
//访问右子树
if (nextNode.rightChild != null) {
nextNode = nextNode.rightChild;
continue;
}
if (stack.isEmpty()) {
break;
}
nextNode = stack.pop();
}
}
水平有限,如果存在错误,希望不吝指出,谢谢。