二叉樹的前序、中序、后序遍歷非遞歸實現


這是leetcode上的3個題目,要求用非遞歸實現,其中以后序遍歷實現最難,既然遞歸實現的三種遍歷程序只需要改變輸入代碼順序,為什么循環不可以呢,帶着這種執拗的想法,我開始了這次研究

我依然是將遞歸用棧來實現,而不打算使用改變二叉樹結構的方法,那個我打算日后研究

首先以前序遍歷為例

遞歸實現是:

void preorderTraversal(TreeNode* root) {
    if (root == nullptr)
        return;
    cout << root->val;
    preorderTraversal(root->left);
    preorderTraversal(root->right);
}

利用循環和棧來實現遞歸

我的思路是每次循環對應一次函數調用,每次函數調用的root加入棧中,思考后,寫出下面的程序

void preorderTraversal(TreeNode* root) {
    if (root == nullptr)
        return;
    stack<TreeNode*> sta;
    sta.push(root);
    while (!sta.empty()) {
        root = sta.top();
        cout << root->val;
        if (root->left != nullptr) {
            sta.push(root->left);
            continue;
        }
        if (root->right != nullptr) {
            sta.push(root->right);
            continue;
        }
        sta.pop();
    }
}

但是經測試發現,這段程序有個巨大的漏洞,以至於無法結束循環,問題出現在pop某節點后,返回到父節點,會再次將該節點push到棧中,從而無限循環

對照遞歸的實現,腦補計算機函數進棧出棧的抽象圖,發現計算機的函數堆棧絕非是保存root的棧可以替代的,它還保存了代碼的執行位置,也就是棧指針幀指針

我們能否用變量保存上次循環執行的代碼位置呢,我們可以設置一些tag標記來模擬棧指針。但是,這里還有更好的實現方法。

我們可以保存上次循環結束時的root節點,其名lastRoot,

如果lastRoot = root->left,則說明root->left已經遍歷過一遍,不用再將其加入棧中,也就是下面的代碼段不用再執行

if (root->left != nullptr) {
    sta.push(root->left);
    continue;
}

如果lastRoot = root->right,則說明root->left和root->right都已經遍歷過一遍,可以直接將root出棧,下面的代碼段不用執行

if (root->left != nullptr) {
    sta.push(root->left);
    continue;
}
if (root->right != nullptr) {
    sta.push(root->right);
    continue;
}

 

理解之后,不難寫出代碼,前序,中序,后續遍歷的區別在於改變輸出當前節點代碼段的位置,同遞歸實現一樣,只是順序的區別,程序員就是這樣懶,妄想一招鮮吃遍天下

// 前序遍歷
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> out;
    if (root == nullptr)
        return out;
    stack<TreeNode*> sta;
    sta.push(root);
    TreeNode* lastRoot = root;
    while (!sta.empty())
    {    
        root = sta.top();
        if(lastRoot != root->right)
        {
            if (lastRoot != root->left) {
                out.push_back(root->val);
                if (root->left != nullptr) {
                    sta.push(root->left);
                    continue;
                }
            }            
            if (root->right != nullptr) {
                sta.push(root->right);
                continue;
            }
        }
        lastRoot = root;
        sta.pop();
    }
    return out;
}

// 中序遍歷
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> out;
    if (root == nullptr)
        return out;
    stack<TreeNode*> sta;
    sta.push(root);
    TreeNode* lastRoot = root;
    while (!sta.empty())
    {
        root = sta.top();
        if (lastRoot != root->right)
        {
            if (lastRoot != root->left) {                
                if (root->left != nullptr) {
                    sta.push(root->left);
                    continue;
                }
            }
            out.push_back(root->val);
            if (root->right != nullptr) {
                sta.push(root->right);
                continue;
            }
        }
        lastRoot = root;
        sta.pop();
    }
    return out;
}

// 后序遍歷
vector<int> postorderTraversal(TreeNode* root) {
    vector<int> out;
    if (root == nullptr)
        return out;
    stack<TreeNode*> sta;
    sta.push(root);
    TreeNode* lastRoot = root;
    while (!sta.empty())
    {
        root = sta.top();
        if (lastRoot != root->right)
        {
            if (lastRoot != root->left) {
                if (root->left != nullptr) {
                    sta.push(root->left);
                    continue;
                }
            }            
            if (root->right != nullptr) {
                sta.push(root->right);
                continue;
            }            
        }
        out.push_back(root->val);
        lastRoot = root;
        sta.pop();
    }
    return out;
}

 

另外還有其他實現方法,比如前序遍歷,網上比較流行的方法是下面這種,這兩種寫法的思路是一樣的,不過下面的要更簡潔一些,雖然這種思路一開始我有些難以接受

不過還是要總結一下

1.選擇一個root節點

2.將其左節點依次加入棧中

3.輸出棧頂節點,彈出,然后將root設置為其右節點,重復1步驟

上面這些僅僅是程序的說明步驟,不能算是思路吧。不過要認真想一下的話,也許是跟人腦中遍歷樹的方式差不多,這里不做深入探討。覺得麻煩的話,可以直接背上面總結的步驟。

vector<int> preorderTraversal(TreeNode* root) {
    stack<TreeNode*> sta;
    vector<int> out;
    while (root || !sta.empty())
    {
        while (root)
        {
            sta.push(root);
            out.push_back(root->val);
            root = root->left;
        }
        if (!sta.empty())
        {
            root = sta.top();
            sta.pop();
            root = root->right;
        }
    }
    return out;
}
// 另一種寫法
vector<int> preorderTraversal(TreeNode* root) {
    stack<TreeNode*> sta;
    vector<int> out;
    while (root || !sta.empty())
    {
        if(root)
        {
            sta.push(root);
            out.push_back(root->val);
            root = root->left;
        }
        else
        {
            root = sta.top();
            sta.pop();
            root = root->right;
        }
    }
    return out;
}

另外一種實現思路,這次棧中僅僅保存右節點,因為是前序遍歷,左節點可以直接輸出,注意,這種方法僅能用於前序遍歷

vector<int> preorderTraversal(TreeNode* root) {
    vector<int> out;
    if (root == nullptr)
        return out;
    stack<TreeNode*> sta;    
    sta.push(root);
    while (!sta.empty())
    {
        out.push_back(root->val);
        if (root->right)
            sta.push(root->right);
        if (root->left)
            root = root->left;
        else
        {
            root = sta.top();
            sta.pop();
        }
    }
    return out;
}

中序遍歷其他方法(同前序遍歷):

vector<int> inorderTraversal(TreeNode* root) {
    stack<TreeNode*> sta;
    vector<int> out;
    TreeNode* cur = root;
    while (cur != nullptr || !sta.empty())
    {
        if (cur != nullptr)
        {
            sta.push(cur);
            cur = cur->left;
        }
        else
        {
            cur = sta.top();
            sta.pop();
            out.push_back(cur->val);
            cur = cur->right;
        }
    }
    return out;
}

后序遍歷只能使用保存上次root節點的或tag標記的方法

 

后記:算法的研究深不見底,你總能從中發現新的東西,越是研究的深入,就越會發現我們平時習慣了的人類思維方式是多么神奇!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM