1. 程式人生 > >二叉樹的遍歷(含統一格式遍歷)

二叉樹的遍歷(含統一格式遍歷)

//非遞迴前序遍歷
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);
        }
    }
}

轉自:https://blog.csdn.net/sdulibh/article/details/50573036
最後我說一句,這個方法是真的利索,我剛開始並沒有看懂,然後跟著走了一遍,才發現真的很好用,至於說缺點不足的地方嘛,或許就是每個節點都兩次入棧吧,但這種方法對於記憶來說真的很容易,就像是遞迴一樣簡潔易記憶。果然大佬就是大佬…