前幾天面試過程中面試官讓手寫一下二叉樹後序遍歷的非遞歸寫法,當時沒有寫出來,本想着可能是因爲面試太緊張的原因,才這麼簡單的題都沒寫出來,後來特地去研究了一下,發現二叉樹的後序遍歷非遞歸實現還真的沒我想的那麼簡單,在此寫個博客記錄一下,順便把前序和中序的非遞歸實現也寫出來。
其實不管是前序、中序還是後序遍歷,都只需要使用一個棧作爲輔助來實現,其實現複雜度由到高分別爲前序、中序、後序。下面按照前中後序遍歷的順序來記錄一下非遞歸實現的思路和代碼。
首先定義一下樹節點的結構體:
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;
}