链式二叉树遍历算法实现(非递归法)
遍历二叉树的常用方法有四种:先(根)序遍历,中(根)序遍历,后(根)序遍历,层序遍历。本文只考虑用非递归方法实现先序遍历,中序遍历,后序遍历算法(递归实现方法见:链式二叉树遍历算法实现(递归法))。
非递归遍历算法是仿照递归遍历算法执行过程中调用栈的变化实现的,因此需要使用到栈,本程序借助于STL容器std::stack来实现。
中序遍历
中序遍历二叉树的节点访问顺序是:左子树、根节点、右子树。实现如下:
void InOrderTraverse1(Node *pstRoot) { if (!pstRoot) { return; } std::stack<Node *> s; s.push(pstRoot); //根节点入栈 Node *pstNode = NULL; while (!s.empty()) { //向左走到尽头,一边走一边入栈 while (pstNode = s.top()) { s.push(pstNode->left); } //空节点退栈 s.pop(); if (!s.empty()) { //访问节点,并向右走一步 pstNode = s.top(); s.pop(); if (pstNode) { printf("%c ", pstNode->data); s.push(pstNode->right); } } } }
上述遍历算法在将二叉树节点入栈时会将空节点也入栈,存在大量的空节点入栈、出栈操作,因此可以优化,以便省略掉空节点的入、出栈,优化后的代码如下:
void InOrderTraverse2(Node *pstRoot) { if (!pstRoot) { return; } std::stack<Node *> s; Node *pstNode = pstRoot; while (pstNode || (!s.empty())) { if (pstNode) {
//根节点入栈,遍历左子树并入栈 s.push(pstNode); pstNode = pstNode->left; } else {
//根节点出栈,访问根节点,再遍历其右子树 pstNode = s.top(); s.pop(); if (pstNode) { printf("%c ", pstNode->data); pstNode = pstNode->right; } } } }
先序遍历
先序遍历二叉树的节点访问顺序是:根节点、左子树、右子树。遍历过程类似于中序遍历,也是要从根节点一直向左访问,直到最左端的叶子节点,但是对这些节点访问后还要再入栈,因为后边还要根据这些节点访问其右子树。每次弹栈时,都将其右子树入栈并访问即可。
代码如下:
void PreOrderTraverse(Node *pstRoot) { if (!pstRoot) { return; } std::stack<Node *> s; Node *pstNode = pstRoot; while (pstNode || !s.empty()) { //向左走到尽头,一边访问节点一边入栈 while (pstNode) { printf("%c ", pstNode->data); s.push(pstNode); pstNode = pstNode->left; } //左子树访问完,弹栈,找右子树 if (!s.empty()) { pstNode = s.top(); s.pop();
if (pstNode) { pstNode = pstNode->right;
} } } }
后序遍历
后序遍历的访问顺序是:左子树、右子树、根节点。
非递归法后序遍历二叉树比较麻烦,因为在后序遍历中,要保证左子树和右子树都已被访问,并且左子树在右子树前访问,之后才能访问根结点。对任一节点而言,访问完左子树后,当前节点即位于栈顶,但此时还不能访问它,需要在其右子树访问完后再次回到此节点时才能访问,也就是说,此节点两次出现于栈顶,只有在第二次出现在栈顶时(即右子树遍历完成)才能访问,因此,需要能够区分出是刚访问完了左子树还是右子树。这就需要设置标记来识别,要么在二叉树节点中增加标志字段(要修改二叉树Node的定义,这往往是不被允许的),要么定义一个临时的数组(存在数组大小设置问题,无法确定合适的大小),两种方法都不太好。或者是像参考链接1的postOrder2()函数那样,将二叉树节点再封装下,增加一个标志字段,入栈、出栈等操作都是对此新节点进行的。
可以使用双栈来处理,这样的话就不需要用标记来区分从左子树返回的还是右子树返回的,见参考链接2。
另外,还有一种处理办法。前面看过的中序遍历和先序遍历处理时,都是从根节点开始一直向左遍历并入栈,到达最左端后再弹栈访问右子树,访问右子树都是通过栈顶节点操作的,这对于后序遍历不适用,如上所述,无法知道是从左子树返回的还是从右子树返回的。可以换种做法,先将当前节点的右孩子入栈,再入栈左孩子,根据栈的后进先出原理,弹栈时,会先访问左孩子,再访问右孩子,最后才是父节点,这样的话就不存在前述问题了。具体的说,对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,则可以直接访问它;如果P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,而左孩子和右孩子都在父结点前被访问。
代码如下:
void PostOrderTraverse(Node *pstRoot) { if (!pstRoot) { return; } std::stack<Node *> s; Node *curNode = NULL; //当前访问节点 Node *preNode = NULL; //前一个节点,可能是当前节点的左孩子或右孩子,或者是空节点(仅在最初时候) s.push(pstRoot); while (!s.empty()) { curNode = s.top(); //访问当前节点的两种情况: //1:当前节点没有左、右孩子,则访问此节点; //2:当前节点存在左孩子、右孩子或左右孩子都存在,上次访问的节点是当前节点的左孩子或右孩子(即左右子树都已访问完),则访问此节点 if ((curNode->left == NULL && curNode->right == NULL) || (preNode != NULL && (preNode == curNode->left || preNode == curNode->right))) { printf("%c ", curNode->data); s.pop(); preNode = curNode; //已出栈节点作为“前一个节点”记录下来 } else { //其他请求下,节点入栈,先将右孩子入栈,再讲左孩子入栈,确保左孩子能先出栈 if (curNode->right) { s.push(curNode->right); } if (curNode->left) { s.push(curNode->left); } } } }
参考:
https://www.cnblogs.com/SHERO-Vae/p/5800363.html
https://blog.csdn.net/tkp2014/article/details/48441161