1. 程式人生 > >對於二叉樹三種非遞迴遍歷方式的理解

對於二叉樹三種非遞迴遍歷方式的理解

解決二叉樹的很多問題的方案都是基於對二叉樹的遍歷。遍歷二叉樹的前序,中序,後序三大方法算是計算機科班學生必寫程式碼了。其遞迴遍歷是人人都能信手拈來,可是在手生時寫出非遞迴遍歷恐非易事。正因為並非易事,所以網上出現無數的介紹二叉樹非遞迴遍歷方法的文章。可是大家需要的真是那些非遞迴遍歷程式碼和講述嗎?程式碼早在學資料結構時就看懂了,理解了,可為什麼我們一而再再而三地忘記非遞迴遍歷方法,卻始終記住了遞迴遍歷方法?

三種遞迴遍歷對遍歷的描述,思路非常簡潔,最重要的是三種方法完全統一,大大減輕了我們理解的負擔。而我們常接觸到那三種非遞迴遍歷方法,除了都使用棧,具體實現各有差異,導致了理解的模糊。本文給出了一種統一的三大非遞迴遍歷的實現思想。

三種遞迴遍歷

//前序遍歷
void preorder(TreeNode *root, vector<int> &path)
{
    if(root != NULL)
    {
        path.push_back(root->val);
        preorder(root->left, path);
        preorder(root->right, path);
    }
}
//中序遍歷
void inorder(TreeNode *root, vector<int> &path)
{
    if(root != NULL
) { inorder(root->left, path); path.push_back(root->val); inorder(root->right, path); } }
//後續遍歷
void postorder(TreeNode *root, vector<int> &path)
{
    if(root != NULL)
    {
        postorder(root->left, path);
        postorder(root->right, path);
        path.push_back(root->val);
    }
}

由上可見,遞迴的演算法實現思路和程式碼風格非常統一,關於“遞迴”的理解可見我的《人腦理解遞迴》

教科書上的非遞迴遍歷

//非遞迴前序遍歷
void preorderTraversal(TreeNode *root, vector<int> &path)
{
    stack<TreeNode *> s;
    TreeNode *p = root;
    while(p != NULL || !s.empty())
    {
        while(p != NULL)
        {
            path.push_back(p->val);
            s.push(p);
            p = p->left;
        }
        if(!s.empty())
        {
            p = s.top();
            s.pop();
            p = p->right;
        }
    }
}
//非遞迴中序遍歷
void inorderTraversal(TreeNode *root, vector<int> &path)
{
    stack<TreeNode *> s;
    TreeNode *p = root;
    while(p != NULL || !s.empty())
    {
        while(p != NULL)
        {
            s.push(p);
            p = p->left;
        }
        if(!s.empty())
        {
            p = s.top();
            path.push_back(p->val);
            s.pop();
            p = p->right;
        }
    }
}
//非遞迴後序遍歷-迭代
void postorderTraversal(TreeNode *root, vector<int> &path)
{
    stack<TempNode *> s;
    TreeNode *p = root;
    TempNode *temp;
    while(p != NULL || !s.empty())
    {
        while(p != NULL) //沿左子樹一直往下搜尋,直至出現沒有左子樹的結點
        {
            TreeNode *tempNode = new TreeNode;
            tempNode->btnode = p;
            tempNode->isFirst = true;
            s.push(tempNode);
            p = p->left;
        }
        if(!s.empty())
        {
            temp = s.top();
            s.pop();
            if(temp->isFirst == true)   //表示是第一次出現在棧頂
            {
                temp->isFirst = false;
                s.push(temp);
                p = temp->btnode->right;
            }
            else  //第二次出現在棧頂
            {
                path.push_back(temp->btnode->val);
                p = NULL;
            }
        }
    }
}

看了上面教科書的三種非遞迴遍歷方法,不難發現,後序遍歷的實現的複雜程度明顯高於前序遍歷和中序遍歷,前序遍歷和中序遍歷看似實現風格一樣,但是實際上前者是在指標迭代時訪問結點值,後者是在棧頂訪問結點值,實現思路也是有本質區別的。而這三種方法最大的缺點就是都使用巢狀迴圈,大大增加了理解的複雜度。

更簡單的非遞迴遍歷二叉樹的方法

這裡我給出統一的實現思路和程式碼風格的方法,完成對二叉樹的三種非遞迴遍歷。

