二叉樹的遍歷有三種經典方式:前序遍歷、中序遍歷、後序遍歷。
遞歸遍歷
遞歸的寫法非常簡單:
前序遍歷
void preorder_traverse(tree_node* root) {
if (root == nullptr)
return;
this->preorder.push_back(root->value);
preorder_traverse(root->left);
preorder_traverse(root->right);
}
中序遍歷
void inorder_traverse(tree_node* root) {
if (root == nullptr)
return;
inorder_traverse(root->left);
this->inorder.push_back(root->value);
inorder_traverse(root->right);
}
後序遍歷
void postorder_traverse(tree_node* root) {
if (root == nullptr)
return;
postorder_traverse(root->left);
postorder_traverse(root->right);
this->postorder.push_back(root->value);
}
非遞歸的深度優先遍歷
二叉樹的非遞歸遍歷是一個經典問題,有教科書式的完美解法,但相信很多人和我一樣:書打開,“嗯。。馬冬梅”,等到要用的時候(比如說面試),“馬什麼梅來着。。?”。遞歸的算法很容易記住,試想,如果我們能夠根據遞歸的算法寫出非遞歸的算法,這個問題不就迎刃而解了麼~現在,這個問題轉化成了——給定任意一個遞歸算法,如何寫出其非遞歸版本?其實,遞歸也是函數調用的一種,只不過遞歸調用的是這個函數本身。讓我們來回憶一下操作系統是如何處理函數調用的:
當發生函數調用時,系統暫存當前的系統狀態(將當前系統狀態壓棧),開始執行子函數,待子函數執行完畢後,再從系統棧頂讀取之前保存的系統狀態,繼續主函數的執行。
這爲我們模擬系統棧遞歸函數非遞歸化提供了一些思路。下面,我們就二叉樹遍歷中最難非遞歸化的後序遍歷爲例,來說明如何模擬系統棧將該遞歸算法非遞歸化。再看一眼後序遍歷的遞歸算法:
1 void postorder_traverse(tree_node* root) {
2 if (root == nullptr)
3 return;
4 postorder_traverse(root->left);
/* 這裏發生了一次遞歸調用,我們需要保存當前的運行狀態。
這個“運行狀態”究竟包含哪些信息呢?
1. 數據:等下到第5行時我們還需要用到 root->right,所以現在應該把 root 壓棧存起來;
2. 命令:等執行完第4行的調用後返回現在這個函數中時,我們需要知道下面該執行哪行代碼了,
所以這裏應該把“第5行”存下來,表示第4行的調用執行完了後該執行第5行代碼了 */
5 postorder_traverse(root->right);
/* 程序運行到這裏,表明第4行的調用已經結束,應該恢復上次的運行狀態,
然而這裏又發生了一次遞歸調用,我們需要再次保存當前的運行狀態:
1. 數據:等下第6行要用 root->value, 所以把 root 壓棧存起來,
2. 命令:“第6行”,表示等第5行的調用結束了該執行第6行 */
6 this->postorder.push_back(root->value);
/* 第5行的調用結束,取出上次保存的運行狀態,繼續程序執行*/
7 }
從上面的分析可以看出,程序的“運行狀態”應該包含“數據”和“命令”這兩種信息。所以我們所模擬的系統棧的應該能保存這兩種信息,這裏爲了實現方便,我們使用兩個棧:
void post_traverse(tree_node* root) {
stack<tree_node*> vars; //存“數據”的棧
stack<string> states; //存“命令”的棧
}
這兩個棧應該怎麼用呢?
root = vars.top(); //取出當前需要處理的數據
vars.pop();
string state = states.top(); //取出當前應該執行的命令
states.pop();
根據 state 指示的代碼行數對數據乾點啥;
/* 遇到函數調用時 */
vars.push(該保存的數據);
states.push(這次調用結束後該執行的命令);
上面的僞代碼表明瞭如何用棧處理調用,但如何開始一個函數的執行,以及如何結束一個函數呢
/* 系統不斷從棧中取出數據和要執行的代碼行數,開始執行當前函數,直至系統棧爲空,此時表明函數(包括其調用的所有子函數),已經執行完畢。*/
1 while (!vars.empty()) {
2 root = vars.top(); //取數據
3 vars.pop();
4 string state = states.top(); //取要執行的命令
5 states.pop();
6 根據 state 指示的代碼行數對數據乾點啥;
/* 遇到函數調用時 */
7 vars.push(該保存的數據);
8 states.push(這次調用結束後該執行的命令);
9 vars.push(子函數需要用到的數據); /* 想想這裏爲什麼要將子函數需要用的數據和命令壓棧?*/
10 states.push(子函數開始執行的命令);
11 continue;
/* 中斷當前函數的執行,開始下一輪循環(即子函數的執行):
在下一輪循環開始時,我們取出剛壓入棧的子函數數據和命令,開始執行子函數。 */
}
經過上面的分析,我們發現應該在子函數的入口處也標記一個命令,這樣才能順利的開始執行子函數。當然,對於遞歸函數來說,子函數還是它自己。再來看一下遞歸算法中需要標記的代碼行數。
1 void postorder_traverse(tree_node* root) {
2 if (root == nullptr) //entrance_1:函數入口
3 return;
4 postorder_traverse(root->left);
5 postorder_traverse(root->right); //entrance_2: 第4行調用返回後的繼續執行的入口
6 this->postorder.push_back(root->value); // entrance_3: 第5行調用返回後的繼續執行的入口
7 }
現在,經過上面的一通分析,我們終於可以寫出後序遍歷的非遞歸版本了:
後序遍歷(非遞歸)
void postorder_traverse2(tree_node *root)
{
stack<tree_node*> vars; //數據棧
stack<string> states; //命令棧
vars.push(root); //preorder_traverse() 開始執行時需要的數據
states.push("entrance_1"); //preorder_traverse() 開始執行時的命令
while (!vars.empty()) {
root = vars.top(); //取數據
vars.pop();
string state = states.top(); //取命令
states.pop();
if (state == "entrance_1") { //現在該執行 preorder_traverse() 的第2行代碼了
if (root == nullptr) { //對着 preorder_traverse() 的第2行代碼,開抄
continue;
}
/* 這裏對應 preorder_traverse() 的第4行代碼 */
vars.push(root); //保存數據
states.push("entrance_2"); //保存命令:待會調用結束該從 entrance_2 執行代碼了
vars.push(root->left); //爲 preorder_traverse() 的第4行調用準備數據
states.push("entrance_1"); //爲 preorder_traverse() 的第4行調用準備命令
continue; //開始執行 preorder_traverse() 的第4行的調用吧~
}
if (state == "entrance_2") { //現在該執行 preorder_traverse() 的第5行代碼了
vars.push(root); //保存數據
states.push("entrance_3"); //保存命令:待會調用結束該從 entrance_2 執行代碼了
vars.push(root->right); //爲 preorder_traverse() 的第5行調用準備數據
states.push("entrance_1"); //爲 preorder_traverse() 的第5行調用準備命令
continue; //開始執行 preorder_traverse() 的第4行的調用吧~
}
if (state == "entrance_3") { //現在該執行 preorder_traverse() 的第6行代碼了
this->postorder.push_back(root->value); //開始抄 preorder_traverse() 的代碼
}
} //這個反括號就是 preorder_traverse() 第7行那個反括號。
}
這種方式雖然沒有教科書上的非遞歸方法那麼精妙絕倫,但形式整齊,便於記憶。更重要的是,當我們掌握了模擬系統棧的方法後,以後不論是後序遍歷、前序遍歷還是別的任何遞歸算法,我們都能通過這種方式“無痛”地寫出對應的非遞歸算法。
下面應用這種方法給出前序遍歷和中序遍歷的非遞歸版本:
前序遍歷(非遞歸)
void preorder_traverse2(tree_node* root) {
stack<tree_node*> vars;
stack<string> states;
vars.push(root);
states.push("entrance_1");
while (!vars.empty()) {
root = vars.top();
vars.pop();
string state = states.top();
states.pop();
if (state == "entrance_1") {
if (root == nullptr) {
continue;
}
this->preorder.push_back(root->value);
vars.push(root);
states.push("entrance_2");
vars.push(root->left);
states.push("entrance_1");
continue;
}
if (state == "entrance_2") {
vars.push(root->right);
states.push("entrance_1");
}
}
}
中序遍歷(非遞歸)
void inorder_traverse2(tree_node* root) {
stack<tree_node*> vars;
stack<string> states;
vars.push(root);
states.push("entrance_1");
while (!vars.empty()) {
root = vars.top();
vars.pop();
string state = states.top();
states.pop();
if (state == "entrance_1") {
if (root == nullptr) {
continue;
}
vars.push(root);
states.push("entrance_2");
vars.push(root->left);
states.push("entrance_1");
continue;
}
if (state == "entrance_2") {
this->inorder.push_back(root->value);
vars.push(root->right);
states.push("entrance_1");
}
}
}
最後,對模擬系統棧的方法做一個總結:
-
設置兩個棧,一個用來存儲數據,一個用來存儲命令(要執行的代碼行數)
-
設置一個循環,不斷地從數據棧和命令棧讀取數據和命令,開始(繼續)當前函數的執行。當棧爲空時,表明該函數(以及其所有調用的子函數)執行完畢。
-
發生函數調用時:
1、保存現場(調用結束後需要用到的數據、調用結束後需要執行的代碼行數)
2、準備調用函數的數據和命令。
3、使用 continue 中斷當前函數執行,開始執行該子函數。
後記:其實,這裏的模擬系統棧功能還不夠全面,比如子函數的返回值怎麼模擬,調用子函數時參數的值傳遞與引用傳遞如何模擬。(因爲二叉樹的遍歷問題裏不要這些所以這裏沒有提及,大家可以自行思考一哈~)