二叉树遍历系列之二(迭代遍历)

本篇讲的是利用一个栈,进行二叉树的非递归遍历。很容易想到,递归是利用了系统的栈,而非递归也是用栈的数据结构对我们而言更方便。

  1. 前序遍历
    相较而言,前序遍历的非递归遍历算法还是很直观的。考虑到每个节点都是先自己,再左孩子,右孩子这样的访问顺序进行遍历。不难看出,每个节点其实只用访问一次就可以了,在第一次访问的时候,就把其值输出,接着只需处理它的左右孩子节点即可,此时该节点的值已不再重要,可完全舍弃。可以将栈看做是存储的均为待visit的节点,且它们孩子都还未visit。所以,可以根据这个思路来进行深度遍历,将遇到的每个节点都保存于栈中而暂不进行处理。然后当visit一个节点时,便输出它的值,接着再保存它的左右孩子节点地址于栈中。而因为我们使用了栈保存了尚未处理的节点,每次只需从栈中取出一个节点进行visit,这样依次就会visit所有节点。需要注意的是,因为栈的LIFO特性,我们在压栈的时候需要先push 右孩子再push左孩子。前序非递归深度遍历代码如下:
vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode *> nodes;
        if(root)
            nodes.push(root);
        while(!nodes.empty()) {
            TreeNode *cur=nodes.top();
            res.push_back(cur->val);
            nodes.pop();
            if(cur->right)
                nodes.push(cur->right);
            if(cur->left)
                nodes.push(cur->left);
         }
        return res;
    }

2. 中序遍历 中序遍历比前序遍历要难一些,也不那么直观了,实现就需要费点心思了。在前序遍历中,每个节点只需visit一次即可丢,所以栈中存储的可以看做是自己及孩子都尚未visit的节点。可是中序就不能如此了。很容易发现,当我们遇到一个节点时,需要把它的地址保存起来,而当我们去处理其left child时,该节点地址依旧是需要保存,不能丢弃。所以问题来了,如果我们将尚未处理的节点都保存,当遇到一个节点时,我们将它和它的右孩子通通压栈,然后去处理它的左孩子。这样,当我们从栈中拿出一个节点时,怎么判断该节点的left child是否处理过了呢?此时是轮到处理它的left child还是轮到它本身呢?所以,我们不能再简单沿照前序遍历的办法了,得再加点东西帮助我们来区分。 找一下规律,换个角度考虑下。对于一个节点,我们其实不必那么着急它的右孩子,因为我们都是在它的左孩子和它处理完成之后,才需要去处理它的右孩子。那么,其实我们也就不必那么着急去将它的右孩子压入栈中。对于一个节点,我们可以等待处理到它了,等到它从栈中移出了,再着手处理它的右孩子,对右孩子进行压栈等相关操作。而在此之前,我们可以完全不必理会它的右孩子。 根据以上思路,我们可以加一个指针,据此指针每次寻找当前节点的最左的子孙节点,并在寻找时将沿途的左树的中间节点均压入栈中。这样循环操作,直到没有节点再能压入栈中,即是该停止的时候,此时我们就该着手处理栈中的节点了。而每次从栈中取出一个节点时,我们都可以确定该节点的左孩子都已处理完毕,只消输出该节点值,下一个即该处理该节点的右子树了。而若该右子树为空,则应再处理栈中节点。这样,可以看出,每次循环结束,要处理的子树的根节点要么是指针所指向的值要么则是在栈顶。根据这些,写出的代码如下: ``` vector inorderTraversal(TreeNode* root) { vector res; stack nodes; TreeNode *cur=root; while(cur || !nodes.empty()) { if(cur) { while(cur) { nodes.push(cur); cur=cur->left; } } else { cur=nodes.top(); nodes.pop(); res.push_back(cur->val); cur=cur->right; } } return res; } ```

3.后序遍历 后序遍历用栈有好几种思路,这里仅举出两种方法 (1).可以发现,后序遍历与先序遍历相似,左右孩子节点的遍历不会被中间节点所打断。于是,我们可以未遍历的节点依次加入栈中,这样一来,如何区分该节点是其下整棵树都未遍历还是孩子均已被遍历过就成了一个很重要的问题。仔细观察后序遍历的规律,我们很容易发现,在后序遍历中,前一个遍历的节点要么是当前节点的左孩子,要么是当前节点的右孩子。依据这个特性,我们就可以用一个lastnode来保存上一个遍历的节点。然后每次据lastnode和当前节点的关系,判断当前节点的孩子是否已被visit。这样我们的代码和先序是十分相似的,因而只需在先序遍历的基础上加一些改动即可。代码如下。 ``` vector postorderTraversal(TreeNode* root) { vector res; stack nodes; if(root) nodes.push(root); TreeNode *lastnode=root,*cur; //make lastnode root not null,only to make it true for 1-child root while(!nodes.empty()) { cur=nodes.top(); if((!cur->left && !cur->right) || (lastnode==cur->left || lastnode==cur->right)) { res.push_back(cur->val); nodes.pop(); lastnode=cur; } else { if(cur->right) nodes.push(cur->right); if(cur->left) nodes.push(cur->left); } } return res; } ```

(2) 观察先序遍历和后序遍历的规律,先序遍历是中左右,后序遍历是左右中。不难想到,我们可以依据构建出一个中右左的序列,然后reverse序列,即可得到后序遍历的结果。代码只需在先序遍历的基础上稍加改动即可完成。代码如下: ``` vector postorderTraversal(TreeNode* root) { vector res; stack nodes; if(root) nodes.push(root); while(!nodes.empty()) { TreeNode *cur=nodes.top(); nodes.pop(); res.push_back(cur->val); if(cur->left) nodes.push(cur->left); if(cur->right) nodes.push(cur->right); } reverse(res.begin(),res.end()); return res; } ```

posted on 2018-03-06 23:53  禾杉  阅读(307)  评论(0)    收藏  举报