//更簡單的非遞迴前序遍歷
void preorderTraversalNew(TreeNode *root, vector<int> &path)
{
    stack< pair<TreeNode *, bool> > s;
    s.push(make_pair(root, false));
    bool visited;
    while(!s.empty())
    {
        root = s.top().first;
        visited = s.top().second;
        s.pop();
        if(root == NULL)
            continue;
        if(visited)
        {
            path.push_back(root->val);
        }
        else
        {
            s.push(make_pair(root->right, false));
            s.push(make_pair(root->left, false));
            s.push(make_pair(root, true));
        }
    }
}
//更簡單的非遞迴中序遍歷
void inorderTraversalNew(TreeNode *root, vector<int> &path)
{
    stack< pair<TreeNode *, bool> > s;
    s.push(make_pair(root, false));
    bool visited;
    while(!s.empty())
    {
        root = s.top().first;
        visited = s.top().second;
        s.pop();
        if(root == NULL)
            continue;
        if(visited)
        {
            path.push_back(root->val);
        }
        else
        {
            s.push(make_pair(root->right, false));
            s.push(make_pair(root, true));
            s.push(make_pair(root->left, false));
        }
    }
}
//更簡單的非遞迴後序遍歷
void postorderTraversalNew(TreeNode *root, vector<int> &path)
{
    stack< pair<TreeNode *, bool> > s;
    s.push(make_pair(root, false));
    bool visited;
    while(!s.empty())
    {
        root = s.top().first;
        visited = s.top().second;
        s.pop();
        if(root == NULL)
            continue;
        if(visited)
        {
            path.push_back(root->val);
        }
        else
        {
            s.push(make_pair(root, true));
            s.push(make_pair(root->right, false));
            s.push(make_pair(root->left, false));
        }
    }
}

以上三種遍歷實現程式碼行數一模一樣,如同遞迴遍歷一樣,只有三行核心程式碼的先後順序有區別。為什麼能產生這樣的效果?下面我將會介紹。

有重合元素的區域性有序一定能導致整體有序

這就是我得以統一三種更簡單的非遞迴遍歷方法的基本思想:有重合元素的區域性有序一定能導致整體有序
如下這段序列,區域性2 3 4和區域性1 2 3都是有序的,但是不能由此保證整體有序。
Image Title

而下面這段序列,區域性2 3 4,4 5 6,6 8 10都是有序的,而且相鄰區域性都有一個重合元素,所以保證了序列整體也是有序的。
Image Title

應用於二叉樹

基於這種思想,我就構思三種非遞迴遍歷的統一思想:不管是前序,中序,後序,只要我能保證對每個結點而言,該結點,其左子結點,其右子結點都滿足以前序/中序/後序的訪問順序,整個二叉樹的這種三結點區域性有序一定能保證整體以前序/中序/後序訪問,因為相鄰的區域性必有重合的結點,即一個區域性的“根”結點是另外一個區域性的“子”結點。

如下圖,對二叉樹而言,將每個框內結點集都看做一個區域性,那麼區域性有A,A B C,B D E,D,E,C F,F,並且可以發現每個結點元素都是相鄰的兩個區域性的重合結點。發覺這個是非常關鍵的,因為知道了重合結點,就可以對一個區域性排好序後,通過取出一個重合結點過渡到與之相鄰的區域性進行新的區域性排序。我們可以用棧來保證區域性的順序(排在順序前面的後入棧,排在後面的先入棧,保證這個區域性元素出棧的順序一定正確),然後通過棧頂元素(重合元素)過渡到對新區域性的排序,對新區域性的排序會導致該重合結點再次入棧,所以當棧頂出現已完成過渡使命的結點時,就可以徹底出棧輸出了(而這個輸出可以保證該結點在它過渡的那個區域性一定就是排在最前面的),而新棧頂元素將會繼續完成新區域性的過渡。當所有結點都完成了過渡使命時,就全部出棧了,這時我敢保證所有區域性元素都是有序出棧,而相鄰區域性必有重合元素則保證了整體的輸出一定是有序的。這種思想的好處是將演算法與順序分離,定義何種順序並不影響演算法,演算法只做這麼一件事:將棧頂元素取出,使以此元素為“根”結點的區域性有序入棧,但若此前已通過該結點將其區域性入棧,則直接出棧輸出即可
Image Title

從實現的程式中可以看到:三種非遞迴遍歷唯一不同的就是區域性入棧的三行程式碼的先後順序。所以不管是根->左->右,左->根->右,左->右->根,甚至是根->右->左,右->根->左,右->左->根定義的新順序,演算法實現都無變化,除了改變區域性入棧順序。

值得一提的是,對於前序遍歷,大家可能發現取出一個棧頂元素,使其區域性前序入棧後,棧頂元素依然是此元素,接著就要出棧輸出了,所以使其隨區域性入棧是沒有必要的,其程式碼就可以簡化為下面的形式。

void preorderTraversalNew(TreeNode *root, vector<int> &path)
{
    stack<TreeNode *> s;
    s.push(root);
    while(!s.empty())
    {
        root = s.top();
        s.pop();
        if(root == NULL)
        {
            continue;
        }
        else
        {
            path.push_back(root->val);
            s.push(root->right);
            s.push(root->left);
        }
    }
}

這就是我要介紹的一種更簡單的非遞迴遍歷二叉樹的方法。