前几天面试过程中面试官让手写一下二叉树后序遍历的非递归写法,当时没有写出来,本想着可能是因为面试太紧张的原因,才这么简单的题都没写出来,后来特地去研究了一下,发现二叉树的后序遍历非递归实现还真的没我想的那么简单,在此写个博客记录一下,顺便把前序和中序的非递归实现也写出来。
其实不管是前序、中序还是后序遍历,都只需要使用一个栈作为辅助来实现,其实现复杂度由到高分别为前序、中序、后序。下面按照前中后序遍历的顺序来记录一下非递归实现的思路和代码。
首先定义一下树节点的结构体:
struct TreeNode
{
int data;
TreeNode* left=nullptr;
TreeNode* right=nullptr;
};
前序遍历(非递归)
思路
前序遍历的思路其实和DFS(深度优先搜索)是一样的,每次只需要从栈顶弹出一个节点,并将该节点的孩子节点压入栈,需要注意的就是,因为栈后入先出的特性,要先压右孩子再压左孩子才能满足前序遍历的访问顺序。
//前序遍历
void preOrderVisit(TreeNode* root)
{
if(root==nullptr)
return ;
stack<TreeNode*> nodeStack;
nodeStack.push(root);
while(!nodeStack.empty())
{
cout<<nodeStack.top()->data<<" ";
TreeNode* temp=nodeStack.top();
nodeStack.pop();
//前序遍历一定要注意先压入当前节点的右孩子,因为栈是先进后出的结构
if(temp->right)
nodeStack.push(temp->right);
if(temp->left)
nodeStack.push(temp->left);
}
}
中序遍历(非递归)
思路
如果使用栈来实现中序遍历,如果栈顶的当前节点没有被访问,那么就需要不断地把栈顶节点的左孩子压入栈,反复循环直到左孩子为空;那么这样就需要解决一个问题:如何判断栈顶的当前节点有没有被访问过呢?(即要判断是第一次访问还是经过回溯弹栈之后再次访问);这里的解决方法是用一个bool类型的变量traceback来标记当前节点是否是通过回溯访问的,traceback初始化为false,即标记为第一次访问而不是回溯访问。关键的是要在当前节点右孩子为空时,即回溯返回,将traceback置为true,当前栈顶的元素是回溯返回的再次方位,这样就不会将其左孩子反复压入栈;如果右孩子不为空,将其右孩子压入栈,并将traceback置为false,
//中序遍历
void inOrderVisit(TreeNode* root)
{
if(root==nullptr)
return ;
stack<TreeNode*> nodeStack;
bool traceback=false; //用来标记当前节点是否是回溯访问的
nodeStack.push(root);
while(!nodeStack.empty())
{
//如果不是回溯访问,那么不断将栈顶节点的左孩子压入栈,直到叶子节点
while(nodeStack.top()->left!=nullptr&&!traceback)
nodeStack.push(nodeStack.top()->left);
TreeNode* temp=nodeStack.top();
cout<<temp->data<<" ";
nodeStack.pop();
if(temp->right)
{
traceback=false; //若存在右孩子则将其压入栈,并标记为不是回溯访问
nodeStack.push(temp->right);
}
else
traceback=true; //若不存在右孩子,则标记为回溯返回
}
}
后序遍历非递归实现
思路
后序遍历的实现思路和中序遍历有相似的地方,都需要判断当前栈顶节点是否是回溯返回,同样的使用一个bool类型的变量来标记是否是回溯返回。不一样的地方就是父节点不能先于右孩子弹出栈,而是要继续留在栈中,这就需要判断当前节点(栈顶元素)的右孩子是否被访问过(和之前判断当前节点是否被访问过不一样,这里是要判断当前节点的右孩子是否被访问过);对此可以使用一个指针用来指向上一次出栈的节点,这样在访问当前节点的时候就可以判断上一次出栈的节点是不是当前节点的右孩子;如果是它的右孩子则说明当前节点的右孩子已经被访问过了,就不需要将右孩子压栈了;否则需要将当前节点的右孩子压入栈
//后序遍历
void postOrderVisit(TreeNode* root)
{
if(root==nullptr)
return ;
stack<TreeNode*> nodeStack;
TreeNode* prev=nullptr; //记录上一次从栈中弹出的节点
bool traceback=false; //用来标记当前节点是否是回溯访问的
nodeStack.push(root);
while(!nodeStack.empty())
{
//如果不是回溯访问,则一直讲栈顶节点的左孩子压入栈
while(nodeStack.top()->left!=nullptr&&!traceback)
nodeStack.push(nodeStack.top()->left);
//如果当前节点没有右孩子或者右孩子已经被访问则弹出该节点
if(nodeStack.top()->right==nullptr||prev==nodeStack.top()->right)
{
cout<<nodeStack.top()->data<<" ";
prev=nodeStack.top();
nodeStack.pop();
traceback=true; //标记回溯返回
}
else if(nodeStack.top()->right!=nullptr)
{
nodeStack.push(nodeStack.top()->right);
traceback=false; //标记非回溯返回
}
}
}
验证
这里自己构建一颗简单的树来验证前面写的前中后序遍历的实现是否正确:
验证代码
int main()
{
/***************构建二叉树*****************/
TreeNode* n1=new TreeNode;
TreeNode* n2=new TreeNode;
TreeNode* n3=new TreeNode;
TreeNode* n4=new TreeNode;
TreeNode* n5=new TreeNode;
TreeNode* n6=new TreeNode;
TreeNode* n7=new TreeNode;
n1->data=1;n1->left=n2;n1->right=n3;
n2->data=2;n2->left=n4;n2->right=n5;
n3->data=3;n3->left=n6;n3->right=n7;
n4->data=4;n5->data=5;n6->data=6;n7->data=7;
/****************************************/
//前序遍历
cout<<"preOrderVisit: ";
preOrderVisit(n1);
cout<<endl;
//中序遍历
cout<<"midOrderVisit: ";
inOrderVisit(n1);
cout<<endl;
//后序遍历
cout<<"postOrderVisit: ";
postOrderVisit(n1);
cout<<endl;
return 0;
}