二叉樹的非遞歸遍歷

二叉樹的遍歷有三種經典方式:前序遍歷、中序遍歷、後序遍歷。

遞歸遍歷

遞歸的寫法非常簡單:

前序遍歷

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 中斷當前函數執行,開始執行該子函數。

後記:其實,這裏的模擬系統棧功能還不夠全面,比如子函數的返回值怎麼模擬,調用子函數時參數的值傳遞與引用傳遞如何模擬。(因爲二叉樹的遍歷問題裏不要這些所以這裏沒有提及,大家可以自行思考一哈~)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章