二叉树的DFS迭代遍历

2,552 阅读4分钟

二叉树的递归遍历还是很简单的,代码也很好写。但是在有的时候可能会要求用迭代法来遍历二叉树,或者你两种方法都会的话肯定也更好。既然可以用递归实现,而递归的实质其实就是栈,那么迭代法遍历很显然就是会用到栈。但是入栈和出栈的顺序是需要考虑清楚的,感觉自己经常会忘记怎么处理这个顺序,从而导致代码又不会写了,那么今天就来总结一个这个入栈出栈的顺序,逻辑理顺了,代码就不难写了。

1. 前序遍历的迭代实现

前序遍历的遍历顺序要保证:中->左->右。 其递归实现如下:

void preOrder(TreeNode* p){
    if(p == NULL)
        return;
    cout << p->val <<endl;
    preOrder(p->left);
    preOrder(p->right);
}
     1
   /   \
  2     3
 / \   / \
4   5 6   7

考虑到如上一个简单的二叉树,那么用先序遍历的实现结果应该是1 2 4 5 3 6 7。
一开始很显然从根节点开始沿着左子树往下遍历,这部分入栈也很简单,就是一直把当前节点入栈,在把当前节点指向左子树,直到当前节点为NULL。对应在这个例子里面就是入栈了1 2 4,然后当前节点已经指向了4的左子树,它是NULL。
问题是碰到这种情况下一步应该怎么做才能保证按正确的顺序遍历到2的右节点5呢?
首先很明确,要想使当前节点直到节点5,那么就必须通过指向2的右树来获得。现在栈顶是4,那么显然4应该被弹出来。
那么4又该什么时候被弹出来呢?
应该是遍历完了4的左树,且获得了4的右树指针之后,遍历4的右树之前。这主要是因为前序遍历要先遍历完左子树才能遍历右子树,在遍历右子树的时候,获取其右子树的指针前,不能把4删掉。而一旦左子树遍历完了之后,获得了指针马上要遍历右子树之前,应该立即把4出栈,因为遍历其右子树,假设其右子树不为空的话,那么就还会有节点入栈,因此得在遍历其右子树之前对4出栈,不然后面就无法在合适的节点出栈了。
因此,这个出栈顺序就确定下来了:当前节点的左子树已经遍历完,获得了其右子树的指针之后,且遍历右子树之前,应该把栈顶元素弹出。

void preOrder(TreeNode* p){
    if( p == NULL)
        return;
    stack<TreeNode*> s;
    while(p != NULL || !s.empty()){
        while(p){
            s.push(p);
            cout<< p->val <<endl;
            p = p->left;
        }
        p = s.top()->right;
        s.pop();
    }
}

2. 中序遍历的迭代实现

中序遍历的顺序:左->中->右。 中序遍历的栈的维护包括入栈出栈操作和前序遍历一致,不同的是,前序遍历按入栈的顺序输出节点的值,中序遍历弹出的时候输出节点的值。这主要是因为当前节点得等其左子树全部遍历完了之后右子树开始遍历之前才能输出,从前面的分析,那就正好是当前节点弹出的时间点。
代码如下:

void inOrder(TreeNode* p){
    if( p == NULL)
        return;
    stack<TreeNode*> s;
    while(p != NULL || !s.empty()){
        while(p){
            s.push(p);
            p = p->left;
        }
        p = s.top()->right;
        cout << s.top()->val << endl;
        s.pop();
    }
}

3. 后序遍历的迭代实现

后序遍历的顺序:左->右->中。
后序遍历和前序、中序不太一样,主要是因为每个节点都会先后用到三次。获得左节点需要一次,获得右节点需要一次,最后还需要在输出自己。而前序和中序输出自身和获取右节点是连续的,因此只需要两次。因为只需要两次,所有通过一次入栈和一次出栈就可以保证顺序正确。但是后序是不够的,所以还需要一个数据结构来存储信息,表明是第二次到还是第三次到当前节点。

void afterOrder(TreeNode* root) {
	if (root== NULL)
		return;
	stack<TreeNode*> s;
	vector<TreeNode*> v;
	s.push(root);
	TreeNode* p = NULL;
	while (!s.empty()) {
		p = s.top();
		bool flag1 = false;
		bool flag2 = false;

		if (p->left == NULL)
			flag1 = true;
		if (p->right == NULL)
			flag2 = true;
		if (!flag1) {
			auto it = v.begin();
			for (; it != v.end(); it++) {
				if (*it == p->left)
					break;
			}
			if (it != v.end()) {
				flag1 = true;
			}
		}
		if (!flag2) {
			auto it = v.begin();
			for (; it != v.end(); it++) {
				if (*it == p->right)
					break;
			}
			if (it != v.end()) {
				flag2 = true;
			}
		}
		if (flag1 == true && flag2 == true) {
			v.push_back(s.top());
			s.pop();
		}
		else if (flag1 == false) {
			s.push(p->left);
		}
		else if (flag1 == true && flag2 == false) {
			s.push(p->right);
		}		
	}
	for (auto p : v) {
		cout << p->val << endl;
	}
}

4. 总结

其实迭代法遍历二叉树不止以上介绍的方法,但是我觉得这种方法还是很好理解的,因为这种遍历的顺序是和递归函数调用的顺序很像的。如果感兴趣的话,也可学习其他迭代方法,进行对比